Story-3028 Walkthrough

Outcome: Vani's agent-side schemas, bounded phase logic, scope pinning, and stream guard are ready for send_record_link, record_card, and record_insights. All three remain honest UNAVAILABLE stubs until the broker registry, executors, and delivery half exists. Vani therefore has one live resolver and 16 stubs.

The bounded flow

first invocation
  → resolve_matter_document
  → broker resolves under counsel authz/RLS

one unambiguous resolved document
  → exactly one record-stub phase is available
  → only the three record schemas are exposed
  → direct model prose and reasoning are rejected and withheld from turn.delta
  → the matching stub returns exact UNAVAILABLE

future broker record result
  → tool-free answer pass
  → no third tool leg, no loop
D16 beats the ticket's older framing. send_record_link returns only which document belongs to which matter. It does not mint or return a URL and accepts no channel. The broker separately mints a panel wrapper or fetches bytes for WhatsApp.

One unavailable contract, both doors

{"status": "UNAVAILABLE", "reason": "<why>"}

This exact two-key shape is shared by local stubs and every future broker cannot-answer result. Empty values and malformed unavailable objects are rejected. More importantly, the stream detects broker results before the first model delta and installs the same output validator on the answer pass. False “nothing found” prose or reasoning is therefore withheld; only the corrected unavailable answer can reach counsel.

Prepared broker result views

These strict views are agent-side plumbing, not a claim that the broker can execute the tools today.

ToolWhitelisted result
send_record_link Matter id and document id only. URL, token, storage, bytes, and delivery fields are rejected at the prompt boundary.
record_card The existing document_card.v1 top level, including cited values and the enrichment/built-from stamp. Nested citations are preserved.
record_insights The existing Story-2328 brief fields. Each non-empty broker-verified pin dictionary passes through intact; Vani never recreates a pin the broker dropped as unverifiable. The persisted broker-side pin DTO is not defined in this CodeZip, so the companion Story must freeze that exact DTO rather than making Vani guess its internal fields.

Broker companion contract

This PR deliberately stops at the agents boundary. The broker companion must provide:

Cached-prefix cost

The exact eager resolve-phase definitions are serialized to the OpenAI wire shape and counted with o200k_base. Relative to the resolver-only baseline, the 13 non-record stubs add 1,952 tokens and 8,913 UTF-8 bytes. The three record schemas are exposed only in the separate bounded record phase, so they do not inflate that cached resolve prefix.

Executable proof

RequirementTest
The dormant intent plumbing pins matter and accepts only the resolved document id test_record_intent_pins_matter_and_accepts_only_resolved_document_id
Prepared reference, card, and insight views exclude delivery data test_each_record_tool_exposes_real_data_through_its_whitelisted_view
A resolved request cannot silently settle as direct model prose test_resolved_document_cannot_silently_finish_without_a_record_tool
A broker false negative never reaches turn.delta test_broker_unavailable_finding_never_reaches_the_delta_stream
All 16 stubs remain exact UNAVAILABLE, never empty test_every_stub_returns_unavailable_and_vani_says_it_is_not_built
Resolve and record phases expose disjoint tool catalogs; measured delta is pinned test_converse_registers_the_exact_specced_tools_and_measured_schema_delta

Verification

agents/mira tests → 314 passed
uv run ruff check . → All checks passed
uv run pytest -q    → 444 passed, 3 dependency warnings