Skip to content

Approved execution stream contract

Released in 0.23.33. This contract is additive within approval-v2 (protocol_version: "v2"); it does not introduce another stream version. Consumers must require mithai 0.23.33 or later; 0.23.32 and earlier do not emit tool_execution_started.

A human-approved call emits tool_execution_started once after approval_resolved and before post-approval admission, mutation checkpointing, or transport work. Other calls and heartbeats may interleave. The event means local execution has started, including admission; it proves neither remote dispatch nor remote commitment. Immediate cancellation or expiry can still produce a terminal not_executed decision.

{"protocol_version":"v2","sequence":5,"run_id":"run-1","channel_id":"C1","type":"tool_execution_started","tool_name":"mcp__last9__create_dashboard","tool_call_id":"write-1","approval_id":"approval-1"}

The envelope uses the existing run sequence and exact tool-call/approval IDs. Deduplicate replay by (run_id, sequence) and correlate a call by (run_id, tool_call_id, approval_id). A new approval attempt has its own identity. The start event carries no arguments, credentials, or claimed resource outcome. Automatic approval, denied/expired approval, and human__ask do not produce it. Adapters may implement the optional on_tool_execution_started(tool_name) hook; existing on_tool_start and on_tool_end signatures are unchanged.

tool_started remains the pre-approval announcement. tool_completed.approved records approval, not success. tool_execution remains the terminal execution decision; execution_status: "executed" alone is not a successful mutation. Read the result/error facts and the durable mutation_outcome/v1 projection. No start event authorizes an automatic retry or substitutes for a receipt.

Post-approval ordered admission polls cancellation and the effective call ceiling, including while a preceding tool waits for human input. Checkpoint, settlement, outcome-read and final-persistence waits are bounded. These changes do not extend any tool budget: hosts must still explicitly select tools using ApprovedMutationBudget. In particular, create_kpi and create_alert_from_chart receive no extra allowance merely because their calls are approved. Default MCP transport caps remain unchanged.

Evidence Terminal execution Durable mutation projection
Cancelled/expired before dispatch not_executed not_executed once any admitted intent settles
Remote response confirmed and settlement acknowledged executed, successful result facts completed
Dispatched timeout or ambiguous transport failure executed, error facts outcome_unknown
Response received but settlement wait expires executed, error facts Unresolved until the storage write finishes

Python cannot interrupt a synchronous backend write. Storage workers are bounded by a process-wide pool of 16 slots and retain their slot through late cleanup. They never dispatch the mutation. An abandoned intent settles as unsent when its backend returns; an already dispatched call’s settlement retains its actual result. A backend that never returns cannot promise an updated durable receipt: existing intent remains conservative and blocks replay. Pool saturation fails closed within the caller’s guard instead of adding unbounded workers.

A terminal stream may therefore precede durable cleanup. No late worker emits after run_completed or run_failed. Query durable mutation state/outcomes again after storage recovers. If logical-turn admission itself was still blocked, use the original delivery identity to find its later receipt. Unknown outcomes require authoritative reconciliation; confirmed non-execution still requires a fresh approval before another attempt.

An idle HTTP response continues delivering sequenced heartbeats (15 seconds on the initial stream) while execution work is blocked. Subscriber loss releases the subscription without cancelling execution. Both the initial response and GET /api/runs/{run_id}/events?after=<sequence> release their subscriber when ASGI exits, even if their synchronous iterator is suspended. Reattachment replays the same identities and never restarts execution. Cleanup is idempotent, so an old response cannot detach a newer subscriber.

Run-event replay is retained in memory under the existing retention rules. Durable mutation replay is separate; see Durable mutation outcomes.

tests/fixtures/approved_execution_v2.json exports success, pre-dispatch expiry, and dispatched-timeout examples generated through the real ASGI trigger, approval-decision and replay routes. tests/approval_execution_fixtures.py normalizes run/approval/mutation identities, removes heartbeat sequence offsets, and selects lifecycle fields. The examples are projections, not complete SSE transcripts; the sequence gaps retain omitted events. They also include durable outcome projections for sibling consumers in last9/ai and last9/last9.

tests/test_approved_execution_http.py validates these fixtures and covers create/update dashboard plus synthetic KPI/alert writes, cancellation, expiry, transport timeout, HTTP heartbeat delivery, two consecutive subscriber losses, replay identity, no ambiguous retry, and terminal fencing. tests/test_approved_execution_lifecycle.py covers both engine entry points, ordered admission stalls and blocked storage (including a backend holding the session lock). The hosted read-only preparation/approval probe also checks the start event before dispatch; no mutating hosted test is added.