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:
0xallam
2026-04-25 16:54:36 -07:00
parent fc96716956
commit 00f5ab33d6
5 changed files with 96 additions and 4 deletions
+1
View File
@@ -450,6 +450,7 @@ async def create_agent(
model_settings=inner.get("model_settings"),
max_turns=int(inner.get("max_turns", 300)),
is_whitebox=bool(inner.get("is_whitebox", False)),
interactive=bool(inner.get("interactive", False)),
diff_scope=inner.get("diff_scope"),
run_id=inner.get("run_id"),
agent_factory=factory,