Mira gets a body — but not yet a voice

Mira is the head of one matter: she will talk (fast streaming chat, replacing Atlas's matter-facing chat) and she will do (a command runs Vega's exact panel to land a proposed frame edit). This Story — the KEYSTONE of the Mira epic — ships neither behaviour. It ships the skeleton: Vega's runtime shell retargeted for Mira, plus the frozen typed contracts every downstream Story and the parallel pramaan-app CCB build against. Everything else in the epic depends on this landing first.
Story-2022 (Story-A) · EPIC-2021 (Mira) · mirrors agents/framer/ (Vega) · reuses the frozen FrameOutput · pramaan-docs ADR-110 (event stream / thin brain) · ADR-117 (Pydantic AI) · ADR-151/116/120 (config-driven OpenRouter, ZDR) · ADR-118/124 (observability) · ADR-121 (secrets) · repo pramaan-agents · design docs/notes/rfd-mira-matter-agent.md §9

Mira mirrors Vega's golden skeleton on purpose (RFD-AGT-007 §1.1): same broker-owned control plane, same agent_event_stream.v1 wire, same fail-closed entrypoint, same dedicated Logfire lane. The one true architecture (ADR-110) does not get re-invented per agent — a new agent is Vega's shell with a new soul bolted on. So this PR is almost entirely mechanism copied + retargeted (MIRA_* env, pramaan-agents-mira service, mira logger/key), and a small amount of genuinely new soul: Mira's identity, persona, and her two typed input contracts.

Why a scaffold ships at all, before any behaviour: the epic has a sibling app Story building the Command Confirm Bar + conversation panel in parallel (RFD §5). Those teams cannot start until the two input shapes and the command output shape are frozen. Landing the contracts first — with a runtime that fail-closes honestly on every route — unblocks that parallel work while Stories B (converse) and C (command) fill in the engines behind the same frozen doors.

1 · What was copied (mechanism) vs written (soul)

FileKindWhat it is
provider.pycopiedByte-identical to Vega: ZDR_SETTINGS (zdr + data_collection:deny + usage:include) pinned in ONE place, build_model. OpenRouter is the only egress (ADR-120).
observability.pyretargetedDedicated Logfire lane (Bug-1294 two-lane rule) + AmbientIdentitySpanProcessor. Retargeted: agent.key=mira, gen_ai.agent.name=Mira, service.name=pramaan-agents-mira. No baggage, no content.
secrets_bootstrap.pyretargetedThe one fenced boto3/Secrets Manager read (MIRA_REGION default). Fail-safe, only-if-unset.
config.pyretargetedRole loader over llm.yaml, env override MIRA_<ROLE>_MODEL. Roles: converse / proposer[] / critic / synthesizer. Adds the conversation route's own tight deadline (MIRA_CONVERSE_ATTEMPT_TIMEOUT_S).
events.pycopiedThe frozen agent_event_stream.v1 builders — no per-agent dialect.
runtime.pyscaffoldThe Bug-1370 boundary watchdog (real), around a handle_request that validates the envelope + per-intent input, then emits a typed not_implemented tail (behaviour is B/C).
main.pyretargetedAgentCore @app.entrypoint: secrets → Logfire → fail closed on missing input AND intent → stream JSONL.
identity.py · SOUL.md · assets/soulMira's persona: MIRA = AgentIdentity(key="mira", display_name="Mira", role_line="MATTER EXPERT", …), the voice (composed into the synthesizer only), the vendored avatar.
models.py · frame_contract.pysoulThe two typed inputs + the conversation turn; the vendored frozen FrameOutput reused as command output.

2 · One unit, two doors — selected by intent

# broker invoke envelope (ADR-110 §3): {firm_id, matter_id, agent_run_id, trace…, intent, input}
main.py  # fail closed if 'input' OR 'intent' missing — no default route, no demo fallback
  └► runtime.stream_invoke        # Bug-1370 watchdog (real shell B/C build on)
      └► handle_request           # parse BEFORE run.started
          ├─ intent="converse" ─► MiraConverseInput  → model_id_for("converse")  # the fast model
          └─ intent="command"  ─► MiraCommandInput   → model_id_for("synthesizer") # Vega's panel seat
# Story-A response: run.started → run.failed{failure_kind:"not_implemented"}  (engines land in B/C)
Why fail-closed on intent, not just input: Vega is ambient — every invoke is a build, so it needs only input. Mira has two routes with opposite cost/latency shapes; a route the caller did not choose must never be guessed. A missing or unknown intent raises before any event (mirrors Vega's input contract), so a malformed invoke errors loudly instead of silently running the wrong — possibly expensive — panel. Story-D wires the dispatch to the real engines; the fail-closed frame is already here.

3 · Reusing the frozen frame without breaking the CodeZip boundary

Mira's command output is Vega's FrameOutput, unchanged (RFD §4) — she invents no frame shape. But each agent is an independently-deployable CodeZip with no cross-agent runtime import (ADR-110 §6), and the members' top-level module names collide (both have a models). So the contract is vendored at source into frame_contract.py, exactly as mechanism is copied today — the shared-agent-code spike folds these into agents/_shared/ later.

Why vendoring is safe here (drift is fenced, not trusted): the single source of truth stays agents/framer/models.py. Its committed vocabulary artifact schemas/frame_output.v1.fields.json — the same one pramaan-functions codegens against (ADR-153) — is read by tests/test_frame_contract_custody.py, which fails CI if Mira's vendored copy diverges by even one field. A duplicate with a guard is not drift; a duplicate without one is.

4 · The typed contracts (frozen for the parallel app build)

TypeRouteShape
MiraConverseInputtalkmatter_id, current_frame: FrameOutput, counsel_message, history: [MiraMessage], matter_meta?. READ-ONLY; the frame IS the compressed matter context (small, fast prompt).
MiraConverseTurntalkmarkdown + optional suggested_command{scope, text} — the "Do it" escalation chip (RFD §2.3).
MiraCommandInputdocommand, scope (a FrameOutput field name or "brief"), current_frame, base_frame_version? (the stale-base race key, RFD §6), optional documents.
FRAME_SCOPESdoThe closed command vocabulary — frozenset(FrameOutput.model_fields) | {"brief"}, derived from the frozen contract so it can never drift from it. Enforced in code in Story-C.

5 · What was deliberately NOT built

No conversation engine (Story-B) and no command engine / panel wiring (Story-C) — handle_request emits a typed not_implemented until they land. No turn.delta streaming builder yet (arrives with the converse route). No manual (T3) spans — conversation is a single model call, not a registry-listed domain op (ADR-124 §10). No model-catalog.yaml — Mira mirrors Vega's per-role llm.yaml by design (RFD §3), so it shares Vega's catalog exemption until it ships to AgentCore. Read-only truth boundary preserved: Mira writes no canonical frame — she proposes, counsel promotes.

6 · Proof

# offline, no key
cd agents/mira && uv run --package mira pytest   # 30 passed
uv run pytest tests                              # 151 passed (governance: catalog exemption, egress, agent-name, custody)
uv run ruff check agents/ tests/                 # clean
uv lock                                          # workspace resolves — mira added

Tests pin: the persona fields + MIRA.avatar_path resolving to the vendored PNG · identity single-sourced with observability · models_for/model_id_for raising on an unset role (no hardcoded default) · ZDR_SETTINGS present · the two input contracts + the scope vocabulary · the fail-closed envelope (missing/unknown intent, missing agent_run_id, malformed input — all raise before run.started) · the boundary watchdog shipping a typed terminal on a wedged brain · the vendored-frame custody gate against framer's committed artifact.