Two routes share one AgentCore unit, selected by intent. This PR fills in intent:"converse" behind the door Story-A froze; the command route stays scaffolded (Story-C). The change is deliberately additive: the dispatch grew one branch, and a new engine file (converse.py) carries all the new behaviour — a sibling Mira Story touches the same runtime in parallel, so nothing existing was rewritten.
Conversation is read-only. The route reasons over the frame and streams markdown; it never proposes a frame, never runs the panel, never writes anything. That is not a comment — it is the shape of the output. The turn is Mira's MiraConverseTurn: markdown plus an (unused-in-v1) suggested_command slot. There is no frame, no changed_fields, no requires_counsel_approval on any converse event — a test asserts none of those keys can appear.
str output that cannot emit a frame; command (Story-C) is the panel with a FrameOutput output that always requires counsel approval.runtime.stream_invoke # Bug-1370 boundary watchdog (brain on a private thread) └► handle_request # parse BEFORE any event (fail-closed) └─ intent="converse" ─► converse.stream_converse(run_id, MiraConverseInput, model_id) run.started # stamps converse model + prompt_version + mira_converse.v1 turn.delta ×N # streamed markdown — real run_stream().stream_text(delta=True) model.usage # real token totals (feeds the broker cost cap) turn.completed # {turn:{markdown, suggested_command:null}} run.completed # stop_reason:"end_turn" # OR, on a stall/provider error: run.started → run.failed{failure_stage:"converse", ...}
The output type is plain str (markdown), streamed via agent.run_stream(...).stream_text(delta=True). Bug-1370 — a wedged OpenRouter stream freezing the AgentCore worker loop — is specific to the flat-DTO PromptedOutput / stream_output path. Plain-text streaming is a different code path, and it is already proven in this repo (agents/atlas/live_feed.py streams text over OpenRouter ZDR on the same SDK). Full reasoning: docs/notes/mira-converse-streaming-decision.md.
run() fallback would be dead code; if a specific model ever hangs its text stream, the per-attempt deadline already turns that into a typed error turn, and the fallback becomes a localized change.| Deadline | Wraps | On trip |
|---|---|---|
MIRA_CONVERSE_ATTEMPT_TIMEOUT_S (20s) | only the model call (asyncio.wait_for(_drive())) — consumer time not counted | run.failed · failure_kind:"timeout" |
MIRA_INVOKE_BUDGET_S (780s) | the whole invoke, on the worker loop (the Bug-1370 watchdog) | run.failed · failure_kind:"invoke_budget_exceeded" |
Counsel is typing live, so first-token latency is what matters; 20s is the anti-hang backstop, not the streaming budget. Any provider error is caught the same way — a typed failure_kind:"model_error" turn (Atlas's error taxonomy), never a raised exception that reads as a frozen spinner.
The prompt is small on purpose (RFD §2.1): light matter metadata + the current frame (as JSON) + recent history + the counsel message. No documents body — the frame is the compressed matter context, which keeps the turn fast. Every untrusted piece rides inside a per-run nonce delimiter (<<MATTER_FRAME_9f3a1c>>…), and the system prompt's input-channel rule treats everything inside <<…>> as data, never instructions — so an adversarial line in a frame can't make Mira leave this matter, invent a fact, or act on the world.
| Test | Guards |
|---|---|
| test_converse.py · full sequence | Drives stream_converse against a real Pydantic AI TestModel: asserts run.started → turn.delta×N → model.usage → turn.completed → run.completed, correct stamps, and that the streamed deltas reassemble to the final markdown. |
| test_converse.py · read-only | The turn carries only {markdown, suggested_command}; no frame/changed_fields/requires_counsel_approval/frame_status leaks on any event. |
| test_converse.py · timeout + error | A stalled agent trips the per-attempt deadline → typed timeout turn; a raising agent → typed model_error turn. Never an unhandled raise. |
| test_runtime.py · dispatch | converse hands the parsed input to stream_converse and relays its frames (no not_wired tail); command stays scaffolded; the watchdog still ships a typed terminal on a wedged brain. |
# the whole run, green, offline uv run --package mira pytest agents/mira/tests/ -q 34 passed
Acceptance: streams first token fast → terminates in turn.completed · no frame mutation · matter-scoped · typed error turn on timeout/provider error · streaming-vs-Bug-1370 decision recorded (docs/notes) · fast ZDR model rostered in llm.yaml · uv run --package mira pytest green.
TestModel exercises the real SDK streaming path so the plumbing is proven; live latency/ZDR-availability is a deploy-time check (Logfire shows invoke_agent under pramaan-agents-mira once deployed).