Kin / Reference
Event vocabulary
The typed seam shared by Kin's Textual UI, local stdio clients, and headless reporting. Every event type and field is part of the client contract.
Read as MarkdownThe seam between the harness and every client. The Textual UI consumes it in process; kin --stdio forwards the same typed events as JSON lines; a headless kin -p run collects them into its exit code and report instead of painting them. Every event type and field is part of the contract.
src/kin/harness/events.py is the single source of truth for the type strings and field names; renaming a field breaks src/kin/tui/app.py’s on_kin_event dispatch. Treat that file as load-bearing.
Shape
Each event is an Event dataclass with seq, type, and a data dict (plus .get(key, default)). Builder functions in events.py stamp a monotonic seq and return a ready-to-emit event.
@dataclass
class Event:
seq: int
type: str
data: dict[str, Any] = field(default_factory=dict)
def get(self, key: str, default: Any = None) -> Any:
return self.data.get(key, default)
Handshake
session and ready open every session (fresh or resumed); the state-echo events below them (mode_changed, model_changed, nudges_changed, etc.) also fire once at session start to seed the UI with the session’s current state. See Sessions, resume & compaction.
session
The session opener. Emitted once, before ready.
| Field | Type | Meaning |
|---|---|---|
session_id |
str |
The session id (also shown on the StatusBar) |
model |
str |
The model id the harness is calling |
workspace |
str |
The session workdir |
posture |
str |
The concise auto-mode trust line for the welcome banner (for example, auto · trusted workspace — shell runs as you); empty in strict mode. Workspace trust is a launch precondition, not a new event kind. |
ready
Emitted after session. The UI treats this as “cold-start done.”
mode_changed
Emitted when the active permission mode changes (and once at session start).
| Field | Type | Meaning |
|---|---|---|
mode |
str |
The new mode (auto or strict) |
error |
str |
Non-empty on a refused change (e.g. an unknown mode name) |
model_changed
Emitted on /model <id> and once at session start. See Models & providers.
| Field | Type | Meaning |
|---|---|---|
model |
str |
The new model id (the configured alias — what the harness sends on the wire; may be "default" for a vLLM-style served-model-name alias) |
error |
str |
Non-empty on a refused change |
model_discovered
The active model id — what the server actually used — surfaced by a
/v1/models probe (session start) or by response.model piggybacked off
each completed turn. Distinct from model_changed: the latter echoes the
configured alias on /model swaps, the former surfaces the resolved name.
Drives the session line’s live model slot below the composer and the welcome
banner’s model row when it’s still mounted. Both events can co-fire for one /model swap
(alias toast + resolved toast).
| Field | Type | Meaning |
|---|---|---|
active |
str |
The resolved model id (canonical first served-model-name entry on a vLLM serve, or the actual model on real providers). Sanitized at capture — control chars, Unicode bidi overrides, and zero-width glyphs are stripped (see sanitize_model_id); an empty result is suppressed so the known model label is retained. |
Display-sink hardening
The
activevalue is server-supplied (untrusted input). The harness sanitizes it at every capture site (_fetch_active_model,_OpenAITurn._run,_AnthropicTurn._run,force_tool_call,count_tokens) AND passesmarkup=Falseon the toast so a compromised / malicious server can’t phish the visible label via markup-shaped strings. The session line and banner render through literalrich.text.Textvalues without markup interpretation. SeeREFERENCE.md § Display sinksfor the pattern.
nudges_changed
Emitted after every /nudges Apply and at every session start, including when
the set is empty. It carries the complete ordered set so a client can replace
its display atomically and clear stale chips after a session switch. See
Workflow migration. No preferences are currently
registered, so current sessions emit an empty set.
| Field | Type | Meaning |
|---|---|---|
active |
list[str] |
Complete active registered-key list in stable registry order |
planning_changed
Emitted when the session’s read-only planning freeze toggles. Drives the
⏸ planning status-bar indicator (shown beside the mode badge when a
non-default mode is active). Mirrors
nudges_changed as a human-visible state echo. Both directions are the
person’s: they enter with /plan, a local client’s planning control, or by
approving a plan_mode_request, and only they exit (the
plan-approval modal, /plan, or that control). Writing a plan never enters the
freeze. Never persisted — a transient overlay, not a mode.
| Field | Type | Meaning |
|---|---|---|
on |
bool |
Whether the read-only planning freeze is now on |
effort_changed
A current-display state echo, not proof that a person changed a setting. Emitted at every session start (including empty), after model/provider replacement, on effort changes, and when capability reconciliation changes the effective value. Subscription ChatGPT echoes its backend-effective request policy, including exact Astra’s unpinned medium; other providers retain explicit-only display. The TUI updates its full-word effort token quietly, while explicit choices retain their audit note. A later service attacher receives a bounded handshake projection of the current model and effort without restarting the session.
| Field | Type | Meaning |
|---|---|---|
level |
str |
Current effective display value (low / medium / high / model-specific), or empty to clear the token; not necessarily a saved choice |
error |
str |
Empty for a normal echo; non-empty on refusal, with level still echoing the unchanged effective value |
settings_mode_changed
Emitted on /settings on|off. Drives the /settings on echo (~800-token
in-conversation guide for the model — propose_settings is otherwise denied
mid-plan, so this is the persistent human-only opt-in). See
Model-writable settings
and settings.toml keys § Editing settings from inside kin.
Mirrors nudges_changed / planning_changed as a third human-only session-wide
state echo. Survives resume + compaction + clear + rewind.
| Field | Type | Meaning |
|---|---|---|
on |
bool |
Whether the session-wide /settings guide is now active |
Assistant output
stream_chunk
A streaming text delta from the model. The TUI renders contiguous prose segments as assistant messages. Interleaved reasoning closes the earlier prose segment, so later prose follows it in arrival order rather than moving above it. The response retains one role label; a provider message id is not a layout parent.
| Field | Type | Meaning |
|---|---|---|
message_id |
str |
The assistant message this chunk belongs to |
content |
str |
The delta (not the full text so far) |
source |
str |
Optional carrier label |
stream_done
Marks the end of streaming for one message_id. The UI finalizes the assistant block.
stream_retry
A headless subagent’s transient mid-stream failure is about to be retried. Its
ForwardingEmit removes the partial tail before the replacement stream begins,
preventing duplicated prose. A root session never re-streams answer text; it
retries only a round that has shown reasoning alone (see reasoning_withdrawn).
| Field | Type | Meaning |
|---|---|---|
discard_chars |
int |
Number of already-accumulated trailing characters to remove |
text
The fully assembled text for a turn segment (assistant / system / user / tool). The legacy “complete text” event the UI prefers for non-streaming paths.
reasoning
A delivered reasoning chunk. The TUI renders neutral reasoning disclosure within
the current contiguous activity group, preserving visible segment order when
prose and reasoning interleave. An unknown empty done-only event creates no row.
Reasoning is not reconstructed from journals. Local stdio and service clients
receive root reasoning only when their hello declares reasoning; a phase
alone does not imply forwarded content. Each event’s content is capped at
4000 characters and content_truncated marks a shortened preview. The service
can replay the current turn’s bounded memory cache on reconnect; see
Local clients.
| Field | Type | Meaning |
|---|---|---|
message_id |
str |
The reasoning block id |
content |
str |
The reasoning chunk (may be empty on deltas) |
done |
bool |
True on the final delta for this block |
reasoning_withdrawn
A root model round failed transiently after showing reasoning but before any
answer text, and Kin is about to retry it. Drop what message_id delivered
since its round began; the retry’s reasoning arrives under the same id as a
fresh round, and earlier rounds a client folded into the same row stay. Nothing
from the withdrawn attempt entered the conversation. The ordinary retry notice
still follows. Local clients receive this only when their hello declares
reasoning_retry beside reasoning.
| Field | Type | Meaning |
|---|---|---|
message_id |
str |
The reasoning block id whose current round is withdrawn |
thinking
The canonical root-turn work phase. Clients read this event instead of
inferring model state from reasoning, text, and tool event order. The existing
status remains compatibility display text; phase is the machine-readable
state. This lifecycle does not describe whether a provider’s model-side
“thinking” option is enabled.
| Field | Type | Meaning |
|---|---|---|
active |
bool |
true for a phase transition; false for the turn’s sole closer |
status |
str |
Compatibility display text. Existing values such as Working, running tools…, and compacting context… remain unchanged. |
phase |
str |
requesting, reasoning, answering, tools, compacting, or plan; empty on the closer |
The ordinary lifecycle is:
| Phase | Emission boundary | Presentation available to a client |
|---|---|---|
requesting |
A model round is waiting for its first delta | The compatibility status is Working |
reasoning |
Once, immediately before the first reasoning delta for a message, and again when a withdrawn round is retried | The phase crosses stdio even though private reasoning content does not |
answering |
Once, immediately before the first visible text delta for a message | The following stream_chunk carries the answer |
tools |
Before the round’s first tool_call |
The call and its progress provide the tool’s own words |
compacting |
Context compaction | Present status verbatim |
plan |
An approved plan begins execution | Present status verbatim |
Every root turn emits exactly one
thinking(active=false, status="", phase="") before its terminal done or
interrupted and, over stdio, before turn_ended. This includes backend and
persistence errors, cancellation, loop detection, context refusal, and turn or
token-budget limits. In-turn compaction changes phase without manufacturing a
second closer. A child’s thinking events do not cross ForwardingEmit and
therefore cannot overwrite the root phase; its correlated subagent and tool
events describe its own work.
Tools
See Tools for what each tool does. TUI activity groups fold contiguous routine root calls and delivered reasoning independently of batch ids; prose, human/steering input, attention, provenance and turn boundaries seal admission. Results still route by exact call id after sealing. Skill and ask rows stay explicit, and child activity retains its task/agent host. None of these presentation choices changes dispatch, event order or journal schema.
tool_call
A model-issued tool invocation.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The tool-call id (used to match the result) |
name |
str |
The tool name |
arguments |
Any |
The arguments (parsed dict for typed callers, JSON string on the legacy OpenAI wire) |
source |
str |
Optional carrier label |
batch_id |
str |
Present only when this call is part of a parallel batch (len(tool_calls) > 1, non-serial); execution metadata, not a TUI disclosure-group id. Absent on single / serial calls. |
batch_size |
int |
Number of calls in the batch (present with batch_id) |
batch_index |
int |
This call’s position in the batch (present with batch_id) |
tool_stream
A live-tail chunk for a tool that’s still running (shell stdout, build output). Shell chunks may contain several complete lines. Treat them as transient
updates; the final tool_result owns retained output. Available complete lines
are delivered without waiting for another read.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The tool-call id this tail belongs to |
name |
str |
The tool name |
message |
str |
The new tail text |
parent_tool_call_id |
str |
For subagent tool calls — the parent’s task tool-call id so the UI can nest the stream |
spawns_agent |
bool |
True when this progress line is the signal that the tool call just became a real subagent spawn (task’s foreground branch — including when the model dispatches critic itself from present_plan’s adversarial-review option) — the UI flips the already-mounted tool-call row into a live-updating agent-mode timer instead of a one-shot tail line. Defaults False. |
tool_call_draft
A throttled progress event while the model is still streaming a tool call’s
arguments. Carries the tool name (once known) and a cumulative raw-JSON char
count so the terminal tab title can read drafting <name>… N chars, and the
live activity title preparing <name>…, while a long
write_file body or shell command is in flight. Never carries argument content
itself (a draft may include api_key values, secrets, or shell command bodies);
the UI is responsible for a name + length only. Dropped by ForwardingEmit
for subagent children (keeps the parent row’s label stable). Headless runs
deliver tool_call_draft to on_event taps and the journal the same as
any other event — they simply don’t paint (no contribution to
HeadlessResult.text).
| Field | Type | Meaning |
|---|---|---|
name |
str |
The tool name once the model has announced a tool_use block; empty until then |
chars |
int |
Cumulative raw-JSON character count streamed so far |
tool_result
A tool’s outcome.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The tool-call id |
name |
str |
The tool name |
result |
Any |
The legacy string form of the result |
error |
str |
Non-empty on a refused / failed call |
parent_tool_call_id |
str |
Subagent nesting (see tool_stream) |
content_blocks |
list | None |
Structured payload (text / image / notebook cell); omitted for plain-string results |
batch_id / batch_size / batch_index |
str / int / int |
Parallel-batch grouping (mirrors tool_call); present only on calls from a len > 1 non-serial batch |
Blocking events
Blocking events park the turn on an in-process Future keyed by a correlation id; the UI’s resolve_* modal workers unblock the turn. The answer encoding lives in REFERENCE.md § "Event contract & IPC". A headless run has no modal to resolve one of these — it resolves the blocking event as a structured needs-human tool error instead of parking forever.
approval_request
A tool call that needs your decision. See Modes & permissions for what Allow / Ask / Deny / Auto mean per mode.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id (used by resolve_approval) |
tool |
str |
The tool name |
arguments |
Any |
The arguments (rendered in the modal) |
scope |
str |
Human-readable scope label for an “always allow this session” choice |
kind |
str |
Permission kind used to choose the modal body |
args |
dict | None |
Parsed structured arguments when available |
diff |
str |
Complete precomputed unified diff within the interactive-review bound; empty when unavailable |
allow_always |
bool |
Whether the reusable session tier is offered |
server_scope |
str |
Optional wider MCP-server tier shown by the modal |
call_signature |
str |
Exact bounded operation identity used to bind the approval to the displayed call |
approval_context |
str |
Opaque reviewed-state identity for a file operation; empty otherwise |
display_signature |
str |
Integrity checksum over every field that affects the local approval choice |
The Textual client recomputes the exact-operation and display signatures before opening the modal. A mismatch resolves as a denial instead of presenting stale or mutated evidence. These unkeyed checksums detect in-process drift; they are not public bearer credentials and do not grant authority by themselves.
question
A single- or multi-select (or free-text) question.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
question |
str |
The prompt text |
header |
str |
Short header |
options |
list[dict] |
The option list |
allow_other |
bool |
Allow a free-text “other” |
multi_select |
bool |
Multi-select |
plan_ready
A finished plan presented for approval (present_plan), parking the planning freeze until you choose.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
plan_path |
str |
Path to the saved plan file (under <workdir>/.kin/plans/) — always populated, since write_plan persists unconditionally |
content |
str |
The plan body, display-sanitized at capture (terminal controls, bidi overrides, and zero-width characters removed; Markdown newlines and tabs preserved) |
options |
list[dict] |
The three action choices — Approve & execute (keep context), Approve, clear & re-inject (reseed just the plan + an execution preamble), Review plan first (adversarial critic). A dismissal (esc) keeps planning |
plan_mode_request
Kin asks to enter plan mode (enter_plan_mode). Approval begins the read-only
planning freeze before the
tool returns; a decline or dismissal leaves the session working and tells Kin
to proceed. A headless run reports needs-human instead.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
reason |
str |
Kin’s one-sentence reason, display-sanitized, at most 500 characters |
proposal_ready
A model-authored settings, skill-bundle, root-guidance, or isolated-artifact
proposal parked on ProposalModal, holding the turn until you approve or reject
the diff. The structured propose_settings, propose_skill, and
propose_agents_md tools use this same event. Mirrors plan_ready, but the
modal answer is the string "apply" / "reject" (decoded by
resolve_proposal), not the int index plan_ready carries. Headless execution
cannot resolve the modal and reports needs_human.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
label |
str |
Proposal label, such as "settings", "project AGENTS.md", or "project skill /triage" |
path |
str |
The exact file or canonical skill-directory target |
content |
str |
Review content: a unified diff or bundle-wide per-file diffs, plus harness-generated scope, shadowing, persistence, or refusal warnings |
handoff
A typed exchange Kin asks the environment to perform — open_url for one
browser effect, open_workspace for a separately confirmed new conversation
in one existing absolute directory, or attention_context for exact current
attention facts — and only when a local stdio client
declared the matching capability in its hello. The client answers through
handoff_result; the Session resolves the exact correlation id. {ok, detail}
is a fact rather than authority over Kin, and only attention_context may add
the bounded context list Kin requested. The TUI never receives this event,
because it never registers a handoff tool.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
kind |
str |
The handoff family: open_url, open_workspace, or attention_context |
payload |
dict |
The exact typed target — {"url": "https://…"}, {"path": "/home/…/scope"}, or {"kinds": ["selection", "window"]} |
Status / lifecycle
context_usage
The active conversation’s context-usage report. The TUI derives its concise
used / hard_prompt_limit reading from the two absolute fields; the other
fields remain available to its hover and detailed views such as /tokens.
usage remains the backward-compatible fraction of the effective compact
budget for clients that already consume it.
| Field | Type | Meaning |
|---|---|---|
usage |
float |
Fraction of the effective prompt budget used (0.0–1.0) |
used |
int |
Total prompt tokens |
max |
int |
The window size |
cached |
int |
Cached tokens (if the carrier reports them) |
level |
str |
Optional override (warning / critical) |
source |
str |
Provenance: model for provider-reported usage, preflight / prune / compaction for exact surface counts (empty only for an older producer) |
output_reserved |
int |
Output tokens reserved from the shared context window |
hard_prompt_limit |
int |
Largest prompt Kin can safely admit after output reservation and framing margin |
compact_at |
int |
Effective auto-compact point; equals the hard limit when no earlier point applies |
todos
The pinned task-panel contents. A successful todos call emits the complete
replacement list, with at most one in_progress item. A call containing
multiple active items returns an error without emitting this replacement
event. See Tasks vs todos for how
this differs from the tasks DAG below.
| Field | Type | Meaning |
|---|---|---|
todos |
list[dict] |
The todo items (content, status ∈ pending / in_progress / completed) |
Over the local protocol this
event reaches only clients that declared todos, bounded to 64 rows of
200-character text with a truncated flag.
Registered subagent lifecycle
subagent_started, subagent_progress, and subagent_completed are the canonical lifecycle for every registered physical run — foreground or background, isolated task or full-context fork_agent. Join a run by (agent_id, run_seq) and place it beneath parent_tool_call_id; profile, execution, and launch_kind repeat on every phase so a client never has to parse tool prose or depend on arrival order. See Subagents.
Kin emits one start after the root registry accepts the row and before any child tool activity can reach the client. It emits one completion after the authoritative row settles, including interruption, kill, or foreground turn cancellation while the run is queued or active. Forwarded child tool_call, tool_result, and tool_stream events carry the same parent_tool_call_id, so a client can fold them into the delegation entry. A retained agent_message continuation increments run_seq and rebinds the parent id to that current call.
None of these events carry the subagent’s prompt, prose, or diagnostics. Lifecycle is presentation/control metadata; the registry and journal remain authority, buffered prose stays behind agent_output, and exact diagnostics stay behind agent_inspect.
subagent_started
Emitted once for every registered foreground or background physical run.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
Stable retained-agent id (for example agent2) |
profile |
str |
Profile name (general, planner, fork, or a custom profile) |
parent_tool_call_id |
str |
Current parent call that owns this run; a continuation uses its current agent_message call |
run_seq |
int |
Physical run sequence on this retained row, starting at 1 |
execution |
str |
foreground or background for this run; a continuation may change mode |
launch_kind |
str |
task or fork |
context_scope |
str |
isolated for task, full_conversation for fork_agent |
inherited_turns |
int |
Represented human turns inherited by a full-context fork; 0 for an isolated task |
route_id |
str |
Resolved model-route id, or empty when no named route owns the child |
provider_preset |
str |
Resolved provider preset, when present |
model |
str |
Resolved child model id |
subagent_dispatched
Background-only compatibility projection emitted beside subagent_started when task or fork_agent uses run_in_background=true. The Textual ambient bar still uses its bounded prompt preview; new clients use subagent_started as lifecycle authority.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
The background agent id (for example agent1) |
profile |
str |
The profile name (general / researcher / coder / custom) |
prompt_preview |
str |
First line of the prompt, truncated to 60 chars (for the row) |
parent_tool_call_id |
str |
The parent’s task tool-call id — the UI registers it so the agent’s forwarded child tool calls fold into its ambient-strip row (live activity), not the transcript |
context_scope |
str |
isolated for task, full_conversation for fork_agent |
inherited_turns |
int |
Number of represented human turns inherited by a full-context fork; 0 for isolated tasks |
subagent_progress
Emitted for cooperative and health transitions on a registered run: pause_requested, paused,
resumed, interrupted, stale, healthy, and closed.
It also carries what the run is doing between its tool calls: thinking,
responding, and retrying. Those phases exist so a delegation entry can
stay alive during a long reasoning pass, a long answer, or a bounded retry
backoff. They carry no message and no child content — the phase is the
whole fact. A phase change publishes at once and an unchanged phase at most
every five seconds, so a streaming child costs one small event every few
seconds rather than one per chunk. Treat an unknown phase as ordinary
liveness: the vocabulary is additive.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
Stable retained-agent id |
phase |
str |
The phase label |
message |
str |
Optional message text |
parent_tool_call_id |
str |
Current parent call for this run |
run_seq |
int |
Current physical run sequence |
profile |
str |
Agent profile |
execution |
str |
foreground or background for this run |
launch_kind |
str |
task or fork |
subagent_completed
Emitted once for a registered subagent run’s terminal state. The panel flips the row while the root event inbox independently regains MCA attention.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
Stable retained-agent id |
status |
str |
ok / error / killed |
duration_s |
float |
Wall-clock seconds from spawn to completion |
tool_calls |
int |
Number of tool calls made by the subagent |
prompt_tokens |
int |
Prompt tokens the subagent used |
completion_tokens |
int |
Completion tokens (0 on backends that don’t report) |
lifecycle |
str |
Terminal lifecycle (idle or closed) |
outcome |
str |
Normal/error/interruption outcome |
done_reason |
str |
Loop terminal reason |
run_seq |
int |
Physical run sequence on this row |
error_available |
bool |
Exact diagnostics exist behind agent_inspect |
context_scope |
str |
isolated or full_conversation |
inherited_turns |
int |
Inherited human-turn count for the launch snapshot |
parent_tool_call_id |
str |
Current parent call for this run |
profile |
str |
Agent profile |
execution |
str |
foreground or background for this run |
launch_kind |
str |
task or fork |
Tasks DAG
See Tasks for the tasks tool this event mirrors.
tasks_changed
Emitted after every tasks tool call. Carries the full post-mutation DAG snapshot — the same authoritative shape the tool returns, so the tool_result and the event agree (and the panel reads the same shape the model sees).
| Field | Type | Meaning |
|---|---|---|
snapshot |
dict |
{tasks: [{id, content, status, blockedBy, blocks, ...}, ...]} |
Over the local protocol it is gated and bounded like todos, and each task
carries exactly id, content, status, blockedBy, and blocks.
Retired workflow telemetry
The scripted workflow runtime no longer emits workflow_started,
workflow_phase, workflow_log, workflow_agent_started,
workflow_agent_status, or workflow_finished. Converted skills use ordinary
turn and supervised subagent events. Historical workflow tool calls and
results can still appear in generic conversation replay; clients should not
interpret them as an executable saved run.
Terminal
done
Emitted at the end of every turn. In a headless run most non-clean reason values map directly onto the exit-code contract (turn_cap/token_budget → exit 1, an unresolved blocking event → exit 2).
The root’s inactive thinking closer always precedes this event. A cancelled
turn closes the phase before interrupted instead and does not invent a clean
done.
| Field | Type | Meaning |
|---|---|---|
reason |
str |
stop (clean) / error / persistence_error / loop_detected / interrupted / turn_cap / token_budget / truncated / retired |
Non-clean reasons mount a SystemNote + transient notify; clean / empty reasons stay silent. turn_cap and token_budget are the fleet-safety caps; loop_detected is the doom-loop guard.
persistence_error means a semantic journal checkpoint failed and Kin stopped
before crossing the next model/tool boundary; the accompanying error event
states whether tool outcomes may already have changed external state.
interrupted
Emitted when you press ++escape++ mid-turn (after the worker re-raises CancelledError and any in-flight tools clean up). See Rewind & retry for what happens to an interrupted turn’s history.
error
A harness-level error (back-end outage that exhausted retries, a config that fails resolution). A headless run’s error paths are documented in Headless runs § Exit codes.
| Field | Type | Meaning |
|---|---|---|
message |
str |
The error message |
recoverable |
bool |
Guidance hint, not a retry gate. true for a transient/endpoint-reachability failure (a dead port, a 5xx/429 after retries exhausted); false (the default) for a request rejection (a 4xx — bad api key / model id / base url) the user must fix first. The UI renders different guidance from it (“the endpoint may be unreachable …” vs “the request was rejected …”) and offers a focusable retry control either way. Set only at the permanent backend-error emit site (loop.py, re-running _is_transient on the cause); every other error emit site leaves the false default. |
redrive |
bool |
Retry gate — distinct from recoverable. true ONLY at the single turn-path backend-hard-error emit site (loop.py’s permanent-error branch). Drives the UI’s RetryNote affordance + the safe re-drive path: Session.pop_for_redrive() pops the failed turn’s user message + trailing partial-assistant and rewrites the journal so a re-send lands ONE fresh user turn (no dup). Every other error site leaves the false default — a /compact failure emit, or a generic catch in _run_turn, would otherwise re-drive into the prior COMPLETED turn and silently erase user input. Companion to recoverable: the guidance bit tells the UI what to render; the redrive bit tells it whether to enable the retry button. |