Kin / Reference
Event vocabulary
The seam between the harness and any client (the Textual UI is one; a wire client is still possible later). Every event the harness emits — its type string and its fields — is the contract. A headless kin -p run colle…
Read as MarkdownThe seam between the harness and any client (the Textual UI is one; a wire client is still possible later). Every event the harness emits — its type string and its fields — is the contract. A headless kin -p run collects the same events into its exit code + report instead of painting them; the Outpost scheduler drives kin -p subprocesses and surfaces their terminal state (done reason, needs_human) through its own job API.
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 containment line for the welcome banner (e.g. auto · shell sandboxed (Seatbelt)); empty in strict mode |
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 top bar’s live model slot 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 top bar never blanks out. |
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 top bar / banner paths render viarich.text.Text.append(str, style=…)which treats the string as literal text (no 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
Session nudges.
| 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 next to the mode badge). Mirrors
nudges_changed as a human-visible state echo, but with a deliberate
asymmetry: the model can ENTER planning (dispatching the read-only planner
subagent, whose write_plan call auto-enters the freeze on its first write —
a capability reduction, so it’s non-escalating and safe to self-trigger),
while only a human EXITs it (the plan-approval modal or a bare /plan).
Never persisted — a transient overlay, not a mode.
| Field | Type | Meaning |
|---|---|---|
on |
bool |
Whether the read-only planning freeze is now on |
effort_changed
Emitted on /effort [low|medium|high|...] and once at session start when a
resumed session restored the level on. Drives the reasoning-effort chip on
the TopBar. Mirrors nudges_changed / planning_changed shape (a
state echo of a wire-time sampling knob).
| Field | Type | Meaning |
|---|---|---|
level |
str |
The reasoning-effort level now active (low / medium / high / model-specific) |
error |
str |
Empty on success; non-empty if the level couldn’t be applied (the UI surfaces the error in the chip) |
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 UI groups these under one AssistantMessage per message_id.
| 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. Root TUI sessions do not enable this retry posture.
| 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 reasoning / thinking chunk. The UI renders a collapsible Reasoning block per message_id.
| 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 |
thinking
Soft-toggle for the model’s “thinking” mode (for example Qwen or a profiled
Poolside Laguna serve using enable_thinking).
| Field | Type | Meaning |
|---|---|---|
active |
bool |
Whether thinking is currently active |
status |
str |
Optional status text |
Tools
See Tools for what each tool does; this section only covers the wire shape.
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); a shared id the UI groups by. 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).
| 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 status bar can flip to drafting <name>… N chars 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 / SANDBOX 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 |
Precomputed unified diff for edit approvals; empty when unavailable |
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 |
proposal_ready
A model-proposed settings change parked on the artifact-edit ProposalModal (the propose_settings tool — see settings.toml keys § Editing settings from inside kin), parking the turn until you approve or reject the diff. Mirrors plan_ready, but the modal answer is the string "apply" / "reject" (decoded by resolve_proposal), not the int index plan_ready carries.
| Field | Type | Meaning |
|---|---|---|
id |
str |
The correlation id |
label |
str |
The artifact label (e.g. "settings") |
path |
str |
The file the change targets (~/.kin/settings.toml) |
content |
str |
The unified diff plus the harness-generated refusal banner naming any human-only keys the model tried to set (deterministic tool text the model’s narrative can’t suppress) |
sandbox_access_request
A typed request for a bounded sandbox capability and lifetime, parked on the dedicated access modal. See Auto mode & the OS sandbox.
| Field | Type | Meaning |
|---|---|---|
id |
str |
Correlation id used by the modal resolver |
request_id |
str |
Identity of the retained denied operation |
executor |
str |
Executor that needs access (shell or run_code) |
operation |
str |
Sanitized operation summary |
cwd |
str |
Requested working directory |
reason |
str |
Why the capability is needed |
permissions |
dict |
Typed capability profile |
evidence |
str |
Reactive denial evidence, when present |
reactive |
bool |
Whether a real sandbox denial triggered the request |
workspace_allowed |
bool |
Whether workspace writes are included |
state_loss |
bool |
Whether denying may discard live executor state |
partial_execution |
bool |
Whether the retained operation may have partly run |
risks |
list |
Human-facing bounded risk notes |
Durable external-work markers
These events let a live client mount the same quiet Beam card that journal replay reconstructs later. The journal marker remains the durable authority; the event is presentation metadata only.
beam_submitted
Emitted after Outpost accepts a Beam handoff.
| Field | Type | Meaning |
|---|---|---|
job_id |
str |
Outpost job identity used for live status |
run_id |
str |
Initial run identity when the submit response supplies one |
workspace |
str |
Exact Outpost workspace slug |
branch |
str |
Feature branch handed off |
folder |
str |
Short local checkout label |
workdir |
str |
Originating local checkout |
focus |
str |
Human-authored handoff focus |
source |
str |
Submission path (for example native) |
beam_returned
Emitted by the deterministic return controller after the locally recorded
feature branch has fast-forwarded in its originating checkout and the pulled
commit count is known. The prompt-driven /beam back path reaches the
controller through the always-ask outpost-send return operation.
| Field | Type | Meaning |
|---|---|---|
job_id |
str |
The returned Outpost job |
run_id |
str |
The terminal (or explicitly partial) run returned |
workspace |
str |
Exact Outpost workspace slug from the local locator |
branch |
str |
Locally recorded feature branch that fast-forwarded |
folder |
str |
Short local checkout label |
workdir |
str |
Originating local checkout |
commits |
int |
Deterministic count of commits pulled, when known |
partial |
bool |
Whether the human explicitly returned a needs-human checkpoint |
source |
str |
Return path (currently controller) |
Status / lifecycle
context_usage
The context-window meter.
| 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 |
The budget side the count is from |
output_reserved |
int |
Output tokens reserved from the shared context window |
compact_at |
int |
Effective prompt-meter denominator and compact trigger/hard limit |
todos
The pinned task-panel contents. Full rewrite on every todos tool call. 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) |
Background subagent lifecycle
Three events layer on top of the existing tool_call / tool_result plumbing (which still re-stamps through ForwardingEmit). They drive the ambient bar and agent panel; the root’s separate exactly-once event inbox drives automatic MCA attention. All three carry an agent_id for correlation. See Subagents.
None of these events carry the subagent’s prose — the model-facing reminder is metadata-only (a bg agent could have fetched web content); the human-facing panel reads it from the registry buffer.
subagent_dispatched
Emitted when a bg subagent is spawned (task or fork_agent with
run_in_background=true). The panel paints the new row immediately.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
The bg agent id (e.g. 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: pause_requested, paused,
resumed, interrupted, stale, healthy, and closed.
| Field | Type | Meaning |
|---|---|---|
agent_id |
str |
The bg agent id |
phase |
str |
The phase label |
message |
str |
Optional message text |
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 |
The bg 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 |
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, ...}, ...]} |
Dynamic workflows
Telemetry for a workflow tool run (a model-authored orchestration script
fanning out subagents). All four carry the workflow_id (the tool-call id) so a
client can correlate a run’s started → phase/log/agent → finished arc. The
authoritative state lives in Session.workflows (the /workflows polling
modal reads it there, mirroring the agent panel); these events are the
human-facing transcript breadcrumbs. None carry subagent prose — the only
thing re-entering model context is the tool’s returned string.
workflow_started
Emitted when a workflow run begins (after approval).
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
goal |
str |
The one-line goal shown at approval |
workflow_phase
Emitted on each phase(title) call in the script.
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
title |
str |
The new phase title |
workflow_log
Emitted on each log(message) call in the script.
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
message |
str |
The log line |
workflow_agent_started
Emitted when a subagent appears in a workflow fan-out (the agent() primitive is
called). The renderer learns the agent exists and its row label here; later
state changes come via workflow_agent_status.
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
agent_id |
str |
The agent’s id, minted as <workflow_id>:<n> |
label |
str |
The row name — the label opt, else a prompt truncation |
status |
str |
Starts at queued |
workflow_agent_status
Emitted on each per-agent transition: queued → running → done / error, plus
two extra states — structuring (the brief forced-tool / prompt-JSON call after a
schema agent’s own loop ends — see structured output)
and cached (a resume hit replayed the result from the run-journal instead of
dispatching — see resume). The
renderer keys on agent_id to update the row.
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
agent_id |
str |
The agent whose state changed |
status |
str |
queued / running / structuring / done / error / cached |
workflow_finished
Emitted on the run’s terminal state.
| Field | Type | Meaning |
|---|---|---|
workflow_id |
str |
The workflow’s tool-call id |
status |
str |
done / error / cancelled |
agents_run |
int |
Total subagents the run spawned |
A workflow tool call mounts a WorkflowCard (anchored on the
tool_call(name="workflow") event, which carries the goal and the script —
the card pre-parses the script’s phase("…") literals into its upcoming-steps
trail), and the renderer registers that id as a suppression key: every forwarded
child tool_call / tool_result / tool_stream stamped with that
parent_tool_call_id is dropped, so the card replaces the subagents’
tool-call firehose. The card is driven by workflow_phase (the trail + the
status-bar spinner label) + workflow_agent_started / workflow_agent_status
(the rows) + workflow_log (the card’s one-line narrator; the /workflows
panel keeps the full history) and finalized by the workflow’s own tool_result
(which carries the distilled string). workflow_started / workflow_finished
are no-ops in the transcript (the card is the surface).
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).
| Field | Type | Meaning |
|---|---|---|
reason |
str |
stop (clean) / 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.
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 or workflow-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. |