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.
_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.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.
| File | Change |
|---|---|
agents/mira/models.py | MiraToolIntent (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.py | Review 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.py | The 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.py | The 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/*.yaml | The 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.md | Doctrine 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. |
| Seam | Where |
|---|---|
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 dropped | runtime._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 payload | MiraToolResult.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 instruct | build_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_results | suffix-only rendering + test |
Foreign deferred calls fail closed as a typed run.failed | UnresolvableToolRequestError + test |
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)
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.