fix(backends,conversations,telegram,ui): a background subagent reports back as its own turn
This commit is contained in:
1 parent
2afd3bc3c1
commit
8019c76e53
15 files changed
+608
-91
No files matched your search
@@ -54,8 +54,15 @@ class Turns(Seeds):
|
||||
tools: bool = True,
|
||||
turn_id: str | None = None,
|
||||
item_origin: str | None = None,
|
||||
shown: str | None = None,
|
||||
attach: bool = False,
|
||||
) -> AsyncIterator[MessageStreamEvent]:
|
||||
"""Run one turn under the conversation's lock; the only path to the backend."""
|
||||
"""Run one turn under the conversation's lock; the only path to the backend.
|
||||
|
||||
``shown`` is what frontends display as the turn's prompt (the user's
|
||||
words, not the envelope). ``attach`` sends nothing: the turn is one
|
||||
the live session started by itself, a subagent reporting back.
|
||||
"""
|
||||
row_id = cast("int", conv.id)
|
||||
runner = self._runner(row_id)
|
||||
backend = self._backend(conv.agent_name)
|
||||
@@ -65,7 +72,7 @@ class Turns(Seeds):
|
||||
async with runner.lock:
|
||||
runner.turn_id = turn_id
|
||||
runner.origin = origin
|
||||
runner.text = _prompt_preview(messages)
|
||||
runner.text = shown if shown is not None else _prompt_preview(messages)
|
||||
runner.started_at = datetime.now(UTC)
|
||||
runner.tools = {}
|
||||
await self._mark_running(conv, turn_id)
|
||||
@@ -81,18 +88,28 @@ class Turns(Seeds):
|
||||
stop = "error"
|
||||
cut = False
|
||||
try:
|
||||
events = backend.complete(
|
||||
agent=self._claude_agent(conv.agent_name),
|
||||
messages=messages,
|
||||
conversation_id=conv.external_id,
|
||||
session_id=resume if use_session else None,
|
||||
reseed=not use_session,
|
||||
capture=capture,
|
||||
kind=conv.kind,
|
||||
pinned=conv.kind == "master",
|
||||
tools=tools,
|
||||
observer=self._observer(conv, runner, turn_id, origin),
|
||||
turn_id=turn_id,
|
||||
observer = self._observer(conv, runner, turn_id, origin)
|
||||
events = (
|
||||
backend.attach(
|
||||
key=conv.external_id,
|
||||
capture=capture,
|
||||
observer=observer,
|
||||
turn_id=turn_id,
|
||||
)
|
||||
if attach
|
||||
else backend.complete(
|
||||
agent=self._claude_agent(conv.agent_name),
|
||||
messages=messages,
|
||||
conversation_id=conv.external_id,
|
||||
session_id=resume if use_session else None,
|
||||
reseed=not use_session,
|
||||
capture=capture,
|
||||
kind=conv.kind,
|
||||
pinned=conv.kind == "master",
|
||||
tools=tools,
|
||||
observer=observer,
|
||||
turn_id=turn_id,
|
||||
)
|
||||
)
|
||||
async for event in events:
|
||||
yield event
|
||||
@@ -128,6 +145,8 @@ class Turns(Seeds):
|
||||
turn_id: str | None = None,
|
||||
item_origin: str | None = None,
|
||||
attachments: Sequence[dict[str, Any]] | None = None,
|
||||
shown: str | None = None,
|
||||
attach: bool = False,
|
||||
) -> tuple[str, TurnCapture]:
|
||||
capture = TurnCapture()
|
||||
acc = StreamAccumulator()
|
||||
@@ -140,6 +159,8 @@ class Turns(Seeds):
|
||||
tools=tools,
|
||||
turn_id=turn_id,
|
||||
item_origin=item_origin,
|
||||
shown=shown,
|
||||
attach=attach,
|
||||
):
|
||||
acc.feed(event)
|
||||
message = acc.finalize(model=agent.model)
|
||||
@@ -160,6 +181,47 @@ class Turns(Seeds):
|
||||
runner = self._runners.get(cast("int", conv.id))
|
||||
return runner.origin if runner is not None and runner.turn_id else None
|
||||
|
||||
async def _woken(self, key: str, note: str) -> None:
|
||||
"""The live session of ``key`` started a turn by itself; run it next."""
|
||||
conv = await self.get(key)
|
||||
if conv is None:
|
||||
_log.warning("self-started turn on unknown conversation %s: %s", key, note)
|
||||
return
|
||||
_log.info("conversation %s woke by itself: %s", key, note)
|
||||
self._ensure_worker(cast("int", conv.id))
|
||||
|
||||
async def _run_pending(self, conv: Conversation) -> None:
|
||||
turn_id = f"turn_{uuid4().hex[:12]}"
|
||||
note = self._backend(conv.agent_name).pending_note(conv.external_id) or ""
|
||||
try:
|
||||
text, _ = await self.run_text_turn(
|
||||
conv,
|
||||
note,
|
||||
origin="task",
|
||||
turn_id=turn_id,
|
||||
item_origin="task",
|
||||
shown=note,
|
||||
attach=True,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception(
|
||||
"self-started turn %s on %s failed", turn_id, conv.external_id
|
||||
)
|
||||
return
|
||||
if not text.strip():
|
||||
return
|
||||
self._bus.publish(
|
||||
"reply",
|
||||
conversation_id=conv.external_id,
|
||||
turn_id=turn_id,
|
||||
item=None,
|
||||
item_origin="task",
|
||||
source="task",
|
||||
prompt=self._texts.task_prompt.format(note=note),
|
||||
user_text=None,
|
||||
text=text,
|
||||
)
|
||||
|
||||
def _ensure_worker(self, row_id: int) -> None:
|
||||
runner = self._runner(row_id)
|
||||
runner.wake.set()
|
||||
@@ -170,6 +232,10 @@ class Turns(Seeds):
|
||||
async def _worker(self, row_id: int) -> None:
|
||||
runner = self._runner(row_id)
|
||||
while True:
|
||||
conv = await self.get_row(row_id)
|
||||
if conv is not None and self._has_pending(conv):
|
||||
await self._run_pending(conv)
|
||||
continue
|
||||
items = await self._queue.pending(row_id)
|
||||
batch, wait = self._pick(items)
|
||||
if batch is None:
|
||||
@@ -233,6 +299,7 @@ class Turns(Seeds):
|
||||
turn_id=turn_id,
|
||||
item_origin=head.origin,
|
||||
attachments=head.attachments if origin == "user" else None,
|
||||
shown=head.text if origin == "user" else None,
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
_log.exception("turn %s on %s failed", turn_id, conv.external_id)
|
||||
@@ -254,6 +321,12 @@ class Turns(Seeds):
|
||||
text=text,
|
||||
)
|
||||
|
||||
def _has_pending(self, conv: Conversation) -> bool:
|
||||
try:
|
||||
return self._backend(conv.agent_name).has_pending(conv.external_id)
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
def _bundle(self, items: Sequence[InjectQueueItem]) -> str:
|
||||
lines = [self._texts.bundle_header.format(since=iso(items[0].created_at))]
|
||||
lines.extend(f"- [{i.origin}] {i.text}" for i in items)
|
||||
|
||||
Reference in new issue
Block a user