kit

Subagents

Kit supports multi-agent orchestration through both subprocess spawning and in-process subagents. The built-in status bar above the parent activity line shows active runs and recent results.

Subprocess pattern

Spawn Kit as a subprocess for isolated agent execution:

kit "Analyze codebase" \
    --json \
    --no-session \
    --no-extensions \
    --quiet \
    --model anthropic/claude-haiku-latest

Key flags for subprocess usage:

Flag Purpose
--quiet Stdout only, no TUI
--no-session Ephemeral, no persistence
--no-extensions Prevent recursive extension loading
--json Machine-readable output
--system-prompt Custom system prompt (string or file path)

Positional arguments are the prompt. @file arguments attach file content as context.

Built-in subagent tool

Kit includes a built-in subagent tool that the LLM can use to delegate tasks to independent child agents:

subagent(
    task: "Analyze the test files and summarize coverage",
    agent: "explore",                                  // optional named agent
    model: "anthropic/claude-haiku-latest",   // optional
    system_prompt: "You are a test analysis expert.",  // optional
    timeout_seconds: 300,                              // optional, max 1800
    session_id: "..."                                  // optional, resume a previous subagent
)

Subagents run as separate in-process Kit instances and inherit the parent’s active tools minus subagent (to prevent recursion); named-agent presets and tool allowlists can narrow that set further. They can run in parallel.

Subagent status bar

The TUI shows a compact status bar above the parent activity line, so you can check child runs without opening their conversations. It includes in-process runs started by the LLM, extensions, or direct SDK calls, not separate Kit subprocesses.

Indicator Run status
Blinking ● Starting or running
✓ Completed
✗ Failed or timed out
■ Stopped

Each indicator shows the agent name, or agent when no name is set. Finished runs remain in the bar for 10 seconds after they end. The bar disappears when there are no active or recent runs. This display limit does not remove retained history: older runs remain available in the picker until history retention removes them.

Inspecting subagent conversations

The native subagent inspector shows a subagent run in place of the parent conversation, with the same view as the parent: streamed text and reasoning, live shell output, tool results, and the activity row with the current tool and the elapsed time. Run /subagents to open the inspector on the newest retained run.

To choose a different run, press Ctrl+Alt+A while composing a prompt or while the parent agent works. The picker lists active and retained finished runs with their agent name, status, model, and first prompt line when available. Use Up/Down to choose a run and Enter to inspect it. Esc closes the picker without cancelling the parent turn. The status bar shows the shortcut when space permits, but the shortcut also works when the bar is hidden.

The inspector opens on the chosen run and shows siblings with the same ParentSessionID. Kit keeps a view for each run from the start of the run, so the inspector updates live and the parent conversation continues to update behind it. The composer is replaced by a bar with the agent name, the run status, the model, and the keys of the inspector. If a parent dialog opens (for example an extension prompt or a password prompt), Kit returns to the parent conversation. If no runs are retained, Kit shows No retained subagent runs. instead of opening the picker or inspector.

Key Action
Left / Right Switch between sibling runs
Up / Down Scroll one line
Page Up / Page Down Scroll one page
Home / End Go to the start / end
Esc Return to the parent conversation
Ctrl+K Stop the selected run if it is still active

The conversation is read-only: you cannot send a follow-up prompt from this view. Stopping a run is still available. The inspector uses in-memory run history, not saved session files: it retains up to 100 finished runs plus all active runs, with up to 500 event records per run. Adjacent text, reasoning, and tool output chunks can be combined into one record. A warning shows when earlier event records have been dropped. Restarting Kit does not restore this history, and resuming a child session does not load its earlier transcript into the inspector.

Command compatibility

/subagent-sessions and its alias /agents have been removed. Use /subagents or Ctrl+Alt+A instead. /subagents always opens the built-in inspector; an extension registered under that name no longer takes precedence.

The subagent-monitor.go example now uses /subagent-monitor for its transcript overlay. Its Ctrl+Alt+S shortcut is unchanged. Custom extensions that used /subagents must choose a different command name to remain accessible.

Killing a running subagent

In the TUI, run /kill-subagent (alias /ks) while subagents run. A picker shows each running subagent with its agent name, run time, model and task. Select one and press Enter to stop it. The subagent tool call then returns this result to the parent agent:

Subagent was killed by the user after 28s. It did not complete its task. Do not start it again unless the user asks you to.

The parent turn continues. Esc closes the picker and does not cancel the turn. The picker lists all in-process subagents, including subagents that extensions start with SpawnSubagent.

Session linking and resuming

Subagent runs are session-backed by default, and their sessions are linked to the parent in both directions:

Passing a previous run’s subagent_session_id back via the session_id parameter resumes that child session instead of starting fresh — the subagent keeps its accumulated context (files read, findings, state), making iterative delegation cheap:

subagent(task: "Research how session persistence works")
→ Subagent completed successfully in 12s.
  subagent_session_id: abc123...

  Result:
  ...

subagent(task: "Now check how it handles errors", session_id: "abc123...")
→ follow-up runs in the same child session, reusing its context

Copy the exact returned ID into session_id; do not use a run ID or tool call ID. The ID line appears before the result body, so truncating a long child response does not remove it. Client metadata is available to the UI and SDK, but is not sent to the model. Including the ID in the text lets the model resume the child and keeps the ID in saved parent conversation history after Kit restarts. Existing metadata consumers and SDK calls are unchanged.

Resumed sessions keep their original parent link; an unknown session_id is an error and fails before the child starts. If an older saved tool result has no ID line, use the child session’s header ID or a previously recorded SubagentResult.SessionID; do not guess an ID. Resuming requires the saved child session to remain available and is incompatible with ephemeral (NoSession) runs.

Named agents

Named agents are reusable subagent presets defined in markdown files. They are advertised in the subagent tool description, so the LLM can delegate to the right specialist by name — with a preset system prompt, model, tool allowlist, temperature, and timeout.

Definition files

The filename (minus .md) is the agent name; YAML frontmatter configures it; the markdown body is the system prompt:

---
description: Reviews code for quality and best practices   # required
model: anthropic/claude-sonnet-4                           # optional model override
tools: [read, grep, find, ls]                              # optional tool allowlist
temperature: 0.1                                           # optional
timeout: 300                                               # optional, seconds
hidden: false                                              # optional: resolvable but not advertised
disabled: false                                            # optional: remove this agent (and anything it shadows)
---
You are in code review mode. Focus on correctness, security, and
maintainability. Report findings with file paths and line references.

Discovery and precedence

Definitions are discovered from (highest to lowest precedence):

Location Scope
<project>/.agents/agents/*.md Project-local (cross-client convention)
<project>/.kit/agents/*.md Project-local (Kit-specific)
~/.config/kit/agents/*.md User-level ($XDG_CONFIG_HOME aware)
Built-in Ships with Kit

Higher-precedence definitions override lower ones by name, so a project can replace — or disable via disabled: true — a built-in or user-level agent.

Two built-in agents ship with Kit:

Agent Tools Purpose
general all tools General-purpose research and multi-step task execution
explore read, grep, find, ls Read-only codebase exploration

Tool allowlists

An agent without a tools: list gets the default subagent tool set (everything except subagent, preventing recursion). With a tools: allowlist, the subagent is restricted to exactly those tools — a read-only explore-style agent cannot edit files or run commands. Explicit model / system_prompt / timeout_seconds arguments in the tool call override the agent’s presets.

Disable named-agent discovery entirely with --no-agents, the no-agents config key, or KIT_NO_AGENTS=true.

--no-skills and --no-agents also apply to the subagents that the subagent tool starts.

Extension subagents

Extensions can spawn subagents programmatically:

_, result, err := ctx.SpawnSubagent(ext.SubagentConfig{
    Prompt:       "Review this code for security issues",
    Model:        "anthropic/claude-sonnet-latest",
    SystemPrompt: "You are a security auditor.",
    Blocking:     true,
})

With Blocking: false (the default), the subagent runs in a background goroutine and SpawnSubagent returns immediately with a non-nil handle (result is nil); use OnComplete/OnEvent callbacks or the handle to observe the run:

handle, _, err := ctx.SpawnSubagent(ext.SubagentConfig{
    Prompt: "Write unit tests for UserService",
    OnOutput: func(chunk string) {
        // Live assistant text chunks (e.g. update a widget)
    },
    OnComplete: func(result ext.SubagentResult) {
        ctx.SendMessage("Subagent finished:\n" + result.Response)
    },
})
// handle.Kill()   — cancel the running subagent
// handle.Wait()   — block until completion, returns SubagentResult
// <-handle.Done() — channel that closes on completion

Background subagents run in-process (no subprocess): they get their own session, event bus, and agent loop, inherit the parent’s active tools minus the subagent tool, and do not load extensions. Sessions are persisted by default; set NoSession: true for ephemeral runs.

Set SessionID to a previous run’s SubagentResult.SessionID to resume that subagent’s session for follow-up prompts, and ParentSessionID to override the parent link recorded in the child session’s header (it defaults to the host’s active persisted session — see Session linking and resuming).

Monitoring subagents from extensions

When the LLM (not the extension itself) spawns a subagent using the subagent tool, extensions can monitor its activity in real-time using three lifecycle event handlers:

// Track active subagents and display their output
var subagentWidgets map[string]*SubagentWidget

func Init(api ext.API) {
    // Subagent started by the main agent
    api.OnSubagentStart(func(e ext.SubagentStartEvent, ctx ext.Context) {
        // e.ToolCallID — unique ID for this subagent invocation
        // e.Task — the task/prompt sent to the subagent
        widget := NewWidget(e.ToolCallID, e.Task)
        subagentWidgets[e.ToolCallID] = widget
        ctx.SetWidget(widget.Config())
    })

    // Real-time streaming from subagent
    api.OnSubagentChunk(func(e ext.SubagentChunkEvent, ctx ext.Context) {
        // e.ToolCallID — matches the start event
        // e.ChunkType — "text", "tool_call", "tool_execution_start", "tool_result"
        // e.Content — text content
        // e.ToolName — tool name (for tool chunks)
        // e.IsError — true if tool result failed
        widget := subagentWidgets[e.ToolCallID]
        if widget != nil {
            widget.AddOutput(e)
            ctx.SetWidget(widget.Config())
        }
    })

    // Subagent completed
    api.OnSubagentEnd(func(e ext.SubagentEndEvent, ctx ext.Context) {
        // e.Response — final response from subagent
        // e.ErrorMsg — error message if subagent failed
        widget := subagentWidgets[e.ToolCallID]
        if widget != nil {
            widget.MarkComplete(e.Response, e.ErrorMsg)
            ctx.SetWidget(widget.Config())
            delete(subagentWidgets, e.ToolCallID)
        }
    })
}

Event structs:

type SubagentStartEvent struct {
    ToolCallID string  // Unique ID for this subagent invocation
    Task       string  // The task/prompt sent to subagent
}

type SubagentChunkEvent struct {
    ToolCallID string  // Matches SubagentStartEvent.ToolCallID
    Task       string  // Task description
    ChunkType  string  // "text", "tool_call", "tool_execution_start", "tool_result"
    Content    string  // For text chunks
    ToolName   string  // For tool-related chunks
    IsError    bool    // For tool_result chunks
}

type SubagentEndEvent struct {
    ToolCallID string  // Matches start event
    Task       string  // Task description
    Response   string  // Final response from subagent
    ErrorMsg   string  // Error message if failed
}

This enables building monitoring widgets that display real-time activity from all subagents spawned by the main agent.

Go SDK subagents

The SDK provides in-process subagent spawning:

result, err := host.Subagent(ctx, kit.SubagentConfig{
    Prompt:       "Summarize the changes in this PR",
    Model:        "anthropic/claude-haiku-latest",
    SystemPrompt: "You are a code reviewer.",
    Timeout:      5 * time.Minute,
})

Set Agent to run the task with a named agent’s presets; explicitly set fields still win:

result, err := host.Subagent(ctx, kit.SubagentConfig{
    Prompt: "Map out the session persistence flow",
    Agent:  "explore", // preset prompt + read-only tool allowlist
})

Set SessionID to a previous run’s SubagentResult.SessionID to resume that subagent’s session, so follow-ups reuse its accumulated context instead of re-establishing it from scratch:

first, err := host.Subagent(ctx, kit.SubagentConfig{
    Prompt: "Research how session persistence works",
})

followUp, err := host.Subagent(ctx, kit.SubagentConfig{
    Prompt:    "Now check how it handles errors",
    SessionID: first.SessionID, // resume the same child session
})

New child sessions automatically record the parent’s session ID in their header when the parent is session-backed (see Session linking and resuming); set ParentSessionID to override the recorded link.

Children inherit the parent’s Options.Providers, so Model can name an app-supplied custom provider backend (for example "local/qwen3-8b").

Children also inherit the parent’s discovery switches: Bare, SkipConfig, NoContextFiles, NoSkills, NoExtensions and NoAgents. A switch only goes from on to off, so a parent never turns a feature back on in a child. A subagent of an isolated Kit therefore does not load .kit.yml, AGENTS.md, skills, extensions or named agents.

A parent with NoSession gives its children an in-memory session too, so a SessionID resume request is rejected. When Tools is nil, the child gets only the core tools that the parent enabled: a parent with DisableCoreTools gives it none, a parent with CoreToolList gives it only those tools, and a parent with Tools gives it those same tools.

List and stop running subagents with RunningSubagents and KillSubagent. The list includes subagents that the LLM starts through the subagent tool (the ID is the tool call ID), subagents that extensions start, and direct Subagent calls. A killed run returns kit.ErrSubagentKilled:

for _, sa := range host.RunningSubagents() {
    fmt.Println(sa.ID, sa.Agent, sa.Model, time.Since(sa.StartedAt), sa.Prompt)
}
if !host.KillSubagent(id) {
    // No subagent with this ID is running (it may have completed).
}

_, err := host.Subagent(ctx, cfg)
if errors.Is(err, kit.ErrSubagentKilled) {
    // Stopped with KillSubagent.
}

Retained run snapshots

Use SubagentRuns and GetSubagentRun to inspect active and finished runs without registering a live event listener. These methods include runs started by the LLM, extensions, and direct SDK calls.

API Use when
RunningSubagents() []RunningSubagent You need only active runs, including child setup
SubagentRuns() []SubagentRun You need retained active and finished runs, oldest first
GetSubagentRun(id string) (SubagentRun, bool) You need a snapshot for one run ID
KillSubagent(id string) bool You need to stop an active run
SubscribeSubagentRuns(func(SubagentRunUpdate)) func() You need every change to every run (registration, each child event, each status change) as it happens
SubscribeSubagent / SubagentConfig.OnEvent You need live event callbacks rather than retained history
for _, run := range host.SubagentRuns() {
    fmt.Println(run.ID, run.Status, run.SessionID, run.ParentSessionID)
    if snapshot, ok := host.GetSubagentRun(run.ID); ok {
        fmt.Printf("Retained: %d events; dropped: %d\n",
            len(snapshot.Events), snapshot.DroppedEvents)
    }
}

SubagentRun.ID identifies an execution and is the ID to pass to GetSubagentRun or KillSubagent. It is not the child’s SessionID, which you pass to SubagentConfig.SessionID to resume a session. ParentSessionID records the configured parent ID, or the host’s session ID when no override is supplied. SessionID is empty until child setup succeeds.

Status is starting, running, completed, failed, stopped, or timed_out. FinishedAt is zero until the run ends; Error describes a failed, stopped, or timed-out run when available.

History is local to the host Kit instance. It retains up to 100 finished runs plus all active runs and up to 500 event records per run. Events is in emission order; adjacent text, reasoning, and tool output chunks can be combined. DroppedEvents counts records removed by the event limit. Snapshot calls copy the event slice, but do not deep-copy payloads inside each event. GetSubagentRun returns false for an unknown or evicted ID. History is also available for NoSession runs, but it is not restored from saved sessions.

SubscribeSubagentRuns calls its listener synchronously for each change, so the listener must return quickly. Each SubagentRunUpdate carries the run ID, the child event (nil for a status-only change), the status and error after the change, and Seq. Seq increases by one for each update of a run and matches SubagentRun.Seq, so a consumer that starts from a snapshot can skip the updates that the snapshot already contains. Subscribe before you take the snapshot, or a change between the two calls is lost. Buffer the updates under a mutex until the snapshot is ready:

var (
    mu      sync.Mutex
    ready   bool
    pending []kit.SubagentRunUpdate
    lastSeq uint64
)
apply := func(u kit.SubagentRunUpdate) { // call with mu held
    if u.Seq <= lastSeq {
        return // already in the snapshot
    }
    lastSeq = u.Seq
    // apply u.Event / u.Status (must not block)
}
unsub := host.SubscribeSubagentRuns(func(u kit.SubagentRunUpdate) {
    if u.RunID != id {
        return // another run
    }
    mu.Lock()
    defer mu.Unlock()
    if !ready {
        pending = append(pending, u)
        return
    }
    apply(u)
})
defer unsub()

snapshot, _ := host.GetSubagentRun(id)
mu.Lock()
lastSeq, ready = snapshot.Seq, true
for _, u := range pending {
    apply(u)
}
pending = nil
mu.Unlock()

RunningSubagents still lists only active runs, and KillSubagent still returns false when the run is no longer active. Existing live callbacks continue to receive events; retention does not replace them.

Inspect the discovered definitions:

defs := host.GetAgents()             // snapshot of discovered definitions
def, ok := host.GetAgent("explore")  // lookup by name

// Standalone discovery without a Kit instance:
defs, err := kit.LoadAgentDefinitions("") // "" = current working directory

Real-time subagent events

Use SubscribeSubagent to receive real-time events from LLM-initiated subagents (i.e., when the model uses the subagent tool). Register inside an OnToolCall handler using the tool call ID:

host.OnToolCall(func(e kit.ToolCallEvent) {
    if e.ToolName == "subagent" {
        host.SubscribeSubagent(e.ToolCallID, func(event kit.Event) {
            switch ev := event.(type) {
            case kit.MessageUpdateEvent:
                fmt.Print(ev.Chunk) // streaming text from child
            case kit.ToolCallEvent:
                fmt.Printf("Child calling: %s\n", ev.ToolName)
            case kit.ToolResultEvent:
                fmt.Printf("Child result: %s\n", ev.ToolName)
            }
        })
    }
})

The listener receives the same event types as Subscribe() (ToolCallEvent, MessageUpdateEvent, ReasoningDeltaEvent, etc.) but scoped to the child agent’s activity. Listeners are cleaned up automatically when the subagent completes.

If no listeners are registered for a tool call, no parent-listener dispatch occurs. Child events are still retained for run snapshots and the native inspector.