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 Markdown

The 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 active value 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 passes markup=False on the toast so a compromised / malicious server can’t phish the visible label via markup-shaped strings. The session line and banner render through literal rich.text.Text values without markup interpretation. See REFERENCE.md § Display sinks for 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.01.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, statuspending / 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.