Wakterm Agent API v1 golden contract#

These fixtures are the cross-repository semantic contract for the live Wakterm Agent API. Panetone can use them for fake-adapter and compatibility tests.

golden-fixtures.json retains two capability snapshots:

  • current_capabilities is an exact live Wakterm capability response.
  • event_stream_capabilities is a compatibility alias for consumers that previously selected the event fixture profile.

Wakterm advertises event_stream.v1 only while its durable event store is available. Consumers must still negotiate the live capability before reading.

The implemented v1 boundary is capability negotiation, the agent catalog, authoritative prompt admission, and the existing return-request terminal stream. Admission is scoped to a stable Wakterm agent ID and opaque current incarnation. Native observed sessions use process identity, while managed Codex sessions use exact app-server provider identity. A definitive non-acceptance means no prompt bytes were written.

Every live catalog entry has a unique agent ID. Wakterm assigns that ID when an agent is registered and persists it across restoration. Admission resolves the exact catalog agent and incarnation pair rather than selecting by agent ID alone. An indeterminate result is never safe to retry under a new request ID.

Each catalog entry also contains a fixed-width pane_id. It is the smallest authoritative locator for joining a Panetone live route to the current Wakterm catalog when route titles and agent names differ. Pane IDs are ephemeral mux coordinates. Consumers must resolve them from a fresh catalog and must never persist them as agent identity, process identity, or an idempotency key.

wakterm agent caller identifies the live registered agent that the command runs for and prints {"schema", "resolved_by", "agent"}, where agent is that agent's catalog entry. A pane's shell and the tools of a PTY harness inherit WAKTERM_PANE, which resolves to the agent in that pane (resolved_by: "wakterm_pane"). Tool commands of a managed Codex thread run in the shared app-server and carry no pane variable; their CODEX_THREAD_ID resolves to the one live managed agent bound to that provider thread (resolved_by: "codex_thread"). The command exits non-zero without output when neither variable is present, when the named pane has no live registered agent, when the thread matches no live managed agent or more than one, or when both variables are present and name different agents. --pane N and --codex-thread ID pass the same inputs explicitly; when either flag is given, the environment is ignored. Consumers should use the result to fill a source agent and pane for the current request only, under the same rules as any catalog pane_id.

The durable event page provides:

  • durable increasing sequence order
  • agent, current-incarnation, and exact turn identity
  • distinct assistant, plan, turn, observer, and agent-lifecycle events
  • catalog ordering through as_of_event_sequence
  • explicit bounded-retention metadata and cursor_too_old recovery
  • classified incompatible-version and unknown-event failures

Interactive requests#

Wakterm publishes a structured approval_requested event for managed Codex command approvals and blocking single-choice questions, and for observer-backed Claude AskUserQuestion calls with one single-choice question. Its approval object carries the request kind, exact agent and incarnation, an opaque request ID, the provider turn and item IDs, the prompt or command context, and ordered choices. Consumers must present only the advertised choices and must treat the request ID as opaque.

Resolve a choice through the supported Agent API rather than writing terminal keys:

wakterm agent approval \
  --request-id REQUEST_ID \
  --agent-id AGENT_ID \
  --incarnation INCARNATION_ID \
  --choice CHOICE_ID

Wakterm answers Codex through the original app-server request, causing the native TUI to dismiss the same modal. Claude has no equivalent control API, so Wakterm validates that the exact tool call is still pending in the exact live session and submits the selected label to that pane's native question UI. Resolution requires the exact live agent incarnation and a choice from that request. A repeated response, a provider-side response, a replaced agent, or a completed request is rejected as stale. Pending interactions are provider state, not a second durable authority; the durable event exists so a transport can notify the user and recover exact identity.

approval_control.v1 covers these single-choice requests. Multiple questions, multiselect questions, free-form questions, Codex file changes, and general permission methods remain owned by the native TUI until they receive an equally exact mapping.

The public contract does not expose the event database, provider paths, parser cursors, or transport implementation. Wakterm's experimental Codex output page remains available for side-effect-free shadow comparison, but it is not the durable event contract.

The catalog as_of_event_sequence is a conservative lower-bound cursor sampled before the live catalog snapshot. Starting after that sequence can replay a lifecycle state already visible in the snapshot, but cannot skip a concurrent lifecycle change. Consumers must apply events idempotently by event ID and agent incarnation.

The live stream starts each newly observed provider session at its tail. It does not replay provider history from before the first durable lifecycle baseline. Provider session replacement, truncation, or rewrite emits an observer_failure and arms a new tail baseline instead of guessing across the gap.

Codex, Claude, Gemini, and OpenCode projections are live. Turn IDs come from provider records: Codex turn IDs, Claude human-user UUIDs, Gemini user-message IDs, and OpenCode assistant parentID values. Finals require provider completion evidence: Codex task_complete, turn_aborted, or app-server turn completion, Claude end_turn, a persisted Gemini response message, or OpenCode finish: stop. OpenCode tool-calls is intermediate and never a final. Plans are emitted separately when the provider records an explicit plan artifact, currently Claude ExitPlanMode.

For observer-backed sessions, a durable provider turn transition updates the catalog and admission snapshot in the same observation. A committed waiting_on_user transition therefore makes the exact agent idle without waiting for terminal input or unrelated API activity.

Only output from the pane's user-visible primary provider session enters the normalized stream. Internal Codex worker sessions, including subagents, approval reviewers, model graders, summaries, and background feature sessions, cannot replace an observer-backed pane's primary session or emit assistant, final, or lifecycle events for it.

For Codex app-server TUI sessions, completed agent-message items and turns are committed from the live app-server notification stream. This includes sessions restored after a mux restart and does not require catalog or prompt activity. The authoritative status returned by app-server resume initializes the restored catalog entry, so an idle session can accept prompt admission immediately.

Terminal failure detail#

For managed Codex turns ending with provider status failed, turn_final keeps outcome: "aborted" and supplies a normalized category in the existing reason field plus a safe user-facing description in detail. These fields are present even when the turn has no assistant output. text remains assistant output and may be null. The policy_aborted_turn_final golden fixture demonstrates this case.

ReasonMeaning
policy_blockedThe provider blocked the response under its content policy.
context_limitThe conversation exceeded the context limit.
usage_limitA session budget or usage limit was reached.
rate_limitedThe provider rate limit was reached.
model_at_capacityThe selected model is at capacity; the user can try a different model.
provider_unavailableThe provider service or connection failed.
authentication_failedThe provider rejected authentication.
invalid_requestThe provider rejected the request.
sandbox_errorThe execution sandbox failed.
provider_errorThe failure has an unknown or absent provider error code.

Descriptions come from Wakterm's fixed mapping of structured provider error codes. Raw provider messages, additional diagnostics, and policy continuation instructions are excluded. Clients should accept unknown reason values and render detail as plain text. recoverable stays null; the category does not authorize retries or changes to provider policy settings.

Codex serverOverloaded maps to model_at_capacity with the fixed detail “Selected model is at capacity. Please try a different model.” HTTP and stream connection failures retain provider_unavailable, including HTTP 503 errors without the explicit capacity code. The capacity_aborted_turn_final fixture covers the capacity notice. Consumers that display detail need no compatibility change.

Consumers may present an aborted terminal event's nonempty detail as a failure notice, including when text is null, and deduplicate it by event_id. It is not an assistant message. Successful turns and ordinary interrupted turns retain their existing behavior. A transient error notification does not itself produce a terminal event; failure detail comes from the authoritative turn-completion payload.

This uses existing Agent API v1 fields and requires no new event kind or capability. Events already committed without failure detail remain unchanged; the new normalization applies to newly recorded completions. Return-request receipts are a separate stream and retain their existing generic aborted detail.

Session continuity and event delivery#

Ordinary event mirroring follows the live pane when its managed Codex TUI starts, resumes, or forks to another provider thread. The stable Wakterm agent ID and pane route continue to receive assistant output, while the exact provider thread and derived incarnation change in the catalog and event provenance. Consumers do not need to replace an event cursor or rediscover the route. Exact prompt admission and return-final correlation still require the current catalog incarnation and provider turn.

Return-final admission uses the same request and receipt contract for observer-backed Codex PTYs and managed Codex app-server sessions. An observer-backed request is correlated through its exact process, provider session, cursor, prompt hash, and provider turn. A managed request arms the durable event sequence for its exact app-server thread and session, binds the first subsequent provider turn, and accepts only that turn's durable final.

For an already-bound observed Codex request, a newer live turn does not invalidate a terminal record retained in the same provider session. Reconciliation checks that bound turn's terminal record after the armed cursor and returns its completion or abortion, preserving the original completion timestamp. Missing terminal evidence or changed process or session identity remains indeterminate.

Codex and Claude JSONL records larger than 4 MiB produce an observer_failure describing the skipped byte range. Observation resumes at the next complete record, with cached turn identity and assistant text cleared to prevent attribution across the gap. An incomplete oversized record waits for its terminating newline. Later records with sufficient turn identity continue to produce events; content inside the skipped record is unavailable through the event stream.

Gemini observation accepts both legacy JSON conversation snapshots and the current append-only JSONL format. Duplicate JSONL records update the same durable provider message, incomplete trailing records wait for the next refresh, and a legacy session that migrates to its .jsonl sibling keeps the same provider session and cursor. Claude can persist separate thinking and text records with end_turn on both; only the user-visible text record produces the turn final. Gemini messages with toolCalls are intermediate assistant output, not finals. OpenCode finals use the provider completion timestamp and never the earlier assistant-message creation time. Explicit Claude and Gemini provider errors emit observer_failure; they do not synthesize a terminal outcome.

The default retention bound is 100,000 events. A reader whose sequence precedes retained history receives cursor_too_old and must take a fresh catalog snapshot. Wakterm records previously available incarnations as unavailable with reason mux_restarted when a new mux runtime opens the store, then emits a new available lifecycle event only if that exact incarnation is observed again.

Provider parsing and SQLite commits run on the observer worker, not the mux reactor. Wakterm watches provider artifact roots for both detected and adopted panes. Artifact changes schedule the existing throttled observer path, and a confirmed adopted session receives a trailing refresh when a hint lands inside the throttle window. Event consumers only read the durable stream. They do not need to poll provider files or call the catalog to make the producer advance.

The CLI page operation is:

wakterm agent events --after 123 --limit 100

Long-lived consumers can reuse one mux connection and receive JSON-lines pages:

wakterm agent events --after 123 --limit 100 --follow

Follow mode emits one line per page and drains retained pages without delay. At the stream head it holds one bounded ReadAgentEvents request until a durable commit or --wait-ms timeout, then repeats from the returned sequence. It exits after cursor_too_old so the consumer can perform the documented catalog-snapshot recovery.

ReadAgentEvents.wait_ms is additive, defaults to zero when absent, and is clamped to 30 seconds by the server. Existing events and retention gaps return immediately.

Unknown additive fields must be tolerated. An incompatible major schema or an unknown event kind must fail explicitly. Provider paths and parser cursors are not public identities.

Wakterm tests validate the current DTO examples, receipt invariants, sequence ordering, lifecycle relationship, retention gap, and required error classes. Panetone should consume this same file rather than copying the examples.