feat(core): seeds without text wait for the first turn
This commit is contained in:
@@ -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(
|
||||
kind=kind, seed=seed, agent=agent, parent=parent, text=text, title=title
|
||||
),
|
||||
window=window,
|
||||
ctx = SeedContext(
|
||||
kind=kind, seed=seed, agent=agent, parent=parent, text=text, title=title
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:✅ отчёт"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user