feat(config,curator,recall): memory curator job on opus high, notes head inline in the envelope, portrait in the morning seed

This commit is contained in:
hh
2026-09-01 16:48:25 +02:00
parent 58e343aae0
commit 3a77a12447
7 changed files with 375 additions and 27 deletions
+64 -1
View File
@@ -30,6 +30,7 @@ from beaver_gateway.frontends.mcp_server import McpServerFrontend
from beaver_gateway.frontends.telegram import TelegramFrontend
from beaver_gateway.mcp.types import HttpMcp, McpServer, McpServerT
from curator import Watched, briefing
from mcps.homeassistant import HomeAssistant
from mcps.komodo import Komodo
from mcps.komodo_alerts import HOLD, AlertMemory, describe, parse, still_bad
@@ -45,7 +46,7 @@ from policy import (
skill_tracker,
vault_zones,
)
from recall import Recall, ReplyLog
from recall import Recall, ReplyLog, notes_head
TZ = "Europe/Warsaw"
VAULT = Path("/vault")
@@ -60,6 +61,8 @@ DIGESTS = BEAVER / "выжимки"
INDEX = BEAVER / "индекс.md"
NOTES = BEAVER / "наблюдения"
REPLIES = BEAVER / "реплики"
CURATOR_JOURNAL = BEAVER / "куратор.md"
PORTRAIT = NOTES / "Бобёр - наблюдения.md"
PEOPLE = VAULT / "👤 люди"
BOARDS = VAULT / "📆 доски"
STATE = BEAVER / "состояние.md"
@@ -118,6 +121,10 @@ _DISTILLER = (PROFILE, environment("джоб"), overlay("distiller", "дисти
DISTILLER_PROMPTS = Prompts(fork=_DISTILLER, job=_DISTILLER)
# §4.1: триаж без голоса и без vault - профиль, окружение джоба, оверлей.
TRIAGE_PROMPTS = Prompts(job=(PROFILE, environment("джоб"), overlay("triage", "триаж")))
# Куратор памяти - тот же менеджер бобрения (голос и всё), в окружении джоба.
CURATOR_PROMPTS = Prompts(
job=(*_BASE, environment("джоб"), overlay("curator", "куратор"))
)
QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый")))
# §4.3: наборы скиллов = папки = плагины. Глубокие грузят общие + vault.
@@ -321,6 +328,23 @@ def distiller(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
)
def curator(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
"""Куратор памяти: opus high, файлы в мета/бобер, без MCP, `inject` в мастер."""
return ClaudeAgent(
name=name,
model=model,
cwd=VAULT,
prompts=CURATOR_PROMPTS,
gateway_tools=("inject",),
options=ClaudeOptions(
effort=effort,
tools=("Read", "Write", "Edit", "Grep", "Glob"),
disallowed_tools=DISTILLER_DISALLOWED,
),
policy=(vault_zones(STRICT_ZONES),),
)
def triage(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
"""§4.1: дешёвый крон-тёрн - sonnet low, без vault, только say/inject."""
return ClaudeAgent(
@@ -364,6 +388,7 @@ agents: list[BaseAgent] = [
deep("beaver-fable-medium", "claude-fable-5", effort="medium"),
deep("beaver-opus-xhigh", "claude-opus-5", effort="xhigh"),
distiller("beaver-distiller", "claude-opus-5", effort="medium"),
curator("beaver-curator", "claude-opus-5", effort="high"),
triage("beaver-triage", "claude-sonnet-5", effort="low"),
raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"),
raycast(
@@ -442,6 +467,12 @@ def seed_body(ctx: SeedContext) -> str | None:
parts.append(
f"Состояние (первые строки, полностью в мета/бобер/состояние.md):\n{signal}"
)
portrait = notes_head(PORTRAIT, ("сейчас", "паттерны"), cap=40)
if portrait:
parts.append(
"Портрет Бобра (шапка "
f"`{PORTRAIT.relative_to(VAULT)}`, лента - там же):\n{portrait}"
)
return "\n\n".join(parts)
@@ -686,6 +717,35 @@ async def memory(run: JobRun) -> None:
)
CURATOR_WATCH = [
Watched("хендауты", DAYS, "????-??-??.md"),
Watched("выжимки", DIGESTS, "*.md"),
Watched("наблюдения", NOTES, "*.md"),
Watched("состояние", BEAVER, "состояние.md"),
Watched("дневник", DIARY, "????-??-??.md"),
Watched("карточки", PEOPLE, "**/*.md"),
]
async def curate(run: JobRun) -> None:
"""§6.0: куратор памяти раз в несколько часов.
Крон собирает «что нового», модель раскладывает по шапкам и лентам;
нечего раскладывать - джоб не спавнится.
"""
text = briefing(
vault=VAULT,
journal=CURATOR_JOURNAL,
replies_dir=REPLIES,
watched=CURATOR_WATCH,
tz=TZ,
)
if text is None:
_log.info("curator: nothing new since the last run")
return
await run.spawn_job(agent="beaver-curator", title="куратор", text=text)
T3CODE_QUESTION_HINT = (
"Если ответ следует из задания - ответь сам: t3_answer(thread_id, "
"{id вопроса: label}). Не знаешь - спроси Бобра через say своими словами, "
@@ -790,6 +850,9 @@ jobs = [
Job("вайбграм", vibegram, cron="*/10 * * * *", critical=False),
Job("закрытие", close_idle, cron="20 4 * * *", critical=False),
Job("память", memory, cron="30 4 * * 0", critical=False),
# крон в UTC контейнера: 02/10/14/18/22 по Варшаве летом;
# руками - POST /api/jobs/куратор/run
Job("куратор", curate, cron="0 0,8,12,16,20 * * *", critical=False),
Job("deploy", deploy, webhook=True),
# алерты идут пачками (тревога и отбой подряд) - схлопывать нельзя
Job("komodo", komodo_alert, webhook=True, dedupe=False),