Story-3101 Walkthrough

Outcome: Vani's converse agent now exposes the 16 approved tool descriptions to the model. Each stub returns one explicit, non-empty result:

{"status": "UNAVAILABLE", "reason": "This capability is not built."}

The prompt requires Vani to tell counsel that the capability is not built. An SDK-native output validator rejects a finding-shaped reply that omits “not built” and asks the model to correct it. Because validation happens after generation, only generations following an UNAVAILABLE tool result withhold answer-text deltas; the validated correction is then emitted. Ordinary turns retain immediate streaming, and provider/thinking streaming continues on the narrow unavailable path.

Boundary. resolve_matter_document remains the one live deferred broker read. current_matter is not registered: it remains the broker's deterministic /current variable read under pramaan-docs ADR-166. No tool accepts a channel parameter. No tool returns a rendering or mints a link.

The model-visible flow

counsel asks for a planned capability
  → Vani selects the matching registered tool
  → local stub returns UNAVAILABLE (never [])
  → Vani says: I cannot do that because it is not built yet

counsel asks to fetch a matter document
  → resolve_matter_document remains deferred
  → broker reads under counsel authz/RLS
  → existing candidate presentation path continues unchanged

Exact registered stubs

request_frame_update · search_evidence · find_exact · find_contradictions · next_hearing · send_record_link · record_card · record_insights · citation_audit · verify_citation · find_authority · court_weight · proposition_match · citation_history · case_status · cause_list.

Every description is read directly from its existing ToolSpec.purpose. The stubs take no arguments because the specs declare no implementation parameters and this Story must not invent them.

SDK decisions

QuestionDecision
ToolReturn or a bare value? Bare typed dictionary. ToolReturn separates a return value from extra model content or hidden metadata; these stubs have exactly one canonical model-visible status and no extra content.
Deferred tool loading? Not adopted. On Vani's current routes the SDK's local search fallback changes tool definitions after discovery and invalidates the cached prefix. This Story explicitly requires the model to see all approved descriptions, so the 16 schemas remain eager.

Cached-prefix cost

The test serializes the exact Pydantic AI definitions into the OpenAI-wire tools shape and counts with o200k_base. Compared with the existing resolve-only baseline, the 16 schemas add 2,497 tokens and 11,418 UTF-8 bytes. This is material and intentionally visible.

Executable proof

RequirementTest
Exact 16 names, exact purposes, zero invented parameters test_converse_registers_the_exact_specced_tools_and_measured_schema_delta
Every call returns non-empty UNAVAILABLE; a fabricated negative is rejected; Vani says “not built” test_every_stub_returns_unavailable_and_vani_says_it_is_not_built
The rejected false finding never reaches any turn.delta test_rejected_unavailable_finding_never_reaches_the_delta_stream
Lab and manifest distinguish live, unavailable, and excluded test_identity_and_manifest_distinguish_live_unavailable_and_excluded
Existing resolver unchanged agents/mira/tests/test_doc_resolution.py

Verification

Focused Story + resolver + converse tests → 34 passed
Vani package suite                         → 268 passed
uv run ruff check .             → All checks passed
uv run pytest -q                → 444 passed, 3 dependency warnings