feat(config): distiller agent, deep chat closing and weekly state jobs, distill prompt

This commit is contained in:
hh
2026-08-29 03:05:50 +02:00
parent 80806d18d8
commit 9dfb8ba201
+90
View File
@@ -9,6 +9,7 @@ from beaver_gateway.agents.base import BaseAgent, ExposedMcp
from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts from beaver_gateway.agents.claude import ClaudeAgent, ClaudeOptions, Prompts
from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences from beaver_gateway.agents.raycast import RaycastAgent, RemoteTool, UserPreferences
from beaver_gateway.core.conversations import ConversationTexts, SeedContext from beaver_gateway.core.conversations import ConversationTexts, SeedContext
from beaver_gateway.core.distill import DistillContext, Distiller, LineCap
from beaver_gateway.core.prompt import assemble from beaver_gateway.core.prompt import assemble
from beaver_gateway.core.registry import Gateway from beaver_gateway.core.registry import Gateway
from beaver_gateway.core.rotation import HandoutContext, RotationPolicy from beaver_gateway.core.rotation import HandoutContext, RotationPolicy
@@ -33,6 +34,12 @@ PROMPTS = BEAVER / "промпты"
GRANULES = PROMPTS / "гранулы" GRANULES = PROMPTS / "гранулы"
SKILLS = BEAVER / "скиллы" SKILLS = BEAVER / "скиллы"
DAYS = BEAVER / "дни" DAYS = BEAVER / "дни"
DIGESTS = BEAVER / "выжимки"
INDEX = BEAVER / "индекс.md"
STATE = BEAVER / "состояние.md"
STATE_MAX_LINES = 60
# §4.5: ночное закрытие трогает только чаты с активностью после запуска.
LAUNCH = datetime(2026, 8, 28, tzinfo=ZoneInfo(TZ))
_log = logging.getLogger("beaver_agent.config") _log = logging.getLogger("beaver_agent.config")
@@ -79,6 +86,10 @@ DISPATCHER_PROMPTS = Prompts(
fork=_DISTILL, fork=_DISTILL,
job=_DISTILL, job=_DISTILL,
) )
# §4.1: дистиллятор без голоса - иначе выжимки с матом: профиль, окружение
# джоба, оверлей. Форк закрываемого чата и недельная чистка состояния.
_DISTILLER = (PROFILE, environment("джоб"), overlay("distiller", "дистиллятор"))
DISTILLER_PROMPTS = Prompts(fork=_DISTILLER, job=_DISTILLER)
# §4.1: триаж без голоса и без vault - профиль, окружение джоба, оверлей. # §4.1: триаж без голоса и без vault - профиль, окружение джоба, оверлей.
TRIAGE_PROMPTS = Prompts(job=(PROFILE, environment("джоб"), overlay("triage", "триаж"))) TRIAGE_PROMPTS = Prompts(job=(PROFILE, environment("джоб"), overlay("triage", "триаж")))
QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый"))) QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый")))
@@ -173,6 +184,8 @@ def deep(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
cwd=VAULT, cwd=VAULT,
prompts=DEEP_PROMPTS, prompts=DEEP_PROMPTS,
skill_sets=DEEP_SKILLS, skill_sets=DEEP_SKILLS,
# §8.4: «ок, обсудили» - тулза, закрытие после ответа.
gateway_tools=("close_chat",),
options=ClaudeOptions( options=ClaudeOptions(
effort=effort, effort=effort,
# §3.7: у глубоких вопросы текстом, планов и сабагентов нет. # §3.7: у глубоких вопросы текстом, планов и сабагентов нет.
@@ -188,6 +201,21 @@ def deep(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
) )
def distiller(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
"""§4.1: форк закрытого чата - opus medium, без MCP, Read/Write в мета/бобер."""
return ClaudeAgent(
name=name,
model=model,
cwd=VAULT,
prompts=DISTILLER_PROMPTS,
options=ClaudeOptions(
effort=effort,
tools=("Read", "Write"),
disallowed_tools=("AskUserQuestion", "Task", "WebSearch", "WebFetch"),
),
)
def triage(name: str, model: str, effort: str | None = None) -> ClaudeAgent: def triage(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
"""§4.1: дешёвый крон-тёрн - sonnet low, без vault, только say/inject.""" """§4.1: дешёвый крон-тёрн - sonnet low, без vault, только say/inject."""
return ClaudeAgent( return ClaudeAgent(
@@ -230,6 +258,7 @@ agents: list[BaseAgent] = [
deep("beaver-opus-medium", "claude-opus-5", effort="medium"), deep("beaver-opus-medium", "claude-opus-5", effort="medium"),
deep("beaver-fable-medium", "claude-fable-5", effort="medium"), deep("beaver-fable-medium", "claude-fable-5", effort="medium"),
deep("beaver-opus-xhigh", "claude-opus-5", effort="xhigh"), deep("beaver-opus-xhigh", "claude-opus-5", effort="xhigh"),
distiller("beaver-distiller", "claude-opus-5", effort="medium"),
triage("beaver-triage", "claude-sonnet-5", effort="low"), triage("beaver-triage", "claude-sonnet-5", effort="low"),
raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"), raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"),
raycast( raycast(
@@ -336,11 +365,32 @@ NEW_DAY = (
"в сиде выше. Скажи Бобру что-то через say, только если есть что сказать." "в сиде выше. Скажи Бобру что-то через say, только если есть что сказать."
) )
DISTILL = (
"Глубокий чат [[{chat}]] закрыт ({reason}), сегодня {day}. Выжимка - `Write` в "
'`{path}`, фронтматтер `source: "[[{chat}]]"`, `date: {day}`; слив - текст '
"ответа, до 5 строк, третье лицо."
)
DISTILL_NO_MEMORY = (
"Глубокий чат [[{chat}]] закрыт ({reason}), сегодня {day}. Память для него "
"выключена: файл не пиши, только слив текстом ответа - до 5 строк, третье лицо."
)
def distill_prompt(ctx: DistillContext) -> str:
"""§8.4: экземплярное для дистиллятора - какой чат, куда файл."""
day = ctx.day.isoformat()
if not ctx.memory:
return DISTILL_NO_MEMORY.format(chat=ctx.chat_name, reason=ctx.reason, day=day)
path = DIGESTS / f"{day} - {slugify(ctx.chat_name, maxlen=60)}.md"
return DISTILL.format(chat=ctx.chat_name, reason=ctx.reason, day=day, path=path)
texts = ConversationTexts( texts = ConversationTexts(
merge_prompt=_read(GRANULES / "слив.md") or ConversationTexts().merge_prompt, merge_prompt=_read(GRANULES / "слив.md") or ConversationTexts().merge_prompt,
seed=seed_body, seed=seed_body,
handout=handout_prompt, handout=handout_prompt,
new_day=NEW_DAY, new_day=NEW_DAY,
distill=distill_prompt,
) )
# §4.6: конверт - полный дифф только у дневника за сегодня, остальное именами; # §4.6: конверт - полный дифф только у дневника за сегодня, остальное именами;
@@ -432,6 +482,43 @@ async def deploy(run: JobRun) -> None:
run.background(komodo_deploy()) run.background(komodo_deploy())
async def close_idle(run: JobRun) -> None:
"""§4.5, §8.4: ночью, после ротации - глубокие без активности 2 дня, ≤ 3."""
for result in await run.close_idle(kind="deep", days=2, limit=3, since=LAUNCH):
_log.info(
"closed deep chat %s: digest=%s error=%s",
result.conversation.external_id,
result.digest.path if result.digest else None,
result.error,
)
MEMORY = (
"Воскресенье, {day}. Перепиши `{state}` из последних хендаутов: {handouts}. "
"`## сейчас` - только невыводимое из vault, `## мои хвосты` - capability "
"requests в работе и обещания длиннее дня. Меньше {max_lines} строк."
)
async def memory(run: JobRun) -> None:
"""§4.5, §6.2: вс 04:30 - состояние.md из 7 хендаутов, потолок держит gateway."""
handouts = sorted(DAYS.glob("????-??-??.md"))[-7:] if DAYS.exists() else []
if not handouts:
_log.info("memory: no handouts yet, nothing to rewrite")
return
await run.spawn_job(
agent="beaver-distiller",
title="память",
text=MEMORY.format(
day=_today().isoformat(),
state=STATE.relative_to(VAULT),
handouts=", ".join(f"`{h.relative_to(VAULT)}`" for h in handouts),
max_lines=STATE_MAX_LINES,
),
line_cap=LineCap(STATE, max_lines=STATE_MAX_LINES),
)
async def komodo_alert(run: JobRun) -> None: async def komodo_alert(run: JobRun) -> None:
payload = run.payload payload = run.payload
summary = payload.get("raw") or {k: v for k, v in payload.items() if k != "trigger"} summary = payload.get("raw") or {k: v for k, v in payload.items() if k != "trigger"}
@@ -441,6 +528,8 @@ async def komodo_alert(run: JobRun) -> None:
jobs = [ jobs = [
Job("ротация", rotate, cron="0 * * * *"), Job("ротация", rotate, cron="0 * * * *"),
Job("вайбграм", vibegram, cron="*/10 * * * *", critical=False), Job("вайбграм", vibegram, cron="*/10 * * * *", critical=False),
Job("закрытие", close_idle, cron="20 4 * * *", critical=False),
Job("память", memory, cron="30 4 * * 0", critical=False),
Job("deploy", deploy, webhook=True), Job("deploy", deploy, webhook=True),
Job("komodo", komodo_alert, webhook=True), Job("komodo", komodo_alert, webhook=True),
] ]
@@ -454,6 +543,7 @@ gateway = Gateway(
rotation=RotationPolicy(tz=TZ), rotation=RotationPolicy(tz=TZ),
watch=watch, watch=watch,
budget=Budget(threshold=0.7), budget=Budget(threshold=0.7),
distiller=Distiller(agent="beaver-distiller", dir=DIGESTS, index=INDEX),
tz=TZ, tz=TZ,
port=62990, port=62990,
public_url=PUBLIC_BASE_URL or None, public_url=PUBLIC_BASE_URL or None,