Story-2074 Walkthrough

Change: CE-15 of the Context Engine (Epic-2049, RFD pramaan-functions/docs/notes/rfd-context-engine.md §14.3 row CE-15; consumer design in docs/notes/rfd-mira-context-panel.md §3.1). CE-13 (bundle builder), CE-14 (broker pre-fetch) and ME-7 (memory stamping) put a ContextBundle and a memory_pack on agent_invoke.v1 input. CE-15 is the brain side: Mira now consumes that pack across all four panel seats and converse — composing the prompt sandwich in the right order, surfacing context_provenance on turn.completed, and failing closed on a fact with no citable span.

The sandwich order (both routes): frame → memory → bundle

Every pack-consuming prompt lays the stable, engine-produced blocks first, most-stable to least, then the volatile per-turn suffix (RFD-agent-caching §5.1):

system + role charter            (instructions, static per process)
  1. matter meta line            (changes ~never)              ┐ stable prefix
  2. current_frame @ frame_version   <<MATTER_FRAME_v5>>      │  bundle-stable
  3. session_memory @ memory_version <<SESSION_MEMORY_v7>>    │  delimiters
  4. context bundle  @ version_hash  <<CONTEXT_BUNDLE_feed…>> ┘  (NO per-run nonce)
  ─────────────────────────────────────────────────────────────
  5. counsel message / command       <<COUNSEL_MESSAGE_ab12cd>> ┐ per-turn suffix
  6. drafts / critique / reply instruction                       ┘  (per-run NONCE)

Nonces in suffix (the acceptance). The engine-produced blocks (frame, memory, bundle) carry bundle-stable delimiters — a version tag (v5, v7) or a content hash of their own bytes — so they are byte-identical across a session's turns / a run's seats (the cache prefix hits) and unforgeable to the adversarial content inside them. The per-run nonce stays where it earns its keep: around the genuinely untrusted, volatile per-turn input (the counsel message, the command). This is the same trade Atlas's cached prefix already made (§5.2 security note); the injection wall still measures green (13 cases held).

Citations — cite the record, or it is a fabrication

The bundle is the record: ranked evidence, chronology, assertions and contradictions, each carrying a citation label ("Exhibit P-3, p.4 ¶2") that the L4 producer already selected and authority-ranked (max 5 per statement, CONTEXT_MAX_CITATIONS_PER_STATEMENT). Converse's role prompt now instructs Mira to cite that label inline when she states a supported fact — the exit-demo: ask Mira → the answer cites the record. The panel's critic gains a cite-or-fabrication pass: a high-risk token (date, amount, party, citation) with no citable pack span is a fabrication finding, high severity. If the synthesizer keeps it, verify_critique_closure raises FrameBuildFailed — no-span fact → fails closed, the second acceptance criterion.

Degraded context

The bundle carries an engine_state (stubbed / partial / live) rendered into the record block. When the pack is stubbed/partial — or absent — the record is incomplete, never wrong: the critic audits against the frame alone, holds the bar to frame-only closure, and never invents a span to fill the gap. An unsupported fact stays a finding; the honest answer is a blank field and a downgraded frame_status, not a fabricated cite (§7).

context_provenance on turn.completed

Every pack-consuming turn surfaces the bundle's own honesty scalars so the broker can persist them beside the landed proposal and the app can badge a frame built blind (§7):

context_provenance = {
    "bundle_version_hash": "feedfacecafebabe1234",
    "engine_state": "live", "profile": "chat", "budget": "S",
    "evidence_count": 1, "sections_truncated": false,
}

Derived straight from the bundle — never re-computed brain-side. No bundle (a frame-only turn, OQ-4) → an honest stubbed marker with a null hash, so the key is always present for the broker.

What ships

FileRole
agents/mira/models.pyContract mirror: ContextBundle gains the honesty scalars (engine_state/profile/budget/evidence_count/sections_truncated); new MemoryPack (memory_version+text); context_provenance(bundle) helper. memory_pack added to MiraConverseInput; bundle+memory_pack added to MiraCommandInput.
agents/mira/converse.pySession-memory block folded into the stable prefix (between frame and breakpoint A); a CITE-THE-RECORD role contract; context_provenance emitted on the converse turn.completed.
agents/mira/panel_prompts.py, agents/mira/panel_models.py_context_blocks is now stable-first: frame → memory → bundle with bundle-stable delimiters (_stable_tag), then the nonce-walled command/locks/notes/docs. PanelInput gains frame_version/bundle/memory_pack. The critic prompt gains the cite-or-fabrication pass + the degraded-context rule; the writer overlay grounds facts in the record.
agents/mira/command.py, agents/mira/runtime.py, agents/mira/events.pyrun_command threads the pack into PanelInput; build_command_turn + the runtime lift context_provenance onto the command turn.completed; turn_completed_event takes the additive context_provenance payload key.
agents/mira/tests/test_context_pack.py (new)The CE-15 acceptance suite (below).

Brain-side contract, wire-shape stubs (ADR-110 §6)

The brain stays stateless. ContextBundle/MemoryPack are the envelope wrappers the broker populates (CE-14/ME-7); the bundle text ships verbatim into the prompt (never re-serialized — the version_hash is defined over the upstream canonical form). Defaults describe an honest stubbed pre-fetch, so a bundle carrying only text+version_hash still validates and Mira consumes an empty pack without fabricating to fill it (§8).

Acceptance criteria → tests

ACProof (tests/test_context_pack.py)
fabrication fails closed (no-span fact → FrameBuildFailed)test_no_span_fact_fails_closed — the critic flags an unsupported fact as a high fabrication; the synth keeps it → run_command raises FrameBuildFailed(reason=unresolved_critique).
nonce-in-suffix assertedtest_converse_nonce_stays_off_the_stable_blocks and test_panel_nonce_stays_off_the_stable_blocks — two nonces, the frame/memory/bundle region is byte-identical; only the nonce-walled counsel-message/command block moves.
pack consumed across all four seats + conversetest_all_four_panel_seats_receive_one_bundle_and_the_memory (p1/p2/p3/critic/synth all see ONE version_hash) + test_converse_sandwich_order_is_frame_then_memory_then_bundle.
context_provenance on turn.completedtest_converse_turn_completed_carries_context_provenance, test_command_turn_carries_context_provenance, test_context_provenance_from_bundle_and_from_none.

Cache alignment (per-seat policy, unchanged)

The bundle-stable version_hash delimiter makes the whole prefix a stable, cacheable region: converse is designed, the panel is within_run (both live from Story-2080/2083). This story adds no cache writes to ambient seats (Vega/Blake stay off on paid-write providers, per Story-2084); it only feeds a larger, citable prefix into the seats already caching.

Verification

uv run --package mira pytest agents/mira/tests -q                    # 148 passed
uv run --package mira python agents/mira/evals/run.py adversarial    # 13 held / 0 failures
uv run pytest tests -q                                              # 175 passed (governance)
uv run ruff check tests/ scripts/ agents/                          # All checks passed!

Wiring + consumption only, no new egress: the brain consumes a pre-fetched pack from the invoke envelope (ADR-110) — no DB creds, no mid-run tool round-trip. The pack goes live when CE-14's broker pre-fetch delivers real bundles; until then Mira consumes the honest stubbed pack unchanged. llm.yaml is untouched by this story.