Story-3136: Vani terminal-turn telemetry

Outcome: an operator can distinguish a healthy Vani answer, a deferred broker round trip, and a rejected empty turn — without inspecting matter content.

model call closes
  → tool phase classifies answer / deferred / unavailable
  → Vani assembles the final turn
  → terminal or deferred producer span opens
  → turn.completed enters the broker stream
Where the empty-turn signal lives. Bug-3140 (#395) already owns whitespace-only markdown: it logs WARNING mira_converse_empty_terminal_blocked run_id=… reason=empty_answer and then raises VaniEmptyTerminalTurnError, so the run fails instead of emitting a turn. This Story does not add a second warning for the same condition. Consequence for the spans: an empty terminal turn is charged and recorded (agents.mira.tool_phase opens first, deliberately), but never reaches agents.mira.terminal_turn — that span means a turn actually handed over. No log or span here carries prompt, matter text, or the generated answer.

Three operations, not spans per function

Registry nameWhy it existsKind
agents.mira.tool_phase Records the one decision between an answer, deferred intent, and unavailable tool result. INTERNAL
agents.mira.deferred_intent_dispatch Marks the producer boundary where any deferred intent leaves Vani for broker execution, including terminal Vega delegation. PRODUCER
agents.mira.terminal_turn Marks the final user-visible turn actually handed to the broker (a rejected empty turn never reaches it). PRODUCER

Two providers, one call site

manual_span(...) validates one closed-registry name and one allowlisted attribute map, then opens the same span on global ADOT for CloudWatch/X-Ray and on Vani's dedicated Logfire provider when that lane exists. Without LOGFIRE_TOKEN, the mirror is a no-op and the turn continues.

Safe query dimensions

The spans carry run_id, agent.key=mira, gen_ai.agent.name=Vani, model_id, seat, markdown_len, intent_count, is_terminal, tool_called, unavailable_returned, and channel_origin. They never carry firm_id, prompt text, matter identifiers, matter content, or markdown.

Executable proof

RequirementTest
A terminal turn opens exactly tool_phase then terminal_turn, content-free test_terminal_turn_opens_the_two_registry_spans
The two spans are peers under the inbound parent, not nested test_dual_spans_are_peers_under_the_same_inbound_parent
firm_id is stamped on the agent-run root only, never the children test_firm_id_is_stamped_only_on_the_agent_run_root
ADOT and Logfire receive identical span data test_manual_span_dual_emits_the_same_name_kind_and_attributes
Missing or broken Logfire cannot break the turn test_absent_logfire_lane_is_a_noop and test_broken_logfire_lane_cannot_break_the_operation
Deferred broker boundary is emitted test_fetch_ask_emits_a_non_terminal_resolve_intent_turn
Prompt/matter attributes are rejected test_prompt_and_matter_content_attributes_are_rejected