feat(config): distiller agent, deep chat closing and weekly state jobs, distill prompt
This commit is contained in:
@@ -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.raycast import RaycastAgent, RemoteTool, UserPreferences
|
||||
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.registry import Gateway
|
||||
from beaver_gateway.core.rotation import HandoutContext, RotationPolicy
|
||||
@@ -33,6 +34,12 @@ PROMPTS = BEAVER / "промпты"
|
||||
GRANULES = PROMPTS / "гранулы"
|
||||
SKILLS = 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")
|
||||
|
||||
@@ -79,6 +86,10 @@ DISPATCHER_PROMPTS = Prompts(
|
||||
fork=_DISTILL,
|
||||
job=_DISTILL,
|
||||
)
|
||||
# §4.1: дистиллятор без голоса - иначе выжимки с матом: профиль, окружение
|
||||
# джоба, оверлей. Форк закрываемого чата и недельная чистка состояния.
|
||||
_DISTILLER = (PROFILE, environment("джоб"), overlay("distiller", "дистиллятор"))
|
||||
DISTILLER_PROMPTS = Prompts(fork=_DISTILLER, job=_DISTILLER)
|
||||
# §4.1: триаж без голоса и без vault - профиль, окружение джоба, оверлей.
|
||||
TRIAGE_PROMPTS = Prompts(job=(PROFILE, environment("джоб"), overlay("triage", "триаж")))
|
||||
QUICK_PROMPT = assemble((*VOICE, PROFILE, overlay("quick", "быстрый")))
|
||||
@@ -173,6 +184,8 @@ def deep(name: str, model: str, effort: str | None = None) -> ClaudeAgent:
|
||||
cwd=VAULT,
|
||||
prompts=DEEP_PROMPTS,
|
||||
skill_sets=DEEP_SKILLS,
|
||||
# §8.4: «ок, обсудили» - тулза, закрытие после ответа.
|
||||
gateway_tools=("close_chat",),
|
||||
options=ClaudeOptions(
|
||||
effort=effort,
|
||||
# §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:
|
||||
"""§4.1: дешёвый крон-тёрн - sonnet low, без vault, только say/inject."""
|
||||
return ClaudeAgent(
|
||||
@@ -230,6 +258,7 @@ agents: list[BaseAgent] = [
|
||||
deep("beaver-opus-medium", "claude-opus-5", effort="medium"),
|
||||
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"),
|
||||
triage("beaver-triage", "claude-sonnet-5", effort="low"),
|
||||
raycast("beaver-gemini-pro-low", "google-gemini-3.1-pro", reasoning_effort="low"),
|
||||
raycast(
|
||||
@@ -336,11 +365,32 @@ NEW_DAY = (
|
||||
"в сиде выше. Скажи Бобру что-то через 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(
|
||||
merge_prompt=_read(GRANULES / "слив.md") or ConversationTexts().merge_prompt,
|
||||
seed=seed_body,
|
||||
handout=handout_prompt,
|
||||
new_day=NEW_DAY,
|
||||
distill=distill_prompt,
|
||||
)
|
||||
|
||||
# §4.6: конверт - полный дифф только у дневника за сегодня, остальное именами;
|
||||
@@ -432,6 +482,43 @@ async def deploy(run: JobRun) -> None:
|
||||
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:
|
||||
payload = run.payload
|
||||
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 = [
|
||||
Job("ротация", rotate, cron="0 * * * *"),
|
||||
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("komodo", komodo_alert, webhook=True),
|
||||
]
|
||||
@@ -454,6 +543,7 @@ gateway = Gateway(
|
||||
rotation=RotationPolicy(tz=TZ),
|
||||
watch=watch,
|
||||
budget=Budget(threshold=0.7),
|
||||
distiller=Distiller(agent="beaver-distiller", dir=DIGESTS, index=INDEX),
|
||||
tz=TZ,
|
||||
port=62990,
|
||||
public_url=PUBLIC_BASE_URL or None,
|
||||
|
||||
Reference in New Issue
Block a user