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.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,
|
||||||
|
|||||||
Reference in New Issue
Block a user