Story-2857 Walkthrough

Change: Mira's converse route gains her ONE server tool, resolve_matter_document (RFD-AGT-013 T3a-b / D18, acceptance CC-19 happy path). When counsel asks to fetch, send, or share a document of the matter, Mira does not guess and does not mint a link — her turn ends as a non-terminal turn.completed carrying a server-plane tool intent. The broker (pramaan-functions) executes that READ under the caller's authz/RLS and the Story-2855 registry leash, then re-invokes the run with tool_results; Mira then presents the candidates — naming the matter, all of them when ambiguous, honestly on no_match — and the wrapper-link mint (T3b-1) consumes the candidate counsel picks. The brain executes nothing and never produces a URL.

Boundary. This is the AGENTS half only. The server tool (registry entry + handler + RLS + audit) merged as Story-2855 in pramaan-functions. The broker's tool round-trip loop (_is_tool_step / _command_with_tool_results) is today gated on kind="dock_chat"; extending that gate to the converse kind is broker-side (functions lane) work outside this ticket — Mira emits the exact dock_chat round-trip wire shape so that extension is a gate change, not a new protocol. Link minting, watermarking, and open-audit are T3b-1/2/3.

The round trip

counsel: "Send me the rent agreement"
  → Mira (converse, tool-bearing pass): the model calls resolve_matter_document(q="rent agreement")
  → the run ends as DeferredToolRequests — the brain EXECUTES NOTHING (ADR-110 §6)
  → turn.completed { turn: { markdown: "", suggested_command: null,
                             tool_intents: [{ name: "resolve_matter_document", plane: "server",
                                              params: { q: "rent agreement", matter_id: <envelope> } }],
                             is_terminal: false } }        ← the dock_chat round-trip wire shape
  → BROKER executes the READ under RLS + the Story-2855 leash, re-invokes with tool_results
  → Mira (answer pass, TOOL-FREE agent): presents the result from the fenced
    <<DOCUMENT_RESOLUTION_nonce>> block — terminal markdown turn, no link, ever.

What each file does

FileChange
agents/mira/models.pyMiraToolIntent (name / plane="server" / params — the broker's AgentToolIntent shape) + MiraToolResult (executor outcome + payload, extra="allow"); MiraConverseInput.tool_results (additive, None default); MiraConverseTurn gains tool_intents + is_terminal (terminal turns keep is_terminal=true, empty list — legacy readers unchanged). Review fix: the prompt renders ONLY MiraToolResult.prompt_view() — the whitelisted MatterDocumentResolutionView/CandidateView (enumerated presentation fields, extra="ignore") — so a URL/storage field the executor ever grows is dropped at the brain boundary; an un-whitelisted tool renders no payload at all.
agents/mira/runtime.pyReview fix: matter scope pinned from the TOP-LEVEL agent_invoke envelope (meta.matter_id, legacy top-level fallback) at the ONE ingress seam, BEFORE the nested input is parsed — a mismatched nested input.matter_id can never drive the emitted intent.
agents/mira/converse.pyThe deferred tool (ExternalToolset + output_type=[str, DeferredToolRequests]); resolve_tool_intents() pins matter_id from the envelope and drops every model-supplied matter reference / junk param; the answer pass (tool_results present) builds a TOOL-FREE agent — one round trip per turn is code, not prompt; tool_results render nonce-fenced in the volatile suffix (the cached stable prefix is byte-identical with and without them); the DOCUMENT FETCH prompt contract (when to call / when not / never a URL / present-don't-pick); UnresolvableToolRequestError fails a foreign-tool run closed (error-message contract: docstring + remediation). Prompt v3, schema mira_converse.v2 (additive turn keys).
agents/mira/tests/test_doc_resolution.pyThe BLOCKING deterministic gate: non-terminal intent turn shape; matter pinning + junk-param drop; foreign-call fail-closed; tool-free answer pass; S-3 cache prefix unchanged by tool_results; a poisoned filename lands INSIDE the nonce fence.
agents/mira/evals/doc_resolution.py + datasets/doc_resolution/*.yamlThe LIVE golden set (6 cases): ask phase (named fetch → intent with counsel's own q; latest-documents fetch; a case question stays a plain answer) + present phase (resolved → confirm by real filename naming the matter; ambiguous → ALL candidates, never an auto-pick; no_match → honest, no invented filename). Every answer case forbids any URL shape. Registered as run.py doc_resolution.
AGENTS.md + docs/notes/agent-registry.mdDoctrine freshness (same-PR rule): Mira's surface row now records the resolve-tool round trip. identity.py carries no surface fields and Mira has no mira.yaml manifest (her config is llm.yaml, models only) — nothing else to update.

Safety seams (all in code, not prompt goodwill)

SeamWhere
Matter identity pinned from the TOP-LEVEL envelope (meta.matter_id) at the ingress seam — a mismatched nested input never wins; model-supplied matter_id/matter_code/junk droppedruntime._envelope_matter_id + resolve_tool_intents() + tests
Tool payloads reach the prompt ONLY through the whitelisted view — URL/storage fields dropped at the brain boundary, unknown tools render no payloadMiraToolResult.prompt_view() + test
One round trip per turn — the answer pass is tool-free_converse_agent(offer_resolve_tool=False) + test
Tool results are fenced DATA (nonce wall); poisoned filename cannot instructbuild_converse_prompt + test
No URL ever leaves the brain — the broker authors the fetch artifact (D18/CC-38)prompt contract + LIVE eval forbidden-token gate
Cache prefix stability (S-3) survives tool_resultssuffix-only rendering + test
Foreign deferred calls fail closed as a typed run.failedUnresolvableToolRequestError + test

Verify (all green)

agents/mira pytest ................ 230 passed (incl. 6 new test_doc_resolution gates)
repo governance pytest tests/ ..... 254 passed (error-contract gate covers the new class)
ruff check agents/mira ............ clean
LIVE eval (real converse seat, kimi-coding:k3 chain):
  uv run python agents/mira/evals/run.py doc_resolution → 6 ok / 0 invariant failures
  (ask → intent with q="rent agreement" + pinned matter; ambiguous → both filenames, no pick;
   no_match → honest; zero URL shapes in any answer)

Deploy (RFD-AGT-013 G5a — named dispatcher)

pramaan-agents has NO deploy-on-merge. After this PR merges, Mira ships via the named dispatcher Mira deploy (Terraform) — .github/workflows/mira-deploy.yml in pramaan-infra, dispatched with the FULL 40-char merge commit sha. Verify the deployed prompt version (mira.converse.v3) via a live invoke, not the workflow color.