feat(entry): interactive mode keeps the root agent alive across cycles
Pre-migration ``BaseAgent.agent_loop`` ran forever in interactive mode, re-entering a "waiting state" after each finish-tool call so user follow-ups could keep the conversation going. Post-migration our ``Runner.run`` returned on ``StopAtTools(finish_scan)`` and the user's next chat message had no listener — silent dead-end. Restore the legacy "agent never dies" semantics using the SDK's canonical demo-loop pattern (``agents/repl.py:run_demo_loop``): - Add ``AgentMessageBus.wait_for_message(agent_id)`` — blocks until an inbox is non-empty. Backed by a per-agent ``asyncio.Event`` fired from ``send``. - Add ``AgentMessageBus.park(agent_id)`` — sets status to ``waiting`` without finalizing (inbox + tree edges + name preserved). Lets ``send`` keep accepting messages between cycles. - Plumb ``interactive`` through ``make_agent_context`` and the ``create_agent`` graph tool (children inherit). - ``StrixOrchestrationHooks.on_agent_end`` parks the root agent instead of finalizing when ``interactive=True`` and the run completed cleanly. Resets ``agent_finish_called`` / ``turn_count`` for the next cycle. - ``entry.run_strix_scan`` adds an outer loop in interactive mode: after ``Runner.run`` returns, ``await bus.wait_for_message(root_id)``, drain pending user messages, and re-invoke ``Runner.run``. SQLite session preserves prior conversation across cycles. For non-interactive (CLI) mode: unchanged — single ``Runner.run``, return. Verified bus behaviors: wait returns immediately on pre-existing message, blocks then wakes on send, ``park`` keeps agent send-able, ``finalize`` evicts. Lint at baseline (3 ruff / 69 mypy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ class AgentMessageBus:
|
||||
names: dict[str, str] = field(default_factory=dict)
|
||||
stats_live: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
stats_completed: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
_events: dict[str, asyncio.Event] = field(default_factory=dict)
|
||||
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
|
||||
async def register(
|
||||
@@ -72,6 +73,23 @@ class AgentMessageBus:
|
||||
if self.statuses[target] in ("completed", "crashed", "stopped"):
|
||||
return
|
||||
self.inboxes.setdefault(target, []).append(msg)
|
||||
event = self._events.get(target)
|
||||
if event is not None:
|
||||
event.set()
|
||||
|
||||
async def wait_for_message(self, agent_id: str) -> None:
|
||||
"""Block until ``agent_id``'s inbox has at least one pending message.
|
||||
|
||||
Used by the interactive-mode outer loop in :func:`run_strix_scan` to
|
||||
wake on the next user message between ``Runner.run`` cycles. Cheap
|
||||
if the inbox already has content (returns immediately).
|
||||
"""
|
||||
async with self._lock:
|
||||
if self.inboxes.get(agent_id):
|
||||
return
|
||||
event = self._events.setdefault(agent_id, asyncio.Event())
|
||||
event.clear()
|
||||
await event.wait()
|
||||
|
||||
async def drain(self, agent_id: str) -> list[dict[str, Any]]:
|
||||
"""Atomically read and clear ``agent_id``'s pending messages.
|
||||
@@ -117,6 +135,20 @@ class AgentMessageBus:
|
||||
self.inboxes.pop(agent_id, None)
|
||||
self.parent_of.pop(agent_id, None)
|
||||
self.names.pop(agent_id, None)
|
||||
self._events.pop(agent_id, None)
|
||||
|
||||
async def park(self, agent_id: str) -> None:
|
||||
"""Mark an agent as ``waiting`` without finalizing.
|
||||
|
||||
Used in interactive mode for the root agent between ``Runner.run``
|
||||
cycles: the run completed, but the agent stays alive on the bus
|
||||
so user messages still land in its inbox until the next cycle
|
||||
starts. Stats stay live (will be merged on actual finalize at
|
||||
scan teardown).
|
||||
"""
|
||||
async with self._lock:
|
||||
if agent_id in self.statuses:
|
||||
self.statuses[agent_id] = "waiting"
|
||||
|
||||
async def total_stats(self) -> dict[str, Any]:
|
||||
"""Snapshot of live + completed stats."""
|
||||
|
||||
Reference in New Issue
Block a user