feat(core): seeds without text wait for the first turn

This commit is contained in:
hh
2026-08-28 19:03:08 +02:00
parent 38986cb82e
commit c88726db9e
3 changed files with 42 additions and 12 deletions
+33 -10
View File
@@ -38,7 +38,7 @@ from claude_agent_sdk import (
from sqlmodel import col, select
from beaver_gateway.core.injects import InjectQueue, inject_header
from beaver_gateway.core.kinds import KINDS, Kind
from beaver_gateway.core.kinds import KINDS, Kind, as_kind
from beaver_gateway.core.transcript import (
messages_from_entries,
render_messages,
@@ -492,7 +492,9 @@ class Conversations:
``binding`` = ``(frontend, external_id)`` puts it into a window that
already exists (a topic the user created) instead of asking the
home frontend to ``materialize`` one. ``text`` rides with the seed
as the first thing the user said, whatever the seed mode.
as the first thing the user said, whatever the seed mode; without
it the seed waits in ``flags`` and opens the first turn, so a fresh
window stays silent and costs nothing until someone speaks.
"""
if seed not in SEEDS:
msg = f"unknown seed {seed!r}"
@@ -526,21 +528,41 @@ class Conversations:
await self.bind(conv, frontend=binding[0], external_id=binding[1])
else:
await self.materialize(conv)
prompt = await self._seed_text(
SeedContext(
ctx = SeedContext(
kind=kind, seed=seed, agent=agent, parent=parent, text=text, title=title
),
window=window,
)
if text is None:
return await self.set_flags(conv, {"seed": seed, "seed_window": window})
await self._queue.push(
conversation_id=cast("int", conv.id),
priority="user",
origin=origin if text and seed != "brief" else f"сид:{seed}",
text=prompt,
origin=f"сид:{seed}" if seed == "brief" else origin,
text=await self._seed_text(ctx, window=window),
)
self._ensure_worker(cast("int", conv.id))
return conv
async def _pending_seed(self, conv: Conversation) -> str | None:
"""A seed nobody has spoken after yet: rendered now, spent once."""
seed = conv.flags.get("seed")
if not seed:
return None
parent = await self.get_row(conv.parent_id) if conv.parent_id else None
ctx = SeedContext(
kind=as_kind(conv.kind),
seed=str(seed),
agent=conv.agent_name,
parent=parent,
text=None,
title=conv.title,
)
window = conv.flags.get("seed_window")
text = await self._seed_text(
ctx, window=window if isinstance(window, int) else None
)
await self.set_flags(conv, {"seed": None, "seed_window": None})
return text
async def fork(
self,
conv: Conversation,
@@ -983,8 +1005,6 @@ class Conversations:
stamp = datetime.now(UTC).astimezone().strftime("%Y-%m-%d %H:%M")
title = f" «{ctx.title}»" if ctx.title else ""
head = f"[сид: {ctx.seed}] {ctx.kind}{title}, {stamp}."
if not ctx.text or ctx.seed == "brief":
head += " Это контекст, не обращение: отвечать не нужно."
body: str | None = None
if self._texts.seed is not None:
produced: Any = self._texts.seed(ctx)
@@ -1065,6 +1085,9 @@ class Conversations:
else:
origin = "inject"
prompt = "\n\n".join(f"{inject_header(i.origin)}\n{i.text}" for i in batch)
seed = await self._pending_seed(conv)
if seed:
prompt = f"{seed}\n\n{prompt}"
try:
text, capture = await self.run_text_turn(
conv, prompt, origin=origin, turn_id=turn_id, item_origin=head.origin
+6
View File
@@ -396,8 +396,14 @@ async def test_copy_seed_forks_parent_with_window(world: World) -> None:
"q6",
[{"type": "text", "text": "a7"}],
]
assert (await world.conversations.get(child.external_id)).flags["seed"] == "copy"
await world.conversations.post(child, "go")
await world.settle(child, 1)
assert ScriptedClient.instances[0].options.resume == child.session_id
prompt = ScriptedClient.instances[0].prompts[0]
assert prompt.startswith("[сид: copy] branch, ")
assert "последние 1 тёрнов" in prompt and prompt.endswith("\n\ngo")
assert (await world.conversations.get(child.external_id)).flags["seed"] is None
async def test_merge_injects_summary_into_parent(world: World) -> None:
+2 -1
View File
@@ -462,13 +462,14 @@ async def test_commands_status_merge_and_new(stack: Stack) -> None:
frontend="telegram", external_id=f"{USER}/902"
)
assert child is not None and child.title == "отчёт"
await stack.world.settle(child, 1)
await asyncio.sleep(0.2)
assert await stack.world.conversations.queue.recent(child.id) == []
assert all(m["thread"] != 902 for m in stack.bot.sent)
assert all(d["message_thread_id"] != 902 for d in stack.bot.drafts)
stack.bot.message("первое в новый топик", thread=902)
reply = await stack.until(lambda: stack.sent_with("первое в новый топик"), what="r")
assert reply["thread"] == 902
assert reply["text"].startswith("ok:[сид: morning] branch «отчёт»")
assert await stack.tg.mark_topic(child)
assert stack.bot.topics[-1] == "edit:902:✅ отчёт"