# Kit

---

## JSON Output

# JSON Output

Use the `--json` flag to get structured output for scripting and automation:

```bash
kit "Explain main.go" --json --quiet --no-session
```

## Response format

```json
{
  "response": "Final assistant response text",
  "model": "anthropic/claude-haiku-latest",
  "stop_reason": "end_turn",
  "session_id": "a1b2c3d4e5f6",
  "usage": {
    "input_tokens": 1024,
    "output_tokens": 512,
    "total_tokens": 1536,
    "cache_read_tokens": 0,
    "cache_creation_tokens": 0
  },
  "messages": [
    {
      "role": "assistant",
      "parts": [
        {"type": "text", "data": "..."},
        {"type": "tool_call", "data": {"name": "...", "args": "..."}},
        {"type": "tool_result", "data": {"name": "...", "result": "..."}}
      ]
    }
  ]
}
```

## Fields

### Top-level

| Field | Type | Description |
|-------|------|-------------|
| `response` | string | The final assistant response text |
| `model` | string | The model that was used |
| `stop_reason` | string | Why the model stopped (e.g., `end_turn`) |
| `session_id` | string | Session identifier (omitted in `--no-session` mode) |
| `usage` | object | Token usage statistics |
| `messages` | array | Full conversation history |

### Usage

| Field | Type | Description |
|-------|------|-------------|
| `input_tokens` | int | Tokens sent to the model |
| `output_tokens` | int | Tokens generated by the model |
| `total_tokens` | int | Sum of input and output tokens |
| `cache_read_tokens` | int | Tokens read from prompt cache |
| `cache_creation_tokens` | int | Tokens written to prompt cache |

### Message parts

Each message contains a `parts` array with typed entries:

| Type | Description |
|------|-------------|
| `text` | Assistant text content |
| `tool_call` | Tool invocation with name and args |
| `tool_result` | Tool execution result |
| `reasoning` | Extended thinking content |
| `finish` | End-of-turn marker |

## Parsing in scripts

### bash + jq

```bash
result=$(kit "Count files" --json --quiet --no-session)
response=$(echo "$result" | jq -r '.response')
tokens=$(echo "$result" | jq '.usage.total_tokens')
```

### Go SDK

For Go programs, use the SDK's `PromptResult` method instead of parsing JSON:

```go
result, err := host.PromptResult(ctx, "Count files")
fmt.Println(result.Response)
fmt.Println(result.Usage.TotalTokens)
```

---

## Subagents

# Subagents

Kit supports multi-agent orchestration through both subprocess spawning and in-process subagents.

## Subprocess pattern

Spawn Kit as a subprocess for isolated agent execution:

```bash
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",
    model: "anthropic/claude-haiku-latest",   // optional
    system_prompt: "You are a test analysis expert.",  // optional
    timeout_seconds: 300                               // optional, max 1800
)
```

Subagents run as separate in-process Kit instances with full tool access (except spawning further subagents, to prevent infinite recursion). They can run in parallel.

## Extension subagents

Extensions can spawn subagents programmatically:

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

### 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:

```go
// 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:**

```go
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:

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

### 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:

```go
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 event dispatching overhead is incurred.

---

## Testing with tmux

# Testing with tmux

Kit's interactive TUI can be tested non-interactively using tmux. This is useful for automated testing, CI pipelines, and extension development.

## Basic pattern

```bash
# Start Kit in a detached tmux session
tmux new-session -d -s kittest -x 120 -y 40 \
  "output/kit -e ext.go --no-session 2>kit_stderr.log"

# Wait for startup
sleep 3

# Capture the current screen
tmux capture-pane -t kittest -p

# Send input
tmux send-keys -t kittest '/command' Enter

# Wait for response
sleep 2

# Capture updated screen
tmux capture-pane -t kittest -p

# Cleanup
tmux kill-session -t kittest
```

## Testing extensions

When testing extensions, the pattern is:

1. Build Kit with your changes
2. Start Kit in tmux with the extension loaded
3. Send slash commands or prompts
4. Capture and verify the screen output
5. Check stderr logs for errors

```bash
# Build first
go build -o output/kit ./cmd/kit

# Start with extension
tmux new-session -d -s kittest -x 120 -y 40 \
  "output/kit -e examples/extensions/widget-status.go --no-session 2>kit_stderr.log"

sleep 3

# Verify widget appears in screen
tmux capture-pane -t kittest -p | grep "Status"

# Send a slash command
tmux send-keys -t kittest '/stats' Enter
sleep 1
tmux capture-pane -t kittest -p

# Cleanup
tmux kill-session -t kittest
```

## Tips

- Use `-x` and `-y` to set consistent terminal dimensions
- Redirect stderr to a log file (`2>kit.log`) for debugging
- Use `--no-session` to avoid creating session files during tests
- Add sufficient `sleep` between commands for the TUI to render
- Use `grep` on captured pane output to verify specific content

---

## Commands

# Commands

## Authentication

For OAuth-enabled providers like Anthropic.

```bash
kit auth login [provider]          # Start OAuth flow (e.g., anthropic)
kit auth login [provider] --set-default  # Set provider's default model as system default
kit auth logout [provider]       # Remove credentials for provider
kit auth status                    # Check authentication status
```

## Model database

Manage the local model database that maps provider names to API configurations.

```bash
kit models [provider]        # List available models (optionally filter by provider)
kit models --all             # Show all providers (not just LLM-compatible)
kit update-models [source]   # Update model database
```

The `update-models` command accepts an optional source argument:
- *(none)* — update from [models.dev](https://models.dev)
- A URL — fetch from a custom endpoint
- A file path — load from a local file
- `embedded` — reset to the bundled database

## Extension management

```bash
kit extensions list          # List discovered extensions
kit extensions validate      # Validate extension files
kit extensions init          # Generate example extension template
```

### Installing extensions from git

```bash
kit install <git-url>        # Install extensions from git repositories
kit install -l <git-url>     # Install to project-local .kit/git/ directory
kit install -u <git-url>     # Update an already-installed package
kit install --uninstall <pkg> # Remove an installed package
kit install --all            # Install all extensions without prompting
```

## Skills

```bash
kit skill                    # Install the Kit extensions skill via skills.sh
```

### Skills CLI flags

Control which skills are loaded at startup:

```bash
# Load a specific skill file
kit --skill path/to/skill.md "prompt"

# Load multiple skill files or directories (flag is repeatable)
kit --skill ./skill1.md --skill ./skill2.md "prompt"

# Load all skills from a custom directory instead of the default locations
kit --skills-dir /path/to/skills "prompt"

# Disable all skill loading (auto-discovery and explicit)
kit --no-skills "prompt"
```

Skills are auto-discovered from `~/.config/kit/skills/`, `.kit/skills/`, and `.agents/skills/` by default. Use `--skills-dir` to override the project-local search root, or `--skill` to load files explicitly (which disables auto-discovery). `--no-skills` suppresses all skill loading regardless of other flags.

## Interactive slash commands

These commands are available inside the Kit TUI during an interactive session:

| Command | Description |
|---------|-------------|
| `/help` | Show available commands |
| `/tools` | List available MCP tools |
| `/servers` | Show connected MCP servers |
| `/model [name]` | Switch model or open model selector |
| `/theme [name]` | Switch color theme or list available themes |
| `/thinking [level]` | Set thinking level (off, none, minimal, low, medium, high) |
| `/compact [focus]` | Summarize older messages to free context |
| `/clear` | Clear conversation |
| `/clear-queue` | Clear queued messages |
| `/usage` | Show token usage |
| `/reset-usage` | Reset usage statistics |
| `/tree` | Navigate session tree |
| `/fork` | Fork to new session from an earlier message |
| `/new` | Start a new session (creates new session file) |
| `/name [name]` | Set or show session display name |
| `/resume` | Open session picker to switch sessions (alias: `/r`) |
| `/session` | Show session info |
| `/export [path]` | Export session as JSONL (default: auto-generated path) |
| `/import <path>` | Import a session from a JSONL file |
| `/share` | Upload session to GitHub Gist and get a shareable viewer URL |
| `/quit` | Exit Kit |

### Prompt history

Use **↑** and **↓** arrow keys to navigate through previously submitted prompts. Kit keeps the last 100 entries. Consecutive duplicates are skipped.

### Cancelling operations

Press **ESC twice** to cancel the current operation:
- During a tool call: rolls back the entire turn to maintain API message pairing
- During streaming: stops the response generation

This ensures that `tool_use` and `tool_result` messages are always sent to the API as matched pairs, avoiding errors from orphaned tool calls.

### External editor

Press **Ctrl+X e** to open your `$VISUAL` or `$EDITOR` in a temporary file pre-populated with the current input text. On save and quit, the edited content replaces the input textarea. On error exit (e.g., `:cq` in Vim), the original input is preserved.

### Mid-turn steering

Press **Ctrl+X s** during streaming to inject a system-level instruction mid-turn. This allows you to steer the conversation direction without waiting for the model to finish:

- Works during streaming output
- Sends a steering instruction as a system message
- Model continues from the interruption point with the new guidance

Example: While the model is writing code, press Ctrl+X s and type "Use async/await instead" to change the implementation approach.

### Image attachments

Attach images to your next prompt straight from the clipboard:

- Copy an image (e.g. a screenshot) to the system clipboard, then press **Ctrl+V** in the input to attach it.
- Press **Ctrl+U** to clear all pending image attachments.
- Attachments are sent alongside your text when you submit, and cleared afterward.

When a terminal supports color, Kit renders a small low-resolution **thumbnail preview** of each pending image directly in the input, below the `[N image(s) attached]` indicator, so you can confirm the right image was attached before sending.

The preview is drawn with Unicode half-block characters and ordinary terminal colors — not a graphics protocol — so it renders correctly inside terminal multiplexers like **tmux** and **zellij**. Thumbnails are capped to a small cell box for a glanceable, low-res look.

- Best fidelity needs a **truecolor** terminal (`COLORTERM=truecolor`); Kit degrades to 256-color where truecolor is unavailable.
- On terminals with neither, the preview is skipped and the `[N image(s) attached]` text indicator is shown alone.

You can also attach image files by referencing them with `@path/to/image.png` — binary files are auto-detected by MIME type. See [Quick Start](/quick-start) for the `@` attachment syntax.

## Prompt templates

### Creating templates

Templates use YAML frontmatter for metadata and support argument placeholders:

```markdown
---
description: Review code for issues
---
Review the following code for bugs and security issues.
Focus on $1 specifically.
```

Save to `~/.kit/prompts/review.md` or `.kit/prompts/review.md`.

### Using templates

Templates appear as slash commands:

```
/review error handling
```

### Argument placeholders

| Placeholder | Description |
|-------------|-------------|
| `$1`, `$2`, etc. | Individual arguments by position |
| `$@`, `$ARGUMENTS` | All arguments joined with spaces (zero or more) |
| `$+` | All arguments joined with spaces (one or more required) |
| `${@:N}` | Arguments from position N onwards |
| `${@:N:L}` | L arguments starting at position N |

Placeholders inside fenced code blocks (`` ``` ``) and inline code spans are ignored, so documentation examples won't be substituted.

### CLI flags

```bash
# Load a specific template by name
kit --prompt-template review

# Disable template loading
kit --no-prompt-templates
```

## ACP server

Run Kit as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server. ACP-compatible clients communicate with Kit over JSON-RPC 2.0 on stdin/stdout.

```bash
kit acp                      # Start as ACP agent
kit acp --debug              # With debug logging to stderr
```

---

## Global Flags

# Global Flags

All flags can be passed to the root `kit` command.

## Model and provider

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--model` | `-m` | `anthropic/claude-sonnet-latest` | Model to use (provider/model format) |
| `--provider-api-key` | — | — | API key for the provider |
| `--provider-url` | — | — | Base URL for provider API |
| `--tls-skip-verify` | — | `false` | Skip TLS certificate verification |

## Session management

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--session` | `-s` | — | Open specific JSONL session file |
| `--continue` | `-c` | `false` | Resume most recent session for current directory |
| `--resume` | `-r` | `false` | Interactive session picker |
| `--no-session` | — | `false` | Ephemeral mode, no persistence |

## Behavior

These flags control Kit's behavior. When a prompt is passed as a positional argument, Kit runs in non-interactive mode.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--quiet` | — | `false` | Suppress all output (non-interactive only) |
| `--json` | — | `false` | Output response as JSON (non-interactive only) |
| `--no-exit` | — | `false` | Enter interactive mode after prompt completes |
| `--max-steps` | — | `0` | Maximum agent steps (0 for unlimited) |
| `--stream` | — | `true` | Enable streaming output |
| `--compact` | — | `false` | Enable compact output mode |
| `--auto-compact` | — | `false` | Auto-compact conversation near context limit |

## Extensions

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--extension` | `-e` | — | Load additional extension file(s) (repeatable) |
| `--no-extensions` | — | `false` | Disable all extensions |
| `--prompt-template` | — | — | Load a specific prompt template by name |
| `--no-prompt-templates` | — | `false` | Disable prompt template loading |

## Skills

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--skill` | — | — | Load skill file or directory (repeatable) |
| `--skills-dir` | — | — | Override the project-local skills directory for auto-discovery |
| `--no-skills` | — | `false` | Disable skill loading (auto-discovery and explicit) |

## Generation parameters

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--max-tokens` | — | `8192` | Base cap for output tokens. Auto-raised per-model up to 32768 when the model's catalog ceiling is higher and no explicit value is set. |
| `--temperature` | — | `0.7` | Randomness 0.0–1.0 |
| `--top-p` | — | `0.95` | Nucleus sampling 0.0–1.0 |
| `--top-k` | — | `40` | Limit top K tokens |
| `--stop-sequences` | — | — | Custom stop sequences (comma-separated) |
| `--frequency-penalty` | — | `0.0` | Penalize frequent tokens (0.0–2.0) |
| `--presence-penalty` | — | `0.0` | Penalize present tokens (0.0–2.0) |
| `--thinking-level` | — | `off` | Extended thinking level: off, none, minimal, low, medium, high |

## System

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--config` | — | `~/.kit.yml` | Config file path |
| `--system-prompt` | — | — | System prompt text or file path |
| `--debug` | — | `false` | Enable debug logging |

---

## Configuration

# Configuration

Kit looks for configuration in the following locations, in order of priority:

1. CLI flags
2. Environment variables (with `KIT_` prefix)
3. `./.kit.yml` / `./.kit.yaml` / `./.kit.json` (project-local)
4. `~/.kit.yml` / `~/.kit.yaml` / `~/.kit.json` (global)

## Basic configuration

Create `~/.kit.yml`:

```yaml
model: anthropic/claude-sonnet-latest
max-tokens: 8192
temperature: 0.7
stream: true
```

## All configuration keys

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `model` | string | `anthropic/claude-sonnet-latest` | Model to use (provider/model format) |
| `max-tokens` | int | `8192` | Base cap for output tokens. Auto-raised per-model up to 32768 when the model's catalog ceiling is higher and no explicit value is set. Use [`modelSettings[provider/model].maxTokens`](#per-model-settings) to override per-model. |
| `temperature` | float | `0.7` | Randomness 0.0–1.0 |
| `top-p` | float | `0.95` | Nucleus sampling 0.0–1.0 |
| `top-k` | int | `40` | Limit top K tokens |
| `stream` | bool | `true` | Enable streaming output |
| `debug` | bool | `false` | Enable debug logging |
| `compact` | bool | `false` | Enable compact output mode |
| `system-prompt` | string | — | System prompt text or file path |
| `max-steps` | int | `0` | Maximum agent steps (0 = unlimited) |
| `thinking-level` | string | `off` | Extended thinking: off, none, minimal, low, medium, high |
| `provider-api-key` | string | — | API key for the provider |
| `provider-url` | string | — | Base URL for provider API |
| `tls-skip-verify` | bool | `false` | Skip TLS certificate verification |
| `frequency-penalty` | float | `0.0` | Penalize frequent tokens (0.0–2.0) |
| `presence-penalty` | float | `0.0` | Penalize present tokens (0.0–2.0) |
| `stop-sequences` | list | — | Custom stop sequences |
| `theme` | object or string | — | UI theme ([inline overrides or file path](/themes)) |
| `prompt-templates` | bool | `true` | Enable prompt template loading |
| `prompt-template` | string | — | Specific template to load by name |
| `no-skills` | bool | `false` | Disable skill loading (auto-discovery and explicit) |
| `skill` | list | — | Explicit skill files or directories to load (disables auto-discovery) |
| `skills-dir` | string | — | Override the project-local directory used for skill auto-discovery |

## Environment variables

Any configuration key can be set via environment variable with the `KIT_` prefix. Hyphens become underscores:

```bash
export KIT_MODEL="openai/gpt-4o"
export KIT_MAX_TOKENS="8192"
export KIT_TEMPERATURE="0.5"
```

Provider API keys use their own environment variables:

```bash
export ANTHROPIC_API_KEY="sk-..."
export OPENAI_API_KEY="sk-..."
export GOOGLE_API_KEY="..."
```

## MCP server configuration

Add external MCP servers to your `.kit.yml`:

```yaml
mcpServers:
  filesystem:
    type: local
    command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed"]
    environment:
      LOG_LEVEL: "info"
    allowedTools: ["read_file", "write_file"]
    excludedTools: ["delete_file"]

  search:
    type: remote
    url: "https://mcp.example.com/search"

  pubmed:
    type: remote
    url: "https://pubmed.mcp.example.com"
    noOAuth: true  # skip OAuth for public servers
    headers:
      - "ApiKey: ${env://API_KEY}"              # required env var
      - "X-Tenant: ${env://TENANT_ID:-default}" # with fallback default

  builds:
    type: remote
    url: "https://builds.mcp.example.com"
    tasksMode: always  # always run tools/call as async tasks (Phase 1 MVP)
```

### MCP server fields

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | `local` (stdio) or `remote` (streamable HTTP) |
| `command` | list | Command and args for local servers |
| `environment` | map | Environment variables for the server process |
| `url` | string | URL for remote servers |
| `allowedTools` | list | Whitelist of tool names to expose |
| `excludedTools` | list | Blacklist of tool names to hide |
| `noOAuth` | bool | Skip OAuth for this server (for public servers that don't require auth) |
| `headers` | list of strings | HTTP headers to attach to every request, each as a `"Key: Value"` string. Values support env-substitution: `${env://VAR}` or `${env://VAR:-default}`. |
| `tasksMode` | string | When to augment `tools/call` with MCP task metadata: `auto` (default — only when the server advertises task support), `never`, or `always`. See [MCP tasks](#mcp-tasks-long-running-tools). |

A legacy format with `transport`, `args`, and `env` fields is also supported; `headers` works in both the current and legacy formats.

### MCP tasks (long-running tools)

Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)
during `initialize` so servers can respond to `tools/call` with a
`CreateTaskResult` (a task ID + `working` status) instead of blocking until
the operation finishes. Kit then polls `tasks/get` / `tasks/result` until the
task reaches a terminal state, and best-effort `tasks/cancel`s on context
cancellation.

This avoids HTTP/SSE proxy timeouts on long builds, deploys, and batch jobs,
and lets the user/agent abort cleanly with Ctrl-C.

**Per-server `tasksMode`:**

| Value | Behaviour |
|-------|-----------|
| `auto` (default) | Augment `tools/call` with task metadata only when the server advertised `tasks/toolCalls` capability. Servers that don't advertise it run synchronously, exactly as before. |
| `never` | Always issue `tools/call` synchronously, regardless of server capability. |
| `always` | Always opt into task augmentation, even when the server didn't advertise the capability. The server may still respond synchronously — this just expresses client intent unconditionally. |

Defaults are safe: any existing MCP server keeps its previous behaviour
bit-for-bit. SDK consumers can also override the mode programmatically and
plug in a progress callback — see [SDK options](/sdk/options#mcp-tasks).

## Custom models

Define custom models in your `.kit.yml` for use with the `custom` provider. This is useful for self-hosted models or API endpoints not in the built-in database:

```yaml
customModels:
  my-model:
    name: "My Custom Model"
    baseUrl: "http://localhost:8080/v1"
    apiKey: "my-secret-key"
    reasoning: true
    temperature: true
    cost:
      input: 0.002
      output: 0.004
    limit:
      context: 128000
      output: 32000
```

### Custom model fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Display name for the model |
| `baseUrl` | string | No | Per-model base URL override; when set, `--provider-url` is not required |
| `apiKey` | string | No | Per-model API key override |
| `reasoning` | bool | No | Whether the model supports reasoning/thinking |
| `temperature` | bool | No | Whether the model supports temperature adjustment |
| `cost.input` | float | No | Cost per 1K input tokens |
| `cost.output` | float | No | Cost per 1K output tokens |
| `limit.context` | int | Yes | Maximum context window in tokens |
| `limit.output` | int | No | Maximum output tokens |

Use with a per-model `baseUrl` (no `--provider-url` needed):

```bash
kit --model custom/my-model "Hello"
```

Or override the base URL at runtime:

```bash
kit --provider-url "http://localhost:8080/v1" --model custom/my-model "Hello"
```

When `--provider-url` is specified without `--model`, Kit defaults to `custom/custom` which has zero cost tracking and a 262K context window.

## Per-model settings

Override generation parameters and system prompt on a per-model basis using `modelSettings`:

```yaml
modelSettings:
  anthropic/claude-sonnet-4-5-20250929:
    temperature: 0.3
    maxTokens: 8192
    systemPrompt: "You are a concise coding assistant."
  openai/gpt-4o:
    temperature: 0.7
    frequencyPenalty: 0.5
```

### Per-model fields

| Field | Type | Description |
|-------|------|-------------|
| `temperature` | float | Temperature override for this model |
| `maxTokens` | int | Max output tokens override |
| `topP` | float | Top-p override |
| `topK` | int | Top-k override |
| `frequencyPenalty` | float | Frequency penalty override |
| `presencePenalty` | float | Presence penalty override |
| `stopSequences` | list | Stop sequences override |
| `thinkingLevel` | string | Thinking level override |
| `systemPrompt` | string | Per-model system prompt (used when no explicit prompt is set) |

Settings from `modelSettings` and `customModels.params` act as model-level defaults — explicit CLI flags, `KIT_*` environment variables, global config values, and SDK `Options.*` fields all take precedence over them.

When switching models via `/model` or `SetModel()`, if the new model has a per-model system prompt and no custom global prompt was set, the per-model prompt automatically replaces the previous one.

### Precedence summary

For the generation and provider parameters documented above, the resolved value at runtime comes from the first source that sets it:

1. CLI flag (e.g. `--max-tokens`, `--temperature`, `--provider-api-key`)
2. SDK `Options.X` when embedding Kit as a library (`kit.Options.MaxTokens`, `Temperature`, `ProviderAPIKey`, etc.)
3. `KIT_*` environment variable (`KIT_MAX_TOKENS`, `KIT_TEMPERATURE`, ...)
4. `.kit.yml` / `.kit.yaml` / `.kit.json` (project-local, then global)
5. Per-model defaults (`modelSettings[provider/model]` / `customModels[...].params`)
6. Provider-level defaults (e.g. Anthropic's own temperature default)
7. SDK last-resort floor — currently an 8192 output-token ceiling matching the CLI `--max-tokens` default, auto-raised per-model up to 32768 when the model's catalog ceiling is higher

See the [SDK options reference](/sdk/options) for the full list of `kit.Options` fields that map to these keys.

## Theme configuration

```yaml
# Inline partial overrides (unspecified fields inherit from default)
theme:
  primary:
    light: "#8839ef"
    dark: "#cba6f7"
  error:
    dark: "#FF0000"
```

```yaml
# Reference external theme file
theme: "./themes/my-custom-theme.yml"
```

See [Themes](/themes) for the full theme file format, built-in themes, and the extension theme API.

## Preferences persistence

Kit automatically saves your UI preferences across sessions to `~/.config/kit/preferences.yml`:

- **Theme** — Set via `/theme <name>` or `ctx.SetTheme()`
- **Model** — Set via `/model <name>` or the model selector
- **Thinking level** — Set via `/thinking <level>` or Shift+Tab cycling

These preferences are restored on next launch. Precedence (highest to lowest):
1. CLI flags (`--model`, `--thinking-level`)
2. Config file (`model:`, `thinking-level:`)
3. Saved preferences (`~/.config/kit/preferences.yml`)
4. Default values

---

## Development

# Development

## Build and test

```bash
# Build
go build -o output/kit ./cmd/kit

# Run all tests
go test -race ./...

# Run a specific test
go test -race ./cmd -run TestScriptExecution

# Lint
go vet ./...

# Format
go fmt ./...
```

## Project structure

```
cmd/kit/             - CLI entry point (main.go)
cmd/                 - CLI command implementations (root, auth, models, etc.)
pkg/kit/             - Go SDK for embedding Kit
internal/app/        - Application orchestrator (agent loop, message store, queue)
internal/agent/      - Agent execution and tool dispatch
internal/auth/       - OAuth authentication and credential storage
internal/acpserver/  - ACP (Agent Client Protocol) server
internal/clipboard/  - Cross-platform clipboard operations
internal/compaction/ - Conversation compaction and summarization
internal/config/     - Configuration management
internal/core/       - Built-in tools (bash with sudo password prompt, read, write, edit, grep, find, ls)
internal/extensions/ - Yaegi extension system
internal/kitsetup/   - Initial setup wizard
internal/message/    - Message content types and structured content blocks
internal/models/     - Provider and model management
internal/session/    - Session persistence (tree-based JSONL)
internal/skills/     - Skill loading and system prompt composition
internal/tools/      - MCP tool integration
internal/ui/         - Bubble Tea TUI components
examples/extensions/ - Example extension files
npm/                 - NPM package wrapper for distribution
```

## Architecture overview

Kit is built around a few key architectural patterns:

### Multi-provider LLM support

The `llm.Provider` interface abstracts different LLM providers. Each provider implements message formatting, tool calling, and streaming for its specific API.

### MCP client-server model

External tools are integrated via the Model Context Protocol (MCP). Kit acts as an MCP client, connecting to MCP servers configured in `.kit.yml`.

### Extension system

Extensions are Go source files interpreted at runtime by Yaegi. The `internal/extensions/` package manages loading, symbol export, and lifecycle dispatch. See the [Extension System](/extensions/overview) docs for details.

### TUI architecture

The interactive terminal UI is built with [Bubble Tea v2](https://github.com/charmbracelet/bubbletea), using a parent-child model where `AppModel` manages child components (`InputComponent`, `StreamComponent`, etc.).

### Decoupling pattern

`cmd/root.go` contains converter functions (e.g., `widgetProviderForUI()`) that bridge `internal/extensions/` types to `internal/ui/` types. The UI never imports the extensions package directly.

## Contributing

Contributions are welcome! Please see the [contribution guide](https://github.com/mark3labs/kit/blob/master/contribute/contribute.md) for guidelines.

## Community

- [Discord](https://discord.gg/RqSS2NQVsY)
- [GitHub Issues](https://github.com/mark3labs/kit/issues)

---

## Capabilities

# Extension Capabilities

## Lifecycle events

Extensions can hook into 27 lifecycle events:

| Event | Description |
|-------|-------------|
| `OnSessionStart` | Session initialized |
| `OnSessionShutdown` | Session ending |
| `OnBeforeAgentStart` | Before the agent loop begins |
| `OnAgentStart` | Agent loop started |
| `OnAgentEnd` | Agent loop completed (carries per-turn aggregates: tool counts, token deltas, cost, duration) |
| `OnLLMUsage` | Per-LLM-call token + cost delta (fires once per provider round-trip) |
| `OnToolCall` | Tool call requested by the model |
| `OnToolCallInputStart` | LLM began generating tool call arguments (tool name known, args streaming) |
| `OnToolCallInputDelta` | Streamed JSON fragment of tool call arguments |
| `OnToolCallInputEnd` | Tool argument streaming complete, before execution begins |
| `OnToolExecutionStart` | Tool execution beginning |
| `OnToolOutput` | Streaming tool output chunk (for long-running tools) |
| `OnToolExecutionEnd` | Tool execution completed |
| `OnToolResult` | Tool result returned |
| `OnInput` | User input received |
| `OnMessageStart` | Assistant message started |
| `OnMessageUpdate` | Streaming text chunk received |
| `OnMessageEnd` | Assistant message completed |
| `OnModelChange` | Model switched |
| `OnContextPrepare` | Context being assembled for the model |
| `OnBeforeFork` | Before forking a conversation branch |
| `OnBeforeSessionSwitch` | Before switching sessions |
| `OnBeforeCompact` | Before conversation compaction |
| `OnCustomEvent` | Custom inter-extension event received |
| `OnSubagentStart` | Subagent spawned by the main agent |
| `OnSubagentChunk` | Real-time output from subagent (text, tool calls, results) |
| `OnSubagentEnd` | Subagent completed with final response/error |

### Example

```go
api.OnToolCall(func(event ext.ToolCallEvent, ctx ext.Context) {
    ctx.PrintInfo("Calling tool: " + event.Name)
})

api.OnAgentEnd(func(e ext.AgentEndEvent, ctx ext.Context) {
    // Per-turn aggregates populated by Kit's runtime — no parallel
    // bookkeeping required in the handler.
    ctx.PrintInfo(fmt.Sprintf(
        "Turn finished: %d tool calls (%v), %d LLM round-trips, $%.4f, %dms",
        e.ToolCallCount, e.ToolNames, e.LLMCallCount, e.CostDelta, e.DurationMs,
    ))
})

// Per-LLM-call usage — fires multiple times per turn (once per round-trip).
// Use for accurate budget enforcement between calls.
api.OnLLMUsage(func(e ext.LLMUsageEvent, ctx ext.Context) {
    ctx.PrintInfo(fmt.Sprintf(
        "%s/%s step=%d tokens=↑%d ↓%d cost=$%.4f (%s)",
        e.Provider, e.Model, e.StepNumber,
        e.InputTokens, e.OutputTokens, e.Cost, e.FinishReason,
    ))
})
```

**`AgentEndEvent` fields** (in addition to `Response` and `StopReason`):

| Field | Type | Description |
|-------|------|-------------|
| `ToolCallCount` | `int` | Total tool invocations during the turn |
| `ToolNames` | `[]string` | Tool names in call order (duplicates preserved) |
| `LLMCallCount` | `int` | LLM round-trips / tool-loop iterations |
| `InputTokensDelta` | `int` | Sum of input tokens across all LLM calls this turn |
| `OutputTokensDelta` | `int` | Sum of output tokens across all LLM calls this turn |
| `CacheReadTokensDelta` | `int` | Sum of cache-read tokens this turn |
| `CacheWriteTokensDelta` | `int` | Sum of cache-write tokens this turn |
| `CostDelta` | `float64` | Cost in USD (zero when pricing is unknown or OAuth credentials) |
| `DurationMs` | `int64` | Wall-clock time from `AgentStart` to `AgentEnd` |

**`LLMUsageEvent` fields**:

| Field | Type | Description |
|-------|------|-------------|
| `InputTokens` / `OutputTokens` | `int` | Per-call token deltas |
| `CacheReadTokens` / `CacheWriteTokens` | `int` | Per-call cache token deltas |
| `Cost` | `float64` | Per-call USD cost (zero when pricing unknown) |
| `Model` / `Provider` | `string` | Model used for this specific call — may differ from earlier calls if `ctx.SetModel` was called mid-turn |
| `StepNumber` | `int` | Zero-based step index within the turn |
| `FinishReason` | `string` | Provider finish reason for this call (`"stop"`, `"tool_calls"`, `"length"`, ...) |
| `RequestID` | `string` | Optional provider correlation id (may be empty) |

## Tools

Register custom tools that the LLM can invoke:

```go
api.RegisterTool(ext.ToolDef{
    Name:        "weather",
    Description: "Get current weather for a location",
    Parameters: map[string]ext.ParameterDef{
        "city": {Type: "string", Description: "City name", Required: true},
    },
    Handler: func(ctx ext.Context, params map[string]any) (string, error) {
        city := params["city"].(string)
        return "Sunny, 72°F in " + city, nil
    },
})
```

## Commands

Register slash commands that users can invoke directly:

```go
api.RegisterCommand(ext.CommandDef{
    Name:        "stats",
    Description: "Show context statistics",
    Handler: func(ctx ext.Context, args string) {
        stats := ctx.GetContextStats()
        ctx.PrintInfo(fmt.Sprintf("Tokens: %d", stats.TotalTokens))
    },
})
```

## Widgets

Add persistent status displays above or below the input area:

```go
ctx.SetWidget(ext.WidgetConfig{
    ID:       "token-count",
    Position: "bottom",
    Content:  ext.WidgetContent{Text: "Tokens: 1,234"},
})

// Update later
ctx.SetWidget(ext.WidgetConfig{
    ID:       "token-count",
    Position: "bottom",
    Content:  ext.WidgetContent{Text: "Tokens: 2,456"},
})

// Remove
ctx.RemoveWidget("token-count")
```

## Headers and footers

Persistent content above and below the conversation:

```go
ctx.SetHeader(ext.HeaderFooterConfig{
    Content: ext.WidgetContent{Text: "Project: my-app | Branch: main"},
})

ctx.SetFooter(ext.HeaderFooterConfig{
    Content: ext.WidgetContent{Text: "Plan Mode (read-only)"},
})
```

## Status bar

Custom status bar entries:

```go
ctx.SetStatus("mode", "Planning")
ctx.RemoveStatus("mode")
```

## Shortcuts

Global keyboard shortcuts:

```go
api.RegisterShortcut(ext.ShortcutDef{
    Key:         "ctrl+t",
    Description: "Toggle plan mode",
}, func(ctx ext.Context) {
    // handle shortcut
})
```

## Overlays

Modal dialogs with markdown content:

```go
ctx.ShowOverlay(ext.OverlayConfig{
    Title:   "Help",
    Content: "# Keyboard Shortcuts\n\n- **ctrl+t** — Toggle plan mode\n- **ctrl+s** — Save session",
})
```

## Tool renderers

Customize how specific tool calls are displayed in the TUI:

```go
api.RegisterToolRenderer(ext.ToolRenderConfig{
    ToolName: "bash",
    Render: func(name, args, result string, isError bool) string {
        return "$ " + args + "\n" + result
    },
})
```

## Message renderers

Custom rendering for assistant messages:

```go
api.RegisterMessageRenderer(ext.MessageRendererConfig{
    Name: "custom",
    Render: func(content string) string {
        return ">> " + content
    },
})
```

## Editor interceptors

Handle key events and wrap the editor's rendering:

```go
ctx.SetEditor(ext.EditorConfig{
    HandleKey: func(key, text string) ext.EditorKeyAction {
        if key == "escape" {
            return ext.EditorKeyAction{Handled: true}
        }
        return ext.EditorKeyAction{Handled: false}
    },
})
```

## Interactive prompts

Select, confirm, input, and multi-select dialogs:

```go
// Single select
response := ctx.PromptSelect(ext.PromptSelectConfig{
    Title:   "Choose a model",
    Options: []string{"claude-sonnet", "gpt-4o", "llama3"},
})

// Confirm
confirmed := ctx.PromptConfirm(ext.PromptConfirmConfig{
    Title: "Delete this file?",
})

// Text input
name := ctx.PromptInput(ext.PromptInputConfig{
    Title:       "Enter project name",
    Placeholder: "my-project",
})
```

## Options

Register configurable extension options:

```go
api.RegisterOption(ext.OptionDef{
    Name:         "auto-commit",
    Description:  "Automatically commit on shutdown",
    DefaultValue: "false",
})
```

## Subagents

Spawn in-process child Kit instances:

```go
result := ctx.SpawnSubagent(ext.SubagentConfig{
    Task:         "Analyze the test files and summarize coverage",
    Model:        "anthropic/claude-haiku-latest",
    SystemPrompt: "You are a test analysis expert.",
})
```

### Monitoring subagents spawned by the main agent

When the LLM uses the built-in `subagent` tool, extensions can monitor the subagent's activity in real-time using three lifecycle events:

```go
// Subagent started
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
    ctx.PrintInfo(fmt.Sprintf("Subagent started: %s", e.Task))
})

// Real-time streaming output from subagent
api.OnSubagentChunk(func(e ext.SubagentChunkEvent, ctx ext.Context) {
    // e.ToolCallID — matches the start event
    // e.Task — task description
    // e.ChunkType — "text", "tool_call", "tool_execution_start", "tool_result"
    // e.Content — text content (for text chunks)
    // e.ToolName — tool name (for tool-related chunks)
    // e.IsError — true if tool result is an error
    switch e.ChunkType {
    case "text":
        // Streaming text output
    case "tool_call":
        // Subagent is calling a tool
    case "tool_execution_start":
        // Tool execution started
    case "tool_result":
        // Tool execution completed (check e.IsError)
    }
})

// Subagent completed
api.OnSubagentEnd(func(e ext.SubagentEndEvent, ctx ext.Context) {
    // e.ToolCallID — matches start event
    // e.Task — task description
    // e.Response — final response from subagent
    // e.ErrorMsg — error message if subagent failed
    if e.ErrorMsg != "" {
        ctx.PrintError(fmt.Sprintf("Subagent failed: %s", e.ErrorMsg))
    } else {
        ctx.PrintInfo(fmt.Sprintf("Subagent completed: %s", e.Response))
    }
})
```

This enables building widgets that display real-time subagent activity.

## LLM completion

Make direct model calls without going through the agent loop:

```go
response := ctx.Complete(ext.CompleteRequest{
    Prompt: "Summarize this in one sentence: " + content,
})
```

## Themes

Register and switch color themes at runtime:

```go
// Register a custom theme
ctx.RegisterTheme("neon", ext.ThemeColorConfig{
    Primary:    ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
    Secondary:  ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
    Success:    ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
    Warning:    ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
    Error:      ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
    Info:       ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
    Text:       ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
    Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
})

// Switch to it
ctx.SetTheme("neon")

// List all available themes
names := ctx.ListThemes()
```

See [Themes](/themes) for the full theme file format, built-in themes, and color reference.

## Custom events

Inter-extension communication:

```go
// Emit
ctx.EmitCustomEvent("my-extension:data-ready", payload)

// Listen
api.OnCustomEvent("my-extension:data-ready", func(data any, ctx ext.Context) {
    // handle event
})
```

## Session state

Last-write-wins key-value store, scoped to the current session and persisted to a sidecar file (`<session>.ext-state.json`) outside the conversation tree:

```go
ctx.SetState("myext:budget-cap", "10.00")

if cap, ok := ctx.GetState("myext:budget-cap"); ok {
    // ...
}

ctx.DeleteState("myext:budget-cap")
keys := ctx.ListState()  // []string, unspecified order
```

Reads are O(1) (no branch walk), writes don't grow the session JSONL, and the store is not duplicated when the conversation forks. State is invisible to the LLM and survives session resume.

### When to use which persistence primitive

| Need | Use | Why |
|------|-----|-----|
| Snapshot state ("current value of X") | `SetState` / `GetState` | O(1) reads, sidecar file, last-write-wins |
| Audit log / event history | `AppendEntry` / `GetEntries` | Append-only, lives in conversation tree, fork-aware |
| One-shot per-turn signal | Enriched `AgentEndEvent` fields | No persistence needed; runtime tracks it for you |
| Per-LLM-call observation | `OnLLMUsage` event | Already attributed to model/provider/step |

Using `AppendEntry` for snapshot state has a cost: it's O(branch_length) to read, fsyncs into the JSONL on every write, and the entry list duplicates on every fork. Prefer `SetState` for "what's the current value of X?"-style data.

For ephemeral / in-memory sessions (no JSONL path) the state lives only in memory for the lifetime of the runner.

## Bridged SDK APIs

Extensions can access powerful internal SDK capabilities that enable advanced features like conversation tree navigation, dynamic skill loading, template parsing, and model resolution.

### Tree Navigation

Navigate the conversation tree, summarize branches, and implement "fresh context" loops:

```go
// Get a specific node by ID with full metadata and children
node := ctx.GetTreeNode("entry-id")
// node.ID, node.ParentID, node.Type ("message"/"branch_summary"/etc)
// node.Role, node.Content, node.Model, node.Children ([]string)

// Get the current branch from root to leaf
branch := ctx.GetCurrentBranch()  // []ext.TreeNode

// Get child entry IDs of a node
children := ctx.GetChildren("entry-id")  // []string

// Navigate/fork to a different entry in the tree
result := ctx.NavigateTo("entry-id")  // ext.TreeNavigationResult{Success, Error}

// Summarize a range of the branch using LLM
summary := ctx.SummarizeBranch("from-id", "to-id")  // string

// Collapse a branch range into a summary entry (fresh context primitive)
result := ctx.CollapseBranch("from-id", "to-id", "summary text")
```

### Skill Loading

Load and inject skills dynamically at runtime:

```go
// Discover skills from standard locations
result := ctx.DiscoverSkills()  // ext.SkillLoadResult{Skills, Error}
// Standard locations: ~/.config/kit/skills/, .kit/skills/, .agents/skills/

// Load a specific skill file
skill, err := ctx.LoadSkill("/path/to/skill.md")  // (*ext.Skill, error string)
// skill.Name, skill.Description, skill.Content, skill.Tags, skill.When

// Load all skills from a directory
result := ctx.LoadSkillsFromDir("/path/to/skills")  // ext.SkillLoadResult

// Inject a skill as context (pre-loads for next turn)
err := ctx.InjectSkillAsContext("skill-name")  // error string

// Inject a skill file directly
err := ctx.InjectRawSkillAsContext("/path/to/skill.md")  // error string

// Get all discovered skills
skills := ctx.GetAvailableSkills()  // []ext.Skill
```

### Template Parsing

Parse and render templates with variable substitution:

```go
// Parse a template to extract {{variables}}
tpl := ctx.ParseTemplate("name", "Hello {{name}}, welcome to {{place}}!")
// tpl.Name, tpl.Content, tpl.Variables ([]string)

// Render a template with variable values
vars := map[string]string{"name": "Alice", "place": "Kit"}
rendered := ctx.RenderTemplate(tpl, vars)  // "Hello Alice, welcome to Kit!"

// Parse command-line style arguments
pattern := ext.ArgumentPattern{
    Positional: []string{"command", "target"},  // $1, $2
    Rest:       "args",                         // $@
    Flags:      map[string]string{"--loop": "loop", "-f": "force"},
}
result := ctx.ParseArguments("deploy staging --loop 5", pattern)
// result.Vars["command"] = "deploy"
// result.Vars["target"] = "staging"
// result.Flags["--loop"] = "5"

// Simple positional argument parsing ($1, $2, $@)
args := ctx.SimpleParseArguments("deploy staging --force", 2)
// args[0] = "deploy staging --force" (full input)
// args[1] = "deploy" ($1)
// args[2] = "staging" ($2)
// args[3] = "--force" ($@)

// Evaluate model conditionals with wildcards
matches := ctx.EvaluateModelConditional("claude-*")  // bool
// Patterns: * matches any, ? matches single char, comma = OR

// Render content with <if-model> conditionals
content := `<if-model is="claude-*">Hi Claude<else>Hi there</if-model>`
rendered := ctx.RenderWithModelConditionals(content)  // based on current model
```

### Model Resolution

Resolve model fallback chains and query capabilities:

```go
// Resolve a chain of model preferences (tries each until available)
result := ctx.ResolveModelChain([]string{
    "anthropic/claude-opus-4",
    "anthropic/claude-sonnet-4",
    "openai/gpt-4o",
})
// result.Model (selected), result.Capabilities, result.Attempted, result.Error

// Get capabilities for a specific model
caps, err := ctx.GetModelCapabilities("anthropic/claude-sonnet-4")
// caps.Provider, caps.ModelID, caps.ContextLimit, caps.Reasoning, caps.Streaming

// Check if a model is available (provider exists)
available := ctx.CheckModelAvailable("anthropic/claude-sonnet-4")  // bool

// Get current provider/model ID
provider := ctx.GetCurrentProvider()  // "anthropic"
modelID := ctx.GetCurrentModelID()    // "claude-sonnet-4"
```

---

## Examples

# Extension Examples

Kit ships with a rich set of example extensions in the `examples/extensions/` directory. These serve as both documentation and starting points for your own extensions.

## UI and display

| Extension | Description |
|-----------|-------------|
| [`minimal.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/minimal.go) | Clean UI with custom footer |
| [`branded-output.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/branded-output.go) | Branded output rendering |
| [`header-footer-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/header-footer-demo.go) | Custom headers and footers |
| [`widget-status.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/widget-status.go) | Persistent status widgets |
| [`overlay-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/overlay-demo.go) | Modal dialogs |
| [`tool-renderer-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-renderer-demo.go) | Custom tool call rendering |
| [`custom-editor-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/custom-editor-demo.go) | Vim-like modal editor |
| [`pirate.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/pirate.go) | Pirate-themed personality |

## Workflow and automation

| Extension | Description |
|-----------|-------------|
| [`auto-commit.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/auto-commit.go) | Auto-commit changes on shutdown |
| [`plan-mode.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/plan-mode.go) | Read-only planning mode |
| [`permission-gate.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/permission-gate.go) | Permission gating for destructive tools |
| [`confirm-destructive.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/confirm-destructive.go) | Confirm destructive operations |
| [`protected-paths.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/protected-paths.go) | Path protection for sensitive files |
| [`project-rules.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/project-rules.go) | Project-specific rules injection |
| [`compact-notify.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/compact-notify.go) | Notification on conversation compaction |

## Interactive features

| Extension | Description |
|-----------|-------------|
| [`prompt-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/prompt-demo.go) | Interactive prompts (select/confirm/input) |
| [`bookmark.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/bookmark.go) | Bookmark conversations |
| [`inline-bash.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/inline-bash.go) | Inline bash execution |
| [`interactive-shell.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/interactive-shell.go) | Interactive shell integration |
| [`notify.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/notify.go) | Desktop notifications |

## Agent and context

| Extension | Description |
|-----------|-------------|
| [`tool-logger.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-logger.go) | Log all tool calls |
| [`context-inject.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/context-inject.go) | Inject context into conversations |
| [`summarize.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/summarize.go) | Conversation summarization |
| [`lsp-diagnostics.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/lsp-diagnostics.go) | LSP diagnostic integration |
| [`usage-budget.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/usage-budget.go) | Per-call usage callback (`OnLLMUsage`), session state (`SetState`/`GetState`), and enriched `OnAgentEnd` per-turn report |

## Bridged SDK APIs

These examples demonstrate the new bridged SDK APIs that give extensions access to internal Kit capabilities:

| Extension | Description |
|-----------|-------------|
| [`bridge-demo.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/bridge_demo.go) | Comprehensive demo of all bridged APIs — tree navigation, skill loading, template parsing, and model resolution |
| [`conversation-manager.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/conversation-manager.go) | Tree navigation (`GetTreeNode`, `GetCurrentBranch`, `NavigateTo`), branch summarization (`SummarizeBranch`), and fresh context loops (`CollapseBranch`) |
| [`prompt-templates.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/prompt-templates.go) | Frontmatter-driven templates with model fallback chains (`ResolveModelChain`), skill injection (`InjectSkillAsContext`), and template parsing (`ParseTemplate`, `RenderTemplate`) |

## Themes

| Extension | Description |
|-----------|-------------|
| [`neon-theme.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/neon-theme.go) | Custom theme registration and switching |

## Multi-agent

| Extension | Description |
|-----------|-------------|
| [`kit-kit.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/kit-kit.go) | Kit-in-Kit sub-agent spawning |
| [`subagent-widget.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/subagent-widget.go) | Multi-agent orchestration with status widget |
| [`subagent-test.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/subagent-test.go) | Subagent testing utilities |

## Development

| Extension | Description |
|-----------|-------------|
| [`dev-reload.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/dev-reload.go) | Development live-reload |
| [`tool-logger_test.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/tool-logger_test.go) | Example extension tests (see [Testing](/extensions/testing)) |
| [`extension_test_template.go`](https://github.com/mark3labs/kit/blob/master/examples/extensions/extension_test_template.go) | Copy-and-paste test template for your extensions |

## Subdirectory extensions

| Directory | Description |
|-----------|-------------|
| [`kit-kit-agents/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/kit-kit-agents) | Multi-agent orchestration example |
| [`kit-telegram/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/kit-telegram) | Telegram bot integration |
| [`status-tools/`](https://github.com/mark3labs/kit/tree/master/examples/extensions/status-tools) | Status bar tool examples |

## Project-local example

The Kit repository also includes a project-local extension at `.kit/extensions/go-edit-lint.go` that demonstrates running `gopls` and `golangci-lint` on Go file edits. This serves as an example of how to create extensions specific to a project by placing them in the `.kit/extensions/` directory.

---

## Loading Extensions

# Loading Extensions

## Auto-discovery

Kit automatically discovers and loads extensions from these paths, in order:

| Path | Scope |
|------|-------|
| `~/.config/kit/extensions/*.go` | Global single files |
| `~/.config/kit/extensions/*/main.go` | Global subdirectory extensions |
| `.kit/extensions/*.go` | Project-local single files |
| `.kit/extensions/*/main.go` | Project-local subdirectory extensions |
| `~/.local/share/kit/git/` | Global git-installed packages |
| `.kit/git/` | Project-local git-installed packages |

## Explicit loading

Load extensions by path using the `-e` flag:

```bash
kit -e path/to/extension.go
```

Load multiple extensions:

```bash
kit -e ext1.go -e ext2.go
```

## Disabling extensions

Disable all auto-discovered extensions:

```bash
kit --no-extensions
```

You can combine `--no-extensions` with `-e` to load only specific extensions:

```bash
kit --no-extensions -e my-extension.go
```

## Installing from git

Install extensions from git repositories using `kit install`:

```bash
# Install globally (to ~/.local/share/kit/git/)
kit install https://github.com/user/my-kit-extension.git

# Install project-locally (to .kit/git/)
kit install -l https://github.com/user/my-kit-extension.git

# Update an installed package
kit install -u https://github.com/user/my-kit-extension.git

# Remove
kit install --uninstall my-kit-extension
```

## Extension structure

### Single-file extensions

A single `.go` file with an `Init` function:

```go
//go:build ignore

package main

import "kit/ext"

func Init(api ext.API) {
    // register handlers, tools, commands, etc.
}
```

The `//go:build ignore` directive prevents the Go toolchain from trying to compile the file as part of a normal build.

### Subdirectory extensions

For more complex extensions, create a directory with a `main.go` entry point:

```
.kit/extensions/my-extension/
├── main.go      # Must contain Init(api ext.API)
├── helpers.go   # Additional source files
└── config.go
```

### Package-level state

Yaegi supports package-level variables captured in closures. This is the standard way to maintain state across event callbacks:

```go
package main

import "kit/ext"

var callCount int

func Init(api ext.API) {
    api.OnToolCall(func(_ ext.ToolCallEvent, ctx ext.Context) {
        callCount++
        ctx.SetFooter(ext.HeaderFooterConfig{
            Content: ext.WidgetContent{
                Text: fmt.Sprintf("Tools called: %d", callCount),
            },
        })
    })
}
```

---

## Extension System

# Extension System

Extensions are Go source files interpreted at runtime via [Yaegi](https://github.com/traefik/yaegi). They can add custom tools, slash commands, widgets, keyboard shortcuts, and intercept lifecycle events — all without recompiling Kit.

## Minimal extension

```go
//go:build ignore

package main

import "kit/ext"

func Init(api ext.API) {
    api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
        ctx.SetFooter(ext.HeaderFooterConfig{
            Content: ext.WidgetContent{Text: "Custom Footer"},
        })
    })
}
```

Run it with:

```bash
kit -e examples/extensions/minimal.go
```

## How extensions work

1. Kit discovers extension files from [auto-discovery paths](/extensions/loading) or explicit `-e` flags
2. Each `.go` file is loaded into a Yaegi interpreter with access to the `kit/ext` package
3. Kit calls the `Init(api ext.API)` function in each extension
4. The extension registers callbacks, tools, commands, and UI components via the `api` and `ctx` objects

## Key concepts

### The `API` object

Passed to `Init()`, the `API` object is used to register lifecycle event handlers and static components:

- **Lifecycle handlers** — `api.OnSessionStart(...)`, `api.OnToolCall(...)`, etc.
- **Tools** — `api.RegisterTool(ext.ToolDef{...})`
- **Commands** — `api.RegisterCommand(ext.CommandDef{...})`
- **Shortcuts** — `api.RegisterShortcut(ext.ShortcutDef{...}, handler)`
- **Tool renderers** — `api.RegisterToolRenderer(ext.ToolRenderConfig{...})`
- **Message renderers** — `api.RegisterMessageRenderer(ext.MessageRendererConfig{...})`
- **Options** — `api.RegisterOption(ext.OptionDef{...})`

### The `Context` object

Passed to event handlers, the `Context` object provides runtime access to Kit's state and UI:

- **Output** — `ctx.Print(...)`, `ctx.PrintInfo(...)`, `ctx.PrintError(...)`
- **UI components** — `ctx.SetWidget(...)`, `ctx.SetHeader(...)`, `ctx.SetFooter(...)`, `ctx.SetStatus(...)`
- **Editor** — `ctx.SetEditor(...)`, `ctx.ResetEditor()`
- **Prompts** — `ctx.PromptSelect(...)`, `ctx.PromptConfirm(...)`, `ctx.PromptInput(...)`
- **Overlays** — `ctx.ShowOverlay(...)`
- **Messages** — `ctx.SendMessage(...)`, `ctx.GetMessages()`
- **Model** — `ctx.SetModel(...)`, `ctx.GetAvailableModels()`
- **Tools** — `ctx.GetAllTools()`, `ctx.SetActiveTools(...)`
- **Context stats** — `ctx.GetContextStats()`
- **Session data** — `ctx.AppendEntry(...)`, `ctx.GetEntries(...)` (append-only, in conversation tree)
- **Session state** — `ctx.SetState(...)`, `ctx.GetState(...)`, `ctx.DeleteState(...)`, `ctx.ListState()` (last-write-wins, sidecar file)
- **Subagents** — `ctx.SpawnSubagent(...)`
- **LLM completion** — `ctx.Complete(...)`
- **Custom events** — `ctx.EmitCustomEvent(...)`

See [Capabilities](/extensions/capabilities) for full details on each component type, and [Testing](/extensions/testing) for writing tests for your extensions.

---

## Testing Extensions

# Testing Extensions

Kit provides a testing package (`github.com/mark3labs/kit/pkg/extensions/test`) that enables you to write unit tests for your extensions. Tests run outside the Yaegi interpreter but load your extension code into an isolated interpreter instance, allowing you to verify behavior without running the full Kit TUI.

## Overview

Extension tests allow you to:

- Test event handlers without running the interactive TUI
- Verify tool/command registration
- Assert that context methods (Print, SetWidget, etc.) are called correctly
- Test blocking and non-blocking event handling
- Simulate user input and tool calls
- Verify widget, header, footer, and status bar updates

## Installation

The test package is part of the Kit codebase. Import it in your extension tests:

```go
import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
    "github.com/mark3labs/kit/internal/extensions"
)
```

## Basic Usage

### Testing an Extension File

Create a test file alongside your extension (e.g., `my-ext_test.go`):

```go
package main

import (
    "testing"
    "github.com/mark3labs/kit/pkg/extensions/test"
    "github.com/mark3labs/kit/internal/extensions"
)

func TestMyExtension(t *testing.T) {
    // Create a test harness
    harness := test.New(t)
    
    // Load your extension
    harness.LoadFile("my-ext.go")
    
    // Emit events and check results
    result, err := harness.Emit(extensions.ToolCallEvent{
        ToolName: "my_tool",
        Input:    `{"key": "value"}`,
    })
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    // Use assertion helpers
    test.AssertNotBlocked(t, result)
    test.AssertPrinted(t, harness, "expected output")
}
```

### Testing Inline Extension Code

For quick tests or edge cases, you can load extension source directly:

```go
func TestToolBlocking(t *testing.T) {
    src := `package main

import "kit/ext"

func Init(api ext.API) {
    api.OnToolCall(func(tc ext.ToolCallEvent, ctx ext.Context) *ext.ToolCallResult {
        if tc.ToolName == "dangerous" {
            return &ext.ToolCallResult{Block: true, Reason: "not allowed"}
        }
        return nil
    })
}
`
    harness := test.New(t)
    harness.LoadString(src, "test-ext.go")
    
    // Test the tool is blocked
    result, _ := harness.Emit(extensions.ToolCallEvent{
        ToolName: "dangerous",
        Input:    "{}",
    })
    
    test.AssertBlocked(t, result, "not allowed")
}
```

## Common Testing Patterns

### Testing Handler Registration

Verify your extension registers the expected handlers:

```go
func TestHandlers(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    test.AssertHasHandlers(t, harness, extensions.ToolCall)
    test.AssertHasHandlers(t, harness, extensions.SessionStart)
    test.AssertNoHandlers(t, harness, extensions.AgentEnd) // Verify no unexpected handlers
}
```

### Testing Tool Registration

```go
func TestTools(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Verify a specific tool is registered
    test.AssertToolRegistered(t, harness, "my_tool")
    
    // Or inspect all tools
    tools := harness.RegisteredTools()
    for _, tool := range tools {
        t.Logf("Tool: %s - %s", tool.Name, tool.Description)
    }
}
```

### Testing Commands

```go
func TestCommands(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    test.AssertCommandRegistered(t, harness, "mycommand")
}
```

### Testing Widgets

```go
func TestWidgets(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Trigger event that creates the widget
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    // Verify widget was set
    test.AssertWidgetSet(t, harness, "my-widget")
    test.AssertWidgetText(t, harness, "my-widget", "Expected Text")
    test.AssertWidgetTextContains(t, harness, "my-widget", "partial")
    
    // Check widget properties directly
    widget, ok := harness.Context().GetWidget("my-widget")
    if ok {
        t.Logf("Border color: %s", widget.Style.BorderColor)
    }
}
```

### Testing Input Handling

```go
func TestInput(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    result, _ := harness.Emit(extensions.InputEvent{
        Text:   "!mycommand",
        Source: "cli",
    })
    
    test.AssertInputHandled(t, result, "handled")
}
```

### Testing Headers and Footers

```go
func TestHeaderFooter(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    
    test.AssertHeaderSet(t, harness)
    test.AssertFooterSet(t, harness)
    
    // Inspect content
    header := harness.Context().GetHeader()
    if header != nil {
        t.Logf("Header text: %s", header.Content.Text)
    }
}
```

### Testing Status Bar

```go
func TestStatus(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.AgentEndEvent{})
    
    test.AssertStatusSet(t, harness, "myext:status")
    test.AssertStatusText(t, harness, "myext:status", "Ready")
}
```

### Testing Print Output

```go
func TestOutput(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    _, _ = harness.Emit(extensions.ToolCallEvent{ToolName: "test"})
    
    // Exact match
    test.AssertPrinted(t, harness, "exact output")
    
    // Partial match
    test.AssertPrintedContains(t, harness, "partial")
    
    // Styled output
    test.AssertPrintInfo(t, harness, "info message")
    test.AssertPrintError(t, harness, "error message")
}
```

### Testing with Prompts

Configure mock prompt results for testing interactive behavior:

```go
func TestWithPrompts(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Configure what prompts should return
    harness.Context().SetPromptSelectResult(extensions.PromptSelectResult{
        Value:     "option1",
        Index:     0,
        Cancelled: false,
    })
    
    harness.Context().SetPromptConfirmResult(extensions.PromptConfirmResult{
        Value:     true,
        Cancelled: false,
    })
    
    // Now when your extension calls ctx.PromptSelect(), it gets this result
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
}
```

### Testing Complete Session Flow

```go
func TestFullSession(t *testing.T) {
    harness := test.New(t)
    harness.LoadFile("my-ext.go")
    
    // Simulate a complete session
    _, _ = harness.Emit(extensions.SessionStartEvent{SessionID: "test"})
    _, _ = harness.Emit(extensions.BeforeAgentStartEvent{})
    _, _ = harness.Emit(extensions.AgentStartEvent{})
    
    // Multiple tool calls
    tools := []string{"Read", "Grep", "Bash"}
    for _, tool := range tools {
        _, _ = harness.Emit(extensions.ToolCallEvent{ToolName: tool})
        _, _ = harness.Emit(extensions.ToolResultEvent{ToolName: tool})
    }
    
    _, _ = harness.Emit(extensions.AgentEndEvent{})
    _, _ = harness.Emit(extensions.SessionShutdownEvent{})
    
    // Verify final state
    test.AssertWidgetTextContains(t, harness, "status", "Complete")
}
```

## Available Assertions

The test package provides these assertion helpers:

### Event Results

| Function | Description |
|----------|-------------|
| `AssertNotBlocked(t, result)` | Verify tool was not blocked |
| `AssertBlocked(t, result, reason)` | Verify tool was blocked with reason |
| `AssertInputHandled(t, result, action)` | Verify input was handled |
| `AssertInputTransformed(t, result, text)` | Verify input was transformed |

### Context Interactions

| Function | Description |
|----------|-------------|
| `AssertPrinted(t, harness, text)` | Verify exact print output |
| `AssertPrintedContains(t, harness, substring)` | Verify partial print output |
| `AssertPrintInfo(t, harness, text)` | Verify PrintInfo was called |
| `AssertPrintError(t, harness, text)` | Verify PrintError was called |
| `AssertWidgetSet(t, harness, id)` | Verify widget was set |
| `AssertWidgetNotSet(t, harness, id)` | Verify widget was not set |
| `AssertWidgetText(t, harness, id, text)` | Verify widget content |
| `AssertWidgetTextContains(t, harness, id, substring)` | Verify widget contains text |
| `AssertHeaderSet(t, harness)` | Verify header was set |
| `AssertFooterSet(t, harness)` | Verify footer was set |
| `AssertStatusSet(t, harness, key)` | Verify status was set |
| `AssertStatusText(t, harness, key, text)` | Verify status text |

### Registration

| Function | Description |
|----------|-------------|
| `AssertToolRegistered(t, harness, name)` | Verify tool registration |
| `AssertCommandRegistered(t, harness, name)` | Verify command registration |
| `AssertHasHandlers(t, harness, eventType)` | Verify handlers exist |
| `AssertNoHandlers(t, harness, eventType)` | Verify no handlers |

### Messaging

| Function | Description |
|----------|-------------|
| `AssertMessageSent(t, harness, text)` | Verify SendMessage was called |
| `AssertCancelAndSend(t, harness, text)` | Verify CancelAndSend was called |

## Helper Functions

For custom assertions, extract result details:

```go
result, _ := harness.Emit(extensions.ToolCallEvent{...})
tcr := test.GetToolCallResult(result)
if tcr != nil {
    t.Logf("Block: %v, Reason: %s", tcr.Block, tcr.Reason)
}

ir := test.GetInputResult(result)
trr := test.GetToolResultResult(result)
```

## Advanced Usage

### Accessing the Mock Context

For custom verification:

```go
ctx := harness.Context()

// Get all recorded prints
prints := ctx.GetPrints()

// Check options
value := ctx.GetOption("my-option")

// Verify widget properties
widget, ok := ctx.GetWidget("my-widget")
if ok && widget.Style.BorderColor == "#ff0000" {
    t.Log("Widget has red border")
}

// Check status entries
status, ok := ctx.GetStatus("myext:status")
```

### Testing Multiple Extensions

Each harness is isolated:

```go
harness1 := test.New(t)
harness1.LoadFile("ext1.go")

harness2 := test.New(t)
harness2.LoadFile("ext2.go")

// Events to one don't affect the other
```

### Running Tests

Run all tests in your extension directory:

```bash
cd examples/extensions
go test -v
```

Run with race detector:

```bash
go test -race -v
```

Run a specific test:

```bash
go test -v -run TestMyExtension
```

## Best Practices

1. **Test one behavior per test** — Keep tests focused and readable
2. **Use inline source for edge cases** — `LoadString()` is great for testing specific scenarios
3. **Use `LoadFile()` for integration tests** — Tests the actual extension file
4. **Assert on context calls** — Verify your extension interacts with the context correctly
5. **Test both positive and negative cases** — Verify tools are blocked AND allowed appropriately
6. **Test all event handlers** — Make sure all registered handlers work correctly
7. **Use descriptive test names** — `TestExtension_BlocksDangerousTools` is clearer than `Test1`

## Limitations

The test harness has these intentional limitations:

- **No TUI rendering** — Widgets are recorded but not rendered visually
- **Prompts return configured values** — Pre-configure prompt results in tests
- **Subagents don't spawn real processes** — `SpawnSubagent()` returns nil/empty results
- **LLM completions are mocked** — `Complete()` returns empty responses
- **Some context methods are no-ops** — `Exit()`, `SetActiveTools()`, etc. don't have side effects

These limitations focus testing on extension logic rather than the full Kit runtime.

## Complete Example

See `examples/extensions/tool-logger_test.go` for a complete example with 14 tests covering:

- Handler registration
- Tool call and result handling
- Session lifecycle events
- Input commands (`!time`, `!status`)
- Unknown command handling
- Concurrent operations (race condition check)
- Real file logging verification

---

## Kit

<div style="text-align: center; margin: 2rem 0;">
  <img src="/logo.jpg" alt="KIT" style="max-width: 400px; width: 100%; margin: 0 auto; display: block;" />
</div>

A powerful, extensible AI coding agent CLI with multi-provider support, built-in tools, and a rich extension system.

## Features

- **Multi-Provider LLM Support** — Anthropic, OpenAI, Google Gemini, Ollama, Azure OpenAI, AWS Bedrock, OpenRouter, and more
- **Built-in Core Tools** — bash (with interactive sudo password prompt), read, write, edit, grep, find, ls, subagent with no MCP overhead
- **Smart @ Attachments** — Binary files auto-detected via MIME type, MCP resources via `@mcp:server:uri`
- **MCP Integration** — Connect external MCP servers for expanded capabilities (tools, prompts, and resources)
- **Extension System** — Write custom tools, commands, widgets, and UI modifications in Go
- **Interactive TUI** — Rich terminal interface powered by Bubble Tea with streaming, syntax highlighting, and custom rendering
- **Session Management** — Tree-based conversation history with branching support
- **Non-Interactive Mode** — Script-friendly positional args with JSON output
- **ACP Server** — Run Kit as an [Agent Client Protocol](https://agentclientprotocol.com) agent over stdio
- **Go SDK** — Embed Kit in your own applications

## Quick links

| Resource | Description |
|----------|-------------|
| [Installation](/installation) | Get Kit up and running |
| [Quick Start](/quick-start) | Your first Kit session |
| [Configuration](/configuration) | Customize Kit for your workflow |
| [Extensions](/extensions/overview) | Build custom tools and UI components |
| [Go SDK](/sdk/overview) | Embed Kit in your applications |

---

## Installation

# Installation

## Using npm / bun / pnpm

```bash
npm install -g @mark3labs/kit
```

```bash
bun install -g @mark3labs/kit
```

```bash
pnpm install -g @mark3labs/kit
```

## Using Go

```bash
go install github.com/mark3labs/kit/cmd/kit@latest
```

## Building from source

```bash
git clone https://github.com/mark3labs/kit.git
cd kit
go build -o kit ./cmd/kit
```

## Verifying the installation

After installing, verify Kit is available:

```bash
kit --help
```

## Setting up a provider

Kit needs at least one LLM provider configured. Set an API key for your preferred provider:

```bash
# Anthropic (default provider)
export ANTHROPIC_API_KEY="sk-..."

# OpenAI
export OPENAI_API_KEY="sk-..."

# Google Gemini
export GOOGLE_API_KEY="..."
```

For OAuth-enabled providers like Anthropic, you can also authenticate interactively:

```bash
kit auth login anthropic
```

See [Providers](/providers) for the full list of supported providers and their configuration.

---

## Providers

# Providers

Kit supports a wide range of LLM providers through a unified `provider/model` string format.

## Supported providers

| Provider | Prefix | Description |
|----------|--------|-------------|
| **Anthropic** | `anthropic/` | Claude models (native, prompt caching, OAuth) |
| **OpenAI** | `openai/` | GPT models |
| **GitHub Copilot** | `copilot/` | Copilot models through GitHub device login (experimental) |
| **Google** | `google/` or `gemini/` | Gemini models |
| **Ollama** | `ollama/` | Local models |
| **Azure OpenAI** | `azure/` | Azure-hosted OpenAI |
| **AWS Bedrock** | `bedrock/` | Bedrock models |
| **Google Vertex** | `google-vertex-anthropic/` | Claude on Vertex AI |
| **OpenRouter** | `openrouter/` | Multi-provider router |
| **Vercel AI** | `vercel/` | Vercel AI SDK models |
| **Custom** | `custom/` | Any OpenAI-compatible endpoint |
| **Auto-routed** | any | Any provider from the models.dev database |

## Model string format

```bash
provider/model            # Standard format
anthropic/claude-sonnet-latest
openai/gpt-4o
copilot/gpt-5.5
ollama/llama3
google/gemini-2.5-flash
```

## Model aliases

Kit provides aliases for commonly used models:

### Anthropic Claude

```bash
claude-opus-latest        → claude-opus-4-6
claude-sonnet-latest      → claude-sonnet-4-6
claude-haiku-latest       → claude-haiku-4-5
claude-4-opus-latest      → claude-opus-4-6
claude-4-sonnet-latest    → claude-sonnet-4-6
claude-4-haiku-latest     → claude-haiku-4-5
claude-3-7-sonnet-latest  → claude-3-7-sonnet-20250219
claude-3-5-sonnet-latest  → claude-3-5-sonnet-20241022
claude-3-5-haiku-latest   → claude-3-5-haiku-20241022
claude-3-opus-latest      → claude-3-opus-20240229
```

### OpenAI GPT

```bash
o1-latest                 → o1
o3-latest                 → o3
o4-latest                 → o4-mini
gpt-5-latest              → gpt-5.4
gpt-5-chat-latest         → gpt-5.4
gpt-4-latest              → gpt-4o
gpt-4                     → gpt-4o
gpt-3.5-latest            → gpt-3.5-turbo
gpt-3.5                   → gpt-3.5-turbo
codex-latest              → codex-mini-latest
```

### Google Gemini

```bash
gemini-pro-latest         → gemini-2.5-pro
gemini-flash-latest       → gemini-2.5-flash
gemini-flash              → gemini-2.5-flash
gemini-pro                → gemini-2.5-pro
```

## Specifying a model

Via CLI flag:

```bash
kit --model openai/gpt-4o
kit -m ollama/llama3
```

Via config file:

```yaml
model: anthropic/claude-sonnet-latest
```

Via environment variable:

```bash
export KIT_MODEL="google/gemini-2.0-flash-exp"
```

## Authentication

### API keys

Set the appropriate environment variable for your provider:

```bash
export ANTHROPIC_API_KEY="sk-..."
export OPENAI_API_KEY="sk-..."
export GOOGLE_API_KEY="..."
```

Or pass it directly:

```bash
kit --provider-api-key "sk-..." --model openai/gpt-4o
```

### OAuth

For providers that support OAuth:

```bash
kit auth login anthropic     # Anthropic OAuth
kit auth login openai        # ChatGPT/Codex OAuth
kit auth login copilot       # GitHub Copilot device login (experimental)
kit auth status              # Check authentication status
kit auth logout copilot      # Remove credentials
```

The experimental `copilot/` provider requires an active GitHub Copilot subscription
and uses GitHub device login; no OpenAI account or OpenAI API key is required.

### Custom provider URL

For self-hosted or proxy endpoints:

```bash
kit --provider-url "https://my-proxy.example.com/v1" --model openai/gpt-4o
```

When `--provider-url` is set with an explicit `--model`, Kit routes through the
`custom` (OpenAI-compatible) wire and strips any provider prefix from the model
name. So `openai/gpt-4o`, `google/gemma-4-12b`, and bare `gpt-4o` all resolve
to the same endpoint — Kit treats `--provider-url` as authoritative about *where*
to send the request, and the model string as just the upstream model id.

This avoids name collisions when a local server (LM Studio, Ollama, vLLM, ...)
happens to expose a model whose name matches a known cloud provider.

When `--provider-url` is provided without `--model`, Kit automatically defaults to `custom/custom`:

```bash
kit --provider-url "http://localhost:8080/v1" "Hello"
```

The `custom/custom` model has zero cost, 262K context window, and supports reasoning. It routes through the `openaicompat` provider and accepts any OpenAI-compatible API endpoint.

Optionally set `CUSTOM_API_KEY` environment variable or use `--provider-api-key` for endpoints requiring authentication.

## Auto-routed providers

Any provider in the [models.dev](https://models.dev) database can be used with the
standard `provider/model` format, even without a dedicated native integration. Kit
auto-routes the request through the matching **wire protocol** — the actual API
shape the provider speaks — rather than requiring a per-provider code path:

| Wire protocol | npm package (models.dev) | Transport used |
|---------------|--------------------------|----------------|
| OpenAI (Responses API) | `@ai-sdk/openai` | OpenAI |
| OpenAI (chat completions) | `@ai-sdk/openai-compatible` | OpenAI-compatible |
| Anthropic | `@ai-sdk/anthropic` | Anthropic |
| Google Gemini | `@ai-sdk/google` | Google |

The provider's `api` URL from the database is used as the base URL. A provider
whose npm package isn't recognized but that has an `api` URL falls back to the
OpenAI-compatible wire.

Because routing follows the wire protocol, aggregator/proxy providers work across
**all** of their models — including ones they re-flavor onto a different protocol
via a per-model override. For example, an aggregator that proxies Claude, GPT,
*and* Gemini routes them to the Anthropic, OpenAI, and Google transports
respectively:

```bash
kit --model opencode/claude-haiku-4-5 "Hello"     # → Anthropic wire
kit --model opencode/gpt-5 "Hello"                # → OpenAI wire
kit --model opencode/gemini-3.5-flash "Hello"     # → Google wire
```

Provide the provider's API key the same way as any other — via its environment
variable (e.g. `OPENCODE_API_KEY`) or `--provider-api-key`.

## Model database

Kit ships with a local model database that maps provider names to API configurations. You can manage it with:

```bash
kit models                   # List available models
kit models openai            # Filter by provider
kit models --all             # Show all providers
kit update-models            # Update from models.dev
kit update-models embedded   # Reset to bundled database
```

---

## Quick Start

# Quick Start

## Basic usage

Start an interactive session:

```bash
kit
```

Run a one-off prompt:

```bash
kit "List files in src/"
```

Attach files as context using the `@` prefix:

```bash
kit @main.go @test.go "Review these files"
```

Binary files (images, audio, PDFs) are automatically detected via MIME type and sent as multimodal attachments. You can also reference MCP resources:

```bash
kit @mcp:myserver:file:///data/report.csv "Summarize this data"
```

Use a specific model:

```bash
kit --model anthropic/claude-sonnet-latest
```

## Non-interactive mode

Kit can run as a non-interactive tool for scripting and automation.

Get JSON output:

```bash
kit "Explain main.go" --json
```

Quiet mode (final response only, no TUI):

```bash
kit "Run tests" --quiet
```

Ephemeral mode (no session file created):

```bash
kit "Quick question" --no-session
```

## Resuming sessions

Continue the most recent session for the current directory:

```bash
kit --continue
# or
kit -c
```

Pick from previous sessions interactively:

```bash
kit --resume
# or
kit -r
```

## ACP server mode

Kit can run as an [ACP (Agent Client Protocol)](https://agentclientprotocol.com) agent server, enabling ACP-compatible clients (such as [OpenCode](https://github.com/sst/opencode)) to drive Kit as a remote coding agent over stdio:

```bash
# Start Kit as an ACP server (JSON-RPC 2.0 on stdin/stdout)
kit acp

# With debug logging to stderr
kit acp --debug
```

The ACP server exposes Kit's full capabilities — LLM execution, tool calls (bash, read, write, edit, grep, etc.), and session persistence — over the standard ACP protocol.

---

## Callbacks

# Callbacks

## Event-based monitoring

Subscribe to events for real-time monitoring. Each method returns an unsubscribe function:

```go
unsub := host.OnToolCall(func(event kit.ToolCallEvent) {
    fmt.Printf("Tool: %s, Args: %s\n", event.ToolName, event.ToolArgs)
})
defer unsub()

unsub2 := host.OnToolResult(func(event kit.ToolResultEvent) {
    fmt.Printf("Result: %s (error: %v)\n", event.ToolName, event.IsError)
})
defer unsub2()

unsub3 := host.OnMessageUpdate(func(event kit.MessageUpdateEvent) {
    fmt.Print(event.Chunk)
})
defer unsub3()

unsub4 := host.OnResponse(func(event kit.ResponseEvent) {
    fmt.Println("Final response received")
})
defer unsub4()

unsub5 := host.OnTurnStart(func(event kit.TurnStartEvent) {
    fmt.Println("Turn started")
})
defer unsub5()

unsub6 := host.OnTurnEnd(func(event kit.TurnEndEvent) {
    fmt.Println("Turn ended")
})
defer unsub6()
```

## Tool call argument streaming

For tools with large arguments (e.g., `write` with a full file body), the `ToolCallEvent` only fires after the full argument JSON finishes streaming — which can take 5-10+ seconds of "dead air." These three events fire during argument generation so UIs can show activity immediately:

```go
host.OnToolCallStart(func(event kit.ToolCallStartEvent) {
    // Fires as soon as the LLM begins generating tool arguments.
    // event.ToolCallID, event.ToolName, event.ToolKind
    fmt.Printf("⏳ %s generating arguments...\n", event.ToolName)
})

host.OnToolCallDelta(func(event kit.ToolCallDeltaEvent) {
    // Each streamed JSON fragment of the tool arguments.
    // event.ToolCallID, event.Delta
    // Useful for live-previewing content or showing byte progress.
})

host.OnToolCallEnd(func(event kit.ToolCallEndEvent) {
    // Tool argument streaming complete — execution about to begin.
    // event.ToolCallID
    fmt.Printf("✓ Arguments ready, executing...\n")
})
```

**Full tool lifecycle**: `ToolCallStartEvent` → `ToolCallDeltaEvent` (repeated) → `ToolCallEndEvent` → `ToolCallEvent` → `ToolExecutionStartEvent` → `ToolOutputEvent` (optional) → `ToolExecutionEndEvent` → `ToolResultEvent`

## Hook system

Hooks can **modify or cancel** operations. Unlike events (read-only), hooks are read-write interceptors.

### BeforeToolCall — block tool execution

```go
host.OnBeforeToolCall(kit.HookPriorityNormal, func(h kit.BeforeToolCallHook) *kit.BeforeToolCallResult {
    // h.ToolCallID, h.ToolName, h.ToolArgs
    if h.ToolName == "bash" && strings.Contains(h.ToolArgs, "rm -rf") {
        return &kit.BeforeToolCallResult{Block: true, Reason: "dangerous command"}
    }
    return nil // allow
})
```

### AfterToolResult — modify tool output

```go
host.OnAfterToolResult(kit.HookPriorityNormal, func(h kit.AfterToolResultHook) *kit.AfterToolResultResult {
    // h.ToolCallID, h.ToolName, h.ToolArgs, h.Result, h.IsError
    if h.ToolName == "read" {
        filtered := redactSecrets(h.Result)
        return &kit.AfterToolResultResult{Result: &filtered}
    }
    return nil
})
```

### BeforeTurn — modify prompt, inject messages

```go
host.OnBeforeTurn(kit.HookPriorityNormal, func(h kit.BeforeTurnHook) *kit.BeforeTurnResult {
    // h.Prompt
    newPrompt := h.Prompt + "\nAlways respond in JSON."
    return &kit.BeforeTurnResult{Prompt: &newPrompt}
    // Also available: SystemPrompt *string, InjectText *string
})
```

### AfterTurn — observation only

```go
host.OnAfterTurn(kit.HookPriorityNormal, func(h kit.AfterTurnHook) {
    // h.Response, h.Error
    log.Printf("Turn completed: %d chars", len(h.Response))
})
```

### PrepareStep — intercept messages between steps

The most powerful hook — fires between steps within a multi-step agent turn, after any steering messages are injected and before messages are sent to the LLM. Can replace the entire context window.

```go
host.OnPrepareStep(kit.HookPriorityNormal, func(h kit.PrepareStepHook) *kit.PrepareStepResult {
    // h.StepNumber — zero-based step index within the turn
    // h.Messages   — current context window (includes any steering)
    
    // Example: transform tool results with images into user messages
    modified := transformImageToolResults(h.Messages)
    return &kit.PrepareStepResult{Messages: modified}
    // Return nil to pass through unchanged
})
```

Use cases: transforming tool results (e.g., image data for vision models), dynamic tool filtering per step, mid-turn context injection, custom stop conditions.

### Hook priorities

```go
kit.HookPriorityHigh   = 0   // runs first
kit.HookPriorityNormal = 50  // default
kit.HookPriorityLow    = 100 // runs last
```

Lower values run first. First non-nil result wins.

## All event types

| Event | Typed Subscriber | Description |
|-------|-----------------|-------------|
| `TurnStartEvent` | `OnTurnStart` | Agent turn started |
| `TurnEndEvent` | `OnTurnEnd` | Agent turn completed |
| `MessageStartEvent` | `OnMessageStart` | New assistant message begins |
| `MessageUpdateEvent` | `OnMessageUpdate` | Streaming text chunk from LLM |
| `MessageEndEvent` | `OnMessageEnd` | Assistant message complete |
| `ToolCallStartEvent` | `OnToolCallStart` | LLM began generating tool call arguments |
| `ToolCallDeltaEvent` | `OnToolCallDelta` | Streamed JSON fragment of tool call arguments |
| `ToolCallEndEvent` | `OnToolCallEnd` | Tool argument streaming complete |
| `ToolCallEvent` | `OnToolCall` | Tool call fully parsed, about to execute |
| `ToolExecutionStartEvent` | `OnToolExecutionStart` | Tool begins executing |
| `ToolExecutionEndEvent` | `OnToolExecutionEnd` | Tool finishes executing |
| `ToolResultEvent` | `OnToolResult` | Tool execution completed with result |
| `ToolCallContentEvent` | `OnToolCallContent` | Text content alongside tool calls |
| `ToolOutputEvent` | `OnToolOutput` | Streaming output chunk from tool (e.g., bash) |
| `ResponseEvent` | `OnResponse` | Final response received |
| `ReasoningStartEvent` | `OnReasoningStart` | LLM begins reasoning/thinking |
| `ReasoningDeltaEvent` | `OnReasoningDelta` | Streaming reasoning/thinking chunk |
| `ReasoningCompleteEvent` | `OnReasoningComplete` | Reasoning/thinking finished |
| `StepStartEvent` | `OnStepStart` | New LLM call begins within a turn |
| `StepFinishEvent` | `OnStepFinish` | Step completes (with usage, finish reason, tool call info) |
| `StepUsageEvent` | `OnStepUsage` | Per-step token usage |
| `StreamFinishEvent` | `OnStreamFinish` | Per-step stream completes (with usage + finish reason) |
| `TextStartEvent` | `OnTextStart` | LLM begins text content generation |
| `TextEndEvent` | `OnTextEnd` | LLM finishes text content generation |
| `WarningsEvent` | `OnWarnings` | LLM provider returned warnings |
| `SourceEvent` | `OnSource` | LLM referenced a source (e.g., web search) |
| `ErrorEvent` | `OnError` | Agent-level error during streaming |
| `RetryEvent` | `OnRetry` | LLM request retried after transient error |
| `CompactionEvent` | `OnCompaction` | Conversation compacted |
| `SteerConsumedEvent` | `OnSteerConsumed` | Steering messages injected into turn |
| `PasswordPromptEvent` | — | Sudo command needs password (respond via `ResponseCh`) |

> **Note:** `OnStreaming` is a deprecated alias for `OnMessageUpdate` and will be removed in a future release.

## Subagent event monitoring

Monitor real-time events from LLM-initiated subagents (when the model uses the `subagent` tool):

```go
host.OnToolCall(func(e kit.ToolCallEvent) {
    if e.ToolName == "subagent" {
        host.SubscribeSubagent(e.ToolCallID, func(event kit.Event) {
            // Receives the same event types as Subscribe(), scoped to the child agent
            switch ev := event.(type) {
            case kit.MessageUpdateEvent:
                fmt.Print(ev.Chunk)
            case kit.ToolCallEvent:
                fmt.Printf("Subagent calling: %s\n", ev.ToolName)
            }
        })
    }
})
```

`SubscribeSubagent` returns an unsubscribe function. Listeners are also cleaned up automatically when the subagent completes. See [Subagents](/advanced/subagents) for more details.

---

## SDK Options

# SDK Options

Pass an `Options` struct to `kit.New()` to configure the Kit instance.

::: tip
For simple setups, `kit.NewAgent(ctx, ...Option)` provides functional-options
helpers (`WithModel`, `WithStreaming`, `Ephemeral`, ...) over the same `Options`
struct. See [Functional options](/sdk/overview#functional-options-newagent).
:::

Each `kit.New` / `kit.NewAgent` call owns an isolated configuration store, so
these options never leak between Kit instances in the same process. See
[Per-instance config isolation](/sdk/overview#per-instance-config-isolation).

## Full options reference

```go
host, err := kit.New(ctx, &kit.Options{
    // Model
    Model:        "ollama/llama3",
    SystemPrompt: "You are a helpful bot",
    ConfigFile:   "/path/to/config.yml",

    // Behavior
    MaxSteps:     10,
    Streaming:    ptrBool(true), // *bool: nil = unset (default true), &false = off
    Quiet:        true,
    Debug:        true,

    // Generation parameters (override env/config/per-model defaults)
    MaxTokens:        16384,              // 0 = auto-resolve; non-zero suppresses right-sizing
    ThinkingLevel:    "medium",           // "off", "none", "minimal", "low", "medium", "high"
    Temperature:      ptrFloat32(0.2),    // pointer so explicit 0.0 != unset
    TopP:             nil,                 // nil = provider/per-model default
    TopK:             nil,
    FrequencyPenalty: nil,
    PresencePenalty:  nil,

    // Provider configuration
    ProviderAPIKey: "sk-...",                      // "" = use config / provider env var
    ProviderURL:    "https://proxy.internal/v1",  // "" = provider default endpoint
    TLSSkipVerify:  false,                         // only effective when true

    // Session
    SessionPath:  "./session.jsonl",
    SessionDir:   "/custom/sessions/",
    Continue:     true,
    NoSession:    true,

    // Tools
    Tools:            []kit.Tool{...},     // Replace default tool set entirely
    ExtraTools:       []kit.Tool{...},     // Add tools alongside defaults
    DisableCoreTools: true,                // Use no core tools (0 tools, for chat-only)

    // Configuration
    SkipConfig:   true,                   // Skip .kit.yml files (viper defaults + env vars still apply)

    // Compaction
    AutoCompact:  true,

    // Skills
    Skills:       []string{"/path/to/skill.md"},
    SkillsDir:    "/path/to/skills/",
    NoSkills:     true,

    // Feature toggles
    NoExtensions:   true,               // disable Yaegi extension loading
    NoContextFiles: true,               // disable automatic AGENTS.md loading

    // Session (advanced)
    SessionManager: myCustomSession,    // custom SessionManager implementation

    // MCP OAuth — both opt-in. Leave MCPAuthHandler nil to disable
    // OAuth entirely (remote MCP 401s bubble up as errors). CLI apps
    // pass kit.NewCLIMCPAuthHandler(); custom UX embedders implement
    // MCPAuthHandler or configure DefaultMCPAuthHandler + OnAuthURL.
    MCPAuthHandler: authHandler,                  // nil = OAuth disabled
    MCPTokenStoreFactory: func(serverURL string) (kit.MCPTokenStore, error) {
        return myStore(serverURL), nil
    },

    // In-Process MCP Servers
    InProcessMCPServers: map[string]*kit.MCPServer{
        "docs": mcpSrv,  // *server.MCPServer from mcp-go
    },
})
```

## Options fields

### Core

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `Model` | `string` | config default | Model string (provider/model format) |
| `SystemPrompt` | `string` | — | System prompt text or file path |
| `ConfigFile` | `string` | `~/.kit.yml` | Path to config file |
| `MaxSteps` | `int` | `0` | Max agent steps (0 = unlimited) |
| `Streaming` | `*bool` | `nil` | Enable streaming output. `nil` leaves it to the precedence chain (env → config → default `true`); `&true`/`&false` forces it. Pointer so unset is distinct from explicit `false`. |
| `Quiet` | `bool` | `false` | Suppress output |
| `Debug` | `bool` | `false` | Enable debug logging |

### Generation parameters

These fields override the corresponding values from `.kit.yml` / `KIT_*`
environment variables. Leaving a field at its zero/nil value lets the
precedence chain resolve a value (`KIT_*` env → config file → per-model
defaults from `modelSettings`/`customModels` → an 8192 SDK floor for
`MaxTokens` (matching the CLI `--max-tokens` default) and provider-level
defaults for samplers).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `MaxTokens` | `int` | auto-resolved | Max output tokens per response. `0` = auto-resolve; non-zero suppresses automatic right-sizing (same semantics as `--max-tokens`). |
| `ThinkingLevel` | `string` | auto-resolved | Reasoning effort: `"off"`, `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`. `""` falls through to config/env/per-model/`"off"`. |
| `Temperature` | `*float32` | — | Sampling randomness. Pointer type so explicit `0.0` is distinguishable from "unset". |
| `TopP` | `*float32` | — | Nucleus sampling cutoff. `nil` leaves provider/per-model default. |
| `TopK` | `*int32` | — | Top-K sampling limit. `nil` leaves provider/per-model default. |
| `FrequencyPenalty` | `*float32` | — | OpenAI-family frequency penalty. `nil` leaves provider default. |
| `PresencePenalty` | `*float32` | — | OpenAI-family presence penalty. `nil` leaves provider default. |

Pointer-typed fields (`Streaming` and the samplers) are populated via tiny helpers:

```go
func ptrBool(v bool) *bool          { return &v }
func ptrFloat32(v float32) *float32 { return &v }
```

These fields eliminate the need for `viper.Set()` calls before `kit.New()`
when embedding Kit as a library.

### Provider configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `ProviderAPIKey` | `string` | — | API key used to authenticate with the provider. `""` falls back to config / provider-specific env var (e.g. `ANTHROPIC_API_KEY`). When set, it takes precedence over config and env values on this instance's store. |
| `ProviderURL` | `string` | — | Override the provider endpoint (e.g. LiteLLM, vLLM, Azure OpenAI, internal proxy). `""` = provider default. |
| `TLSSkipVerify` | `bool` | `false` | Disable TLS certificate verification on the provider HTTP client. Only effective when `true`; to force-disable, use config file or env var instead. For self-signed dev certs only. |

### Session

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `SessionPath` | `string` | — | Open a specific session file |
| `SessionDir` | `string` | — | Base directory for session discovery |
| `Continue` | `bool` | `false` | Resume most recent session |
| `NoSession` | `bool` | `false` | Ephemeral mode (no persistence) |
| `SessionManager` | `SessionManager` | — | Custom session backend (advanced) |

### Tools & extensions

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `Tools` | `[]Tool` | — | Replace the entire default tool set |
| `ExtraTools` | `[]Tool` | — | Additional tools alongside core/MCP/extension tools |
| `DisableCoreTools` | `bool` | `false` | Use no core tools (0 tools, for chat-only) |
| `NoExtensions` | `bool` | `false` | Disable Yaegi extension loading |
| `NoContextFiles` | `bool` | `false` | Disable automatic AGENTS.md loading |

### Skills & configuration

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `SkipConfig` | `bool` | `false` | Skip `.kit.yml` file loading (viper defaults + env vars still apply) |
| `Skills` | `[]string` | — | Explicit skill files/dirs to load |
| `SkillsDir` | `string` | — | Override default skills directory |
| `NoSkills` | `bool` | `false` | Disable skill loading entirely |

These fields only control the **initial** skill and context-file set picked
up by `New()`. To add, remove, or replace skills and `AGENTS.md`-style
context files at runtime (e.g. per user or per session), use the
`AddSkill` / `LoadAndAddSkill` / `RemoveSkill` / `SetSkills` and
`AddContextFile` / `AddContextFileContent` / `RemoveContextFile` /
`SetContextFiles` methods on `*kit.Kit`. See
[Runtime skills and context files](/sdk/overview#runtime-skills-and-context-files).

### Compaction & MCP

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `AutoCompact` | `bool` | `false` | Auto-compact when near context limit |
| `CompactionOptions` | `*CompactionOptions` | — | Configuration for auto-compaction |
| `MCPAuthHandler` | `MCPAuthHandler` | — | OAuth handler for remote MCP servers. `nil` disables OAuth (servers returning 401 fail with the authorization-required error). See [MCP OAuth](#mcp-oauth-authorization) below. |
| `MCPTokenStoreFactory` | `func` | — | Custom OAuth token storage for MCP servers (default: JSON file in `$XDG_CONFIG_HOME/.kit/mcp_tokens.json`). |
| `InProcessMCPServers` | `map[string]*MCPServer` | — | In-process mcp-go servers (no subprocess) |
| `MCPTaskMode` | `map[string]MCPTaskMode` | — | Per-server override for task-augmented `tools/call`. Keys are server names; missing entries fall back to the `tasksMode` field of the matching `MCPServerConfig`. See [MCP Tasks](#mcp-tasks). |
| `MCPTaskTimeout` | `time.Duration` | `15m` | Maximum wall-clock to wait for a task to reach a terminal state. Independent of any per-call context deadline. |
| `MCPTaskTTL` | `time.Duration` | — | TTL hint sent in `TaskParams` for every task-augmented call. Zero omits the field and lets the server pick. |
| `MCPTaskPollInterval` | `time.Duration` | `1s` | Fallback interval between `tasks/get` requests when the server does not suggest one. |
| `MCPTaskMaxPollInterval` | `time.Duration` | `5s` | Cap on the polling interval (a server-supplied `pollInterval` can otherwise grow without bound). |
| `MCPTaskProgress` | `MCPTaskProgressHandler` | — | Optional callback invoked once when a task is accepted and on every observed status transition. The final invocation always carries a terminal status. |

## MCP OAuth Authorization

When a remote MCP server (SSE or Streamable HTTP) requires OAuth, Kit runs
the full authorization flow (dynamic client registration → PKCE → user
consent → token exchange → token persistence) but delegates the **user-facing
step** — displaying the authorization URL and receiving the callback — to
an `MCPAuthHandler`.

The SDK is deliberately inert when `MCPAuthHandler` is `nil`: it does **not**
auto-construct a default handler, bind a local TCP port, or open a browser.
This keeps library, daemon, and web-app embedders free of surprise I/O.
Consumers opt in by passing a handler explicitly.

| Building block | When to use |
|---|---|
| `MCPAuthHandler = nil` (default) | OAuth disabled. Remote MCP servers requiring auth fail with a clear error. Correct for libraries, daemons, and web apps. |
| `kit.NewCLIMCPAuthHandler()` | CLI/TUI apps. Opens the system browser, prints status to stderr (or via `NotifyFunc`), runs a localhost callback server. Used by the `kit` binary. |
| `kit.NewDefaultMCPAuthHandler()` + `OnAuthURL` | Custom UX. Use the SDK's port reservation and callback server; plug in your own presentation via the `OnAuthURL(serverName, authURL)` closure. |
| Implement `kit.MCPAuthHandler` directly | Full control. No localhost binding — e.g. return the URL from an HTTP endpoint and have the consumer POST the callback URL back. |

**CLI-style embedder:**

```go
authHandler, err := kit.NewCLIMCPAuthHandler()
if err != nil {
    log.Fatal(err)
}
defer authHandler.Close() // release the reserved port

host, _ := kit.New(ctx, &kit.Options{
    MCPAuthHandler: authHandler,
})
```

**Custom UX embedder (TUI modal, QR code, web redirect, etc.):**

```go
authHandler, _ := kit.NewDefaultMCPAuthHandler()
authHandler.OnAuthURL = func(serverName, authURL string) {
    // No browser or terminal assumptions — render however you like.
    myUI.ShowAuthPrompt(serverName, authURL)
}
defer authHandler.Close()

host, _ := kit.New(ctx, &kit.Options{
    MCPAuthHandler: authHandler,
})
```

**Fully custom handler (no local port binding at all):**

```go
type WebAuthHandler struct {
    redirectURI string
    callbacks   chan string
}

func (h *WebAuthHandler) RedirectURI() string { return h.redirectURI }

func (h *WebAuthHandler) HandleAuth(ctx context.Context, serverName, authURL string) (string, error) {
    // Push the URL to the user's existing browser session via your web app,
    // then block on the callback that your HTTP handler pushes onto the channel.
    h.pushToUserSession(serverName, authURL)
    select {
    case callbackURL := <-h.callbacks:
        return callbackURL, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
```

::: warning
`DefaultMCPAuthHandler` with no `OnAuthURL` set will silently drop the
authorization URL and hang until the 2-minute callback timeout fires. Always
set `OnAuthURL`, or use a higher-level wrapper like `CLIMCPAuthHandler`.
:::

## MCP Tasks

The [MCP Tasks utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)
turns a synchronous `tools/call` into a pollable async job: the server
returns a `taskId` with status `working` immediately, and the client polls
`tasks/get` / `tasks/result` until the task reaches a terminal state.

Kit advertises task support during `initialize` and, by default, augments
`tools/call` with task metadata only when the server advertises
`tasks/toolCalls` capability — so any existing MCP server keeps its previous
synchronous behaviour bit-for-bit. Long-running tools (builds, deployments,
batch jobs, sub-agent runs) get HTTP/SSE timeout-resistance and clean
cancellation "for free" once both sides opt in.

### Per-server mode

```go
import "time"

host, _ := kit.New(ctx, &kit.Options{
    MCPTaskMode: map[string]kit.MCPTaskMode{
        "build-server": kit.MCPTaskModeAlways, // force task-augmented calls
        "chat-server":  kit.MCPTaskModeNever,  // force synchronous calls
        // any server not in the map honours its `tasksMode` config field
        // (default "auto")
    },
})
```

| Mode | Behaviour |
|---|---|
| `MCPTaskModeAuto` (default) | Augment `tools/call` with `TaskParams` only when the server advertised `tasks/toolCalls`. |
| `MCPTaskModeNever` | Always issue `tools/call` synchronously, ignoring server capability. |
| `MCPTaskModeAlways` | Always opt in, even when the server didn't advertise the capability. The server may still respond synchronously. |

### Progress callbacks

```go
host, _ := kit.New(ctx, &kit.Options{
    MCPTaskTimeout:  15 * time.Minute,        // total wall-clock cap
    MCPTaskTTL:      30 * time.Minute,        // server retention hint
    MCPTaskProgress: func(p kit.MCPTaskProgress) {
        log.Printf("%s/%s: %s %s", p.Server, p.TaskID, p.Status, p.Message)
    },
})
```

The handler fires once when a task is accepted and again on every observed
status transition. The final call always carries a terminal status
(`MCPTaskStatusCompleted`, `MCPTaskStatusFailed`, or `MCPTaskStatusCancelled`).
Do not block in the handler — dispatch long work on a goroutine.

### Inspecting and cancelling tasks

```go
tasks, _ := host.ListMCPTasks(ctx, "build-server")
for _, t := range tasks {
    fmt.Printf("%s: %s (%s)\n", t.TaskID, t.Status, t.StatusMessage)
}

t, _ := host.GetMCPTask(ctx, "build-server", taskID)
if !t.Status.IsTerminal() {
    _, _ = host.CancelMCPTask(ctx, "build-server", taskID)
}
```

`Kit.ListMCPTasks`, `Kit.GetMCPTask`, and `Kit.CancelMCPTask` work against any
loaded MCP server that advertises the corresponding capability.
`MCPTaskStatus.IsTerminal()` is the canonical check for completion.

Context cancellation also works end-to-end: cancelling the `ctx` passed to a
tool execution triggers a best-effort `tasks/cancel` before the call returns.

## Precedence

For any given generation or provider field, the effective value is resolved
in this order (highest priority first):

1. `Options.X` (SDK caller)
2. `KIT_X` environment variable
3. `.kit.yml` (project-local then `~/.kit.yml`)
4. Per-model defaults (`modelSettings[provider/model]` or `customModels[...].params`)
5. Provider-level defaults (e.g. Anthropic's own temperature default)
6. SDK last-resort floor (currently: `MaxTokens = 8192`, matching the CLI `--max-tokens` default)

Sampling params that remain `nil` after the SDK resolution step are left out
of the provider call entirely, so the LLM library applies its own default.

## Tool configuration

**`Tools`** replaces ALL default tools (core + MCP + extension). **`ExtraTools`** adds tools alongside the defaults. Use `Tools` to restrict capabilities; use `ExtraTools` to extend them.

Create custom tools with `kit.NewTool` — no external dependencies needed:

```go
type LookupInput struct {
    ID string `json:"id" description:"Record ID to look up"`
}

lookupTool := kit.NewTool("lookup", "Look up a record by ID",
    func(ctx context.Context, input LookupInput) (kit.ToolOutput, error) {
        record := db.Find(input.ID)
        return kit.TextResult(record.String()), nil
    },
)

host, _ := kit.New(ctx, &kit.Options{
    ExtraTools: []kit.Tool{lookupTool},
})
```

See [Overview](/sdk/overview#custom-tools) for full custom tool documentation.

---

## Go SDK

# Go SDK

The `pkg/kit` package lets you embed Kit as a library in your Go applications.

## Installation

```bash
go get github.com/mark3labs/kit/pkg/kit
```

## Basic usage

```go
package main

import (
    "context"
    "log"

    kit "github.com/mark3labs/kit/pkg/kit"
)

func main() {
    ctx := context.Background()

    // Create Kit instance with default configuration
    host, err := kit.New(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer host.Close()

    // Send a prompt
    response, err := host.Prompt(ctx, "What is 2+2?")
    if err != nil {
        log.Fatal(err)
    }

    println(response)
}
```

## Functional options (`NewAgent`)

For simple programmatic setups, `kit.NewAgent` offers an ergonomic
functional-options front door over `kit.New`. Streaming is **enabled by
default**; pass `kit.WithStreaming(false)` to opt out.

```go
host, err := kit.NewAgent(ctx,
    kit.WithModel("anthropic/claude-sonnet-4-5-20250929"),
    kit.WithSystemPrompt("You are a helpful assistant."),
    kit.WithMaxTokens(8192),
    kit.WithThinkingLevel("medium"),
    kit.Ephemeral(), // in-memory session, no persistence
)
if err != nil {
    log.Fatal(err)
}
defer host.Close()
```

Available options:

| Option | Sets |
|--------|------|
| `WithModel(string)` | `Options.Model` (provider/model format) |
| `WithSystemPrompt(string)` | `Options.SystemPrompt` (inline text or file path) |
| `WithStreaming(bool)` | `Options.Streaming` (default `true` under `NewAgent`) |
| `WithMaxTokens(int)` | `Options.MaxTokens` |
| `WithThinkingLevel(string)` | `Options.ThinkingLevel` |
| `WithTools(...Tool)` | `Options.Tools` (replaces the default set) |
| `WithExtraTools(...Tool)` | `Options.ExtraTools` (adds alongside defaults) |
| `WithProviderAPIKey(string)` | `Options.ProviderAPIKey` |
| `WithProviderURL(string)` | `Options.ProviderURL` |
| `WithConfigFile(string)` | `Options.ConfigFile` |
| `WithDebug()` | `Options.Debug = true` |
| `Ephemeral()` | `Options.NoSession = true` |

Options are applied in order, so later options override earlier ones. `Option`
is a plain `func(*Options)`, so you can define your own. For advanced
configuration not covered by the helpers (custom MCP config, in-process MCP
servers, session backends, MCP task tuning) construct an `Options` value
explicitly and call `kit.New`.

### When to use which

| Constructor | Use when |
|-------------|----------|
| `kit.NewAgent(ctx, ...Option)` | Quick programmatic setups; you only need the common fields. Streaming defaults on. |
| `kit.New(ctx, *Options)` | You need fields without a `With*` helper (`MCPConfig`, `InProcessMCPServers`, `SessionManager`, MCP task tuning, etc.), or you already hold an `Options` value. |

## Per-instance config isolation

Each `kit.New` / `kit.NewAgent` call owns an **isolated configuration store**,
so constructing multiple Kit instances in the same process is safe: setting the
model, thinking level, or generation parameters on one never affects another,
and runtime mutators (`SetModel`, `SetThinkingLevel`) only touch the owning
instance. This makes subagent spawning and multi-Kit embedding race-free with
no external synchronization required.

```go
a, _ := kit.NewAgent(ctx, kit.WithThinkingLevel("low"))
b, _ := kit.NewAgent(ctx, kit.WithThinkingLevel("high"))

a.SetThinkingLevel(ctx, "medium")
// a.GetThinkingLevel() == "medium"; b.GetThinkingLevel() is still "high"
```

## Multi-turn conversations

Conversations retain context automatically across calls:

```go
host.Prompt(ctx, "My name is Alice")
response, _ := host.Prompt(ctx, "What's my name?")
// response: "Your name is Alice"
```

## Additional prompt methods

The SDK provides several prompt variants:

| Method | Description |
|--------|-------------|
| `Prompt(ctx, message)` | Simple prompt, returns response string |
| `PromptWithOptions(ctx, message, opts)` | With per-call options |
| `PromptResult(ctx, message)` | Returns full `TurnResult` with usage stats |
| `PromptResultWithFiles(ctx, message, files)` | Multimodal with file attachments |
| `Steer(ctx, instruction)` | System-level steering without user message |
| `FollowUp(ctx, text)` | Continue without new user input |

## Custom tools

Create custom tools with `kit.NewTool`. The JSON schema is auto-generated from the input struct — no external dependencies required:

```go
type WeatherInput struct {
    City string `json:"city" description:"City name"`
}

weatherTool := kit.NewTool("get_weather", "Get current weather for a city",
    func(ctx context.Context, input WeatherInput) (kit.ToolOutput, error) {
        return kit.TextResult("72°F, sunny in " + input.City), nil
    },
)

host, _ := kit.New(ctx, &kit.Options{
    ExtraTools: []kit.Tool{weatherTool},
})
```

Struct tags control the schema:

- `json:"name"` — parameter name
- `description:"..."` — description shown to the LLM
- `enum:"a,b,c"` — restrict valid values
- `omitempty` — marks the parameter as optional

Return values:

| Helper | Description |
|--------|-------------|
| `kit.TextResult(s)` | Successful text result |
| `kit.ErrorResult(s)` | Error result (LLM sees it as a tool error) |
| `kit.ImageResult(s, data, mediaType)` | Image result with binary data (e.g. `"image/png"`) |
| `kit.MediaResult(s, data, mediaType)` | Non-image media result (e.g. `"audio/mpeg"`) |

Binary data (images, audio, etc.) in `ToolOutput.Data` is automatically forwarded to the LLM when `MediaType` is set. For advanced use, return a `kit.ToolOutput` struct directly with `Data`, `MediaType`, and `Metadata` fields.

Use `kit.NewParallelTool` for tools that are safe to run concurrently. Use `kit.ToolCallIDFromContext(ctx)` to retrieve the LLM-assigned call ID for logging or tracing.

## Generation & provider overrides

SDK consumers can configure generation parameters and provider endpoints
entirely in-code via `Options`, without touching `.kit.yml` or `viper.Set()`:

```go
host, _ := kit.New(ctx, &kit.Options{
    Model:          "anthropic/claude-sonnet-4-5-20250929",
    MaxTokens:      16384,             // 0 = auto-resolve (env → config → per-model → floor)
    ThinkingLevel:  "high",            // "off" | "none" | "minimal" | "low" | "medium" | "high"
    Temperature:    ptrFloat32(0.2),   // nil = provider/per-model default
    ProviderAPIKey: os.Getenv("MY_SECRET"), // overrides pre-existing viper state
    ProviderURL:    "https://proxy.internal/v1",
})

func ptrFloat32(v float32) *float32 { return &v }
```

See [Options](/sdk/options#generation-parameters) for the full field reference,
including `TopP`, `TopK`, `FrequencyPenalty`, `PresencePenalty`, and `TLSSkipVerify`.

## Event system

Subscribe to events for monitoring:

```go
unsubscribe := host.OnToolCall(func(event kit.ToolCallEvent) {
    fmt.Println("Tool called:", event.Name)
})
defer unsubscribe()

host.OnToolResult(func(event kit.ToolResultEvent) {
    fmt.Println("Tool result:", event.Name)
})

host.OnMessageUpdate(func(event kit.MessageUpdateEvent) {
    fmt.Print(event.Chunk)
})
```

## Model management

Switch models at runtime:

```go
host.SetModel(ctx, "openai/gpt-4o")
info := host.GetModelInfo()
models := host.GetAvailableModels()
```

## Dynamic MCP servers

Add and remove MCP servers at runtime:

```go
n, err := host.AddMCPServer(ctx, "github", kit.MCPServerConfig{
    Command: []string{"npx", "-y", "@modelcontextprotocol/server-github"},
})
fmt.Printf("Loaded %d tools\n", n)

err = host.RemoveMCPServer("github")
servers := host.ListMCPServers() // []kit.MCPServerStatus
```

### In-process MCP servers

Register mcp-go servers running in the same process — zero subprocess overhead:

```go
import (
    "github.com/mark3labs/mcp-go/mcp"
    "github.com/mark3labs/mcp-go/server"
)

mcpSrv := server.NewMCPServer("my-tools", "1.0.0",
    server.WithToolCapabilities(true),
)
mcpSrv.AddTool(mcp.NewTool("search_docs",
    mcp.WithDescription("Search documentation"),
    mcp.WithString("query", mcp.Required()),
), searchHandler)

// At init time
host, _ := kit.New(ctx, &kit.Options{
    InProcessMCPServers: map[string]*kit.MCPServer{
        "docs": mcpSrv,
    },
})

// Or at runtime
n, _ := host.AddInProcessMCPServer(ctx, "docs", mcpSrv)
```

## Runtime skills and context files

Kit auto-discovers skills and `AGENTS.md`-style context files during `New()`,
but multi-tenant hosts (chatbots, web services, per-user agents) often need
to swap these **after** construction. The runtime mutators below recompose
the system prompt and apply it to the agent so the next turn picks up the
updated instructions — no restart, no file shuffling.

```go
// Add a programmatic skill — no file on disk required.
host.AddSkill(&kit.Skill{
    Name:        "polite-french",
    Description: "Respond in French and always greet the user.",
    Content:     "Always reply in French. Open every response with 'Bonjour'.",
})

// Or load one from disk.
host.LoadAndAddSkill("/var/skills/refund-policy.md")

// Project context (AGENTS.md equivalents): inline content from a DB...
host.AddContextFileContent(
    fmt.Sprintf("session://%s/AGENTS.md", userID),
    rulesFromDB,
)
// ...or load from disk.
host.LoadAndAddContextFile("/etc/agents/tenant-acme.md")

// Remove individually when a session ends.
host.RemoveSkill("polite-french")
host.RemoveContextFile(fmt.Sprintf("session://%s/AGENTS.md", userID))

// Or replace the whole set in one call.
host.SetSkills(activeSkillsForUser)
host.SetContextFiles(activeContextForUser)

// Inspect current state (snapshot copies — safe to mutate).
skills := host.GetSkills()
ctxFiles := host.GetContextFiles()
```

Key points:

- **Auto-refresh.** Every `Add*` / `Remove*` / `Set*` call recomposes the system
  prompt against the captured base prompt (preserving per-model overrides and
  `--system-prompt` resolution) and pushes the result onto the agent. Call
  `host.RefreshSystemPrompt()` only if you mutate state through a different
  path and need to force a re-render.
- **Dedup keys.** Skills dedupe by `Name`; context files dedupe by `Path`.
  Re-adding the same key replaces the entry instead of appending a duplicate.
- **Path is opaque.** `ContextFile.Path` does not have to point at a real file
  — it's only used for dedup and for the `Instructions from: <Path>` header
  injected into the prompt. URIs like `session://user-123/AGENTS.md` work fine.
- **Thread safety.** All readers and mutators are safe to call concurrently
  from multiple goroutines; the underlying state is guarded by an internal
  `RWMutex`.
- **Init-time options still apply.** `Options.Skills`, `Options.SkillsDir`,
  `Options.NoSkills`, and `Options.NoContextFiles` continue to control the
  startup set; the runtime API mutates from whatever state `New()` produced.
  See [SDK options](/sdk/options#skills--configuration).

## MCP prompts and resources

Query prompts and resources exposed by connected MCP servers:

```go
// List and expand prompts
prompts := host.ListMCPPrompts()
result, _ := host.GetMCPPrompt(ctx, "server", "prompt-name", map[string]string{"key": "value"})

// List and read resources
resources := host.ListMCPResources()
content, _ := host.ReadMCPResource(ctx, "server", "file:///path")
```

## MCP tasks (long-running tools)

Kit advertises [MCP task support](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)
during `initialize`, so cooperating servers can return a `taskId` immediately
and let Kit poll `tasks/get` / `tasks/result` until the operation completes.
This avoids HTTP/SSE proxy timeouts on long tools and gives you clean
cancellation via context.

```go
host, _ := kit.New(ctx, &kit.Options{
    MCPTaskMode: map[string]kit.MCPTaskMode{
        "build-server": kit.MCPTaskModeAlways,
    },
    MCPTaskProgress: func(p kit.MCPTaskProgress) {
        log.Printf("%s: %s", p.TaskID, p.Status)
    },
})

// Inspect / cancel in-flight tasks
tasks, _ := host.ListMCPTasks(ctx, "build-server")
_, _    = host.CancelMCPTask(ctx, "build-server", tasks[0].TaskID)
```

Defaults to `MCPTaskModeAuto` per server, so any existing MCP server keeps
its previous synchronous behaviour. See [SDK options → MCP Tasks](/sdk/options#mcp-tasks)
for the full surface.

## Context and compaction

Monitor and manage context usage:

```go
tokens := host.EstimateContextTokens()
stats := host.GetContextStats()

if host.ShouldCompact() {
    result, err := host.Compact(ctx, nil, "")
}
```

## In-process subagents

Spawn child Kit instances without subprocess overhead:

```go
result, err := host.Subagent(ctx, kit.SubagentConfig{
    Prompt:    "Analyze the test files",
    Model:     "anthropic/claude-haiku-3-5-20241022",
    NoSession: true,
    Timeout:   2 * time.Minute,
})
```

See [Options](/sdk/options), [Callbacks](/sdk/callbacks), and [Sessions](/sdk/sessions) for more details.

---

## SDK Sessions

# SDK Sessions

## Automatic persistence

By default, Kit automatically persists sessions to JSONL files. Multi-turn conversations retain context across calls:

```go
host.Prompt(ctx, "My name is Alice")
response, _ := host.Prompt(ctx, "What's my name?")
// response: "Your name is Alice"
```

## Accessing session info

```go
// Get the current session file path
path := host.GetSessionPath()

// Get the session ID
id := host.GetSessionID()

// Get the current model string
model := host.GetModelString()
```

## Configuring sessions via Options

Session behavior is configured at initialization:

```go
// Open a specific session file
host, _ := kit.New(ctx, &kit.Options{
    SessionPath: "./my-session.jsonl",
})

// Resume the most recent session for the current directory
host, _ := kit.New(ctx, &kit.Options{
    Continue: true,
})

// Ephemeral mode (no file persistence)
host, _ := kit.New(ctx, &kit.Options{
    NoSession: true,
})

// Custom session directory
host, _ := kit.New(ctx, &kit.Options{
    SessionDir: "/custom/sessions/",
})
```

## Clearing history

Clear the in-memory conversation history (does not delete the session file):

```go
host.ClearSession()
```

## Tree-based sessions

Kit's session model is tree-based, supporting branching. You can branch from any entry to explore alternate conversation paths:

```go
// Access the tree session manager
ts := host.GetTreeSession()

// Branch from a specific entry
err := host.Branch("entry-id-123")
```

## Listing and managing sessions

Package-level functions for session discovery:

```go
// List sessions for a specific directory
sessions := kit.ListSessions("/home/user/project")

// List all sessions across all directories
all := kit.ListAllSessions()

// Delete a session file
kit.DeleteSession("/path/to/session.jsonl")
```

## Custom session manager

For advanced use cases (databases, cloud storage, multi-user apps), implement the `SessionManager` interface to replace the default JSONL file backend:

```go
host, _ := kit.New(ctx, &kit.Options{
    SessionManager: myCustomSession,
})
```

The interface requires methods for message storage, branching, compaction, extension data, and lifecycle management. See the [SDK skill reference](https://github.com/mark3labs/kit) for the complete interface definition.

When using a custom `SessionManager`, the `SessionPath`, `Continue`, and `NoSession` options are ignored — your manager handles its own storage and session selection.

---

## Session Management

# Session Management

Kit uses a tree-based session model that supports branching and forking conversations.

## Session storage

Sessions are stored as JSONL (JSON Lines) files:

```
~/.kit/sessions/<cwd-path>/<timestamp>_<id>.jsonl
```

Path separators in the working directory are replaced with `--`. For example, `/home/user/project` becomes `home--user--project`.

Each line in the session file is a JSON entry representing a message, tool call, model change, or extension data. The tree structure allows branching from any message to explore alternate paths.

## Compaction

When conversations grow long, Kit can compact them to free up context window space. The compaction system:

- **Non-destructive**: Old messages remain on disk for history; only the LLM context is summarized
- **File tracking**: Tracks which files were read and modified across compactions
- **Split-turn handling**: Can summarize large single turns by splitting them
- **Tool result truncation**: Caps tool output during serialization to stay within token budgets

Use `/compact [focus]` to manually compact, or enable `--auto-compact` to compact automatically near the context limit.

## Auto-cleanup

Kit automatically cleans up empty sessions on shutdown and when using `/resume`. A session is considered empty if it has no messages beyond the initial system prompt. This prevents cluttering your sessions directory with unused files.

To start fresh without creating a session file at all, use ephemeral mode:

```bash
kit --no-session
```

## Resuming sessions

### Continue most recent

```bash
kit --continue
kit -c
```

### Interactive picker

Choose from previous sessions interactively:

```bash
kit --resume
kit -r
```

The session picker supports search, scope/filter toggles (all sessions vs. current directory), and session deletion. You can also open it during a session with the `/resume` slash command.

### Open a specific session

```bash
kit --session path/to/session.jsonl
kit -s path/to/session.jsonl
```

## Session commands

These slash commands are available during an interactive session:

| Command | Description |
|---------|-------------|
| `/name [name]` | Set or display the session's display name |
| `/session` | Show session info (path, ID, message count) |
| `/resume` | Open the session picker to switch sessions |
| `/export [path]` | Export session as JSONL (auto-generates path if omitted) |
| `/import <path>` | Import and switch to a session from a JSONL file |
| `/share` | Upload session to GitHub Gist and get a shareable viewer URL |
| `/tree` | Navigate the session tree |
| `/fork` | Fork to new session from an earlier message (creates new session file) |
| `/new` | Start a new session (creates new session file) |

## Ephemeral mode

Run without creating a session file:

```bash
kit --no-session
```

This is useful for one-off prompts, scripting, and subagent patterns where persistence isn't needed.

## Sharing sessions

The `/share` command uploads your session JSONL to GitHub Gist (via the `gh` CLI) and prints a shareable viewer URL:

```
/share
```

The shared session includes:
- The **system prompt** that was active during the conversation
- The **model** used (e.g., `anthropic/claude-sonnet-4-5`)

The viewer displays this information in a collapsible "System Prompt" section at the top of the session, with the model shown as a badge in the header.

The viewer is available at `https://go-kit.dev/session/#GIST_ID` and supports all message types including text, reasoning blocks, tool calls, images, and model changes.

You can also load any JSONL session via URL parameter: `https://go-kit.dev/session/?url=https://example.com/session.jsonl`

## Preferences persistence

Kit automatically saves your preferences across sessions to `~/.config/kit/preferences.yml`:

- **Theme** — Set via `/theme <name>`
- **Model** — Set via `/model <name>` or the model selector
- **Thinking level** — Set via `/thinking <level>` or Shift+Tab cycling

These preferences are restored on next launch. Precedence: CLI flag > config file > saved preference > default.

---

## Themes

# Themes

Kit ships with 22 built-in color themes and supports custom themes via YAML/JSON files or the extension API. Themes control all UI colors: input borders, popups, system messages, markdown rendering, syntax highlighting, and diff displays.

## Quick start

Switch themes at runtime with the `/theme` command:

```
/theme dracula
/theme catppuccin
/theme kitt
```

Run `/theme` with no arguments to list all available themes.

**Theme selections are automatically saved** to `~/.config/kit/preferences.yml` and restored on next launch. You don't need to add anything to your config file — just `/theme <name>` and it sticks. This persistence also applies to **model** and **thinking level** selections.

## Built-in themes

| Theme | Style |
|-------|-------|
| `kitt` | KITT-inspired reds and ambers (default) |
| `catppuccin` | Soothing pastels (Mocha/Latte) |
| `dracula` | Purple and cyan dark theme |
| `tokyonight` | Cool blues with warm accents |
| `nord` | Arctic, north-bluish palette |
| `gruvbox` | Retro groove colors |
| `monokai` | Classic syntax theme |
| `solarized` | Precision colors for machines and people |
| `github` | GitHub's light and dark palettes |
| `one-dark` | Atom One Dark |
| `rose-pine` | Soho vibes with muted tones |
| `ayu` | Simple with bright colors |
| `material` | Material Design palette |
| `everforest` | Green-focused comfortable theme |
| `kanagawa` | Dark theme inspired by Katsushika Hokusai |
| `amoled` | Pure black background, vivid accents |
| `synthwave` | Retro neon glows |
| `vesper` | Warm minimalist dark theme |
| `flexoki` | Inky reading palette |
| `matrix` | Green-on-black terminal aesthetic |
| `vercel` | Clean monochrome with blue accents |
| `zenburn` | Low-contrast, warm dark theme |

All themes support both light and dark terminal modes via adaptive colors.

## Custom theme files

Create a `.yml`, `.yaml`, or `.json` file with color definitions. Kit discovers themes from two directories:

| Location | Scope | Precedence |
|----------|-------|------------|
| `~/.config/kit/themes/` | User (global) | Overrides built-ins |
| `.kit/themes/` | Project-local | Overrides user and built-ins |

### Theme file format

A theme file defines adaptive color pairs with `light` and `dark` hex values. Any field left empty inherits from the default KITT theme.

```yaml
# ~/.config/kit/themes/my-theme.yml

# Core semantic colors
primary:
  light: "#8839ef"
  dark: "#cba6f7"
secondary:
  light: "#04a5e5"
  dark: "#89dceb"
success:
  light: "#40a02b"
  dark: "#a6e3a1"
warning:
  light: "#df8e1d"
  dark: "#f9e2af"
error:
  light: "#d20f39"
  dark: "#f38ba8"
info:
  light: "#1e66f5"
  dark: "#89b4fa"

# Text and chrome
text:
  light: "#4c4f69"
  dark: "#cdd6f4"
muted:
  light: "#6c6f85"
  dark: "#a6adc8"
very-muted:
  light: "#9ca0b0"
  dark: "#6c7086"
background:
  light: "#eff1f5"
  dark: "#1e1e2e"
border:
  light: "#acb0be"
  dark: "#585b70"
muted-border:
  light: "#ccd0da"
  dark: "#313244"

# Semantic roles
system:
  light: "#179299"
  dark: "#94e2d5"
tool:
  light: "#fe640b"
  dark: "#fab387"
accent:
  light: "#ea76cb"
  dark: "#f5c2e7"
highlight:
  light: "#e6e9ef"
  dark: "#181825"

# Diff backgrounds
diff-insert-bg:
  light: "#d5f0d5"
  dark: "#1a3a2a"
diff-delete-bg:
  light: "#f5d5d5"
  dark: "#3a1a2a"
diff-equal-bg:
  light: "#eceef3"
  dark: "#232336"
diff-missing-bg:
  light: "#e4e6eb"
  dark: "#1a1a2e"

# Code block backgrounds
code-bg:
  light: "#eceef3"
  dark: "#232336"
gutter-bg:
  light: "#e4e6eb"
  dark: "#1a1a2e"
write-bg:
  light: "#d5f0d5"
  dark: "#1a3a2a"

# Markdown and syntax highlighting
markdown:
  heading:
    light: "#1e66f5"
    dark: "#89b4fa"
  link:
    light: "#1e66f5"
    dark: "#89b4fa"
  keyword:
    light: "#8839ef"
    dark: "#cba6f7"
  string:
    light: "#40a02b"
    dark: "#a6e3a1"
  number:
    light: "#fe640b"
    dark: "#fab387"
  comment:
    light: "#9ca0b0"
    dark: "#6c7086"
```

### Partial themes

You only need to define the colors you want to change. Unspecified fields fall back to the default theme:

```yaml
# Just override the primary and accent colors
primary:
  dark: "#FF00FF"
accent:
  dark: "#00FFFF"
```

### Distributing themes

- **Personal**: Drop a file in `~/.config/kit/themes/`
- **Team/project**: Drop a file in `.kit/themes/` and commit it to version control
- **Override built-in**: Name your file the same as a built-in (e.g., `dracula.yml`) and it takes precedence

## Config file theme

You can also set theme colors directly in `.kit.yml`:

```yaml
theme:
  primary:
    light: "#8839ef"
    dark: "#cba6f7"
  error:
    dark: "#FF0000"
```

Or reference an external theme file:

```yaml
theme: "./themes/my-custom-theme.yml"
```

## Extension theme API

Extensions can register and switch themes programmatically at runtime.

### Registering a theme

```go
api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) {
    ctx.RegisterTheme("neon", ext.ThemeColorConfig{
        Primary:    ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
        Secondary:  ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"},
        Success:    ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
        Warning:    ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"},
        Error:      ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"},
        Info:       ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"},
        Text:       ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"},
        Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"},
        MdKeyword:  ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"},
        MdString:   ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"},
        MdComment:  ext.ThemeColor{Light: "#888888", Dark: "#555555"},
    })
})
```

### Switching themes

```go
err := ctx.SetTheme("dracula")
```

### Listing available themes

```go
names := ctx.ListThemes()
```

### ThemeColorConfig fields

| Field | Description |
|-------|-------------|
| `Primary` | Main brand/accent color |
| `Secondary` | Secondary accent |
| `Success` | Success states |
| `Warning` | Warning states |
| `Error` | Error/critical states |
| `Info` | Informational states |
| `Text` | Primary text |
| `Muted` | Dimmed text |
| `VeryMuted` | Very dimmed text |
| `Background` | Base background |
| `Border` | Panel borders |
| `MutedBorder` | Subtle dividers |
| `System` | System messages |
| `Tool` | Tool-related elements |
| `Accent` | Secondary highlight |
| `Highlight` | Highlighted regions |
| `MdHeading` | Markdown headings |
| `MdLink` | Markdown links |
| `MdKeyword` | Syntax: keywords |
| `MdString` | Syntax: strings |
| `MdNumber` | Syntax: numbers |
| `MdComment` | Syntax: comments |

Each field is an `ext.ThemeColor` with `Light` and `Dark` hex strings. Empty fields inherit from the default theme.

## Precedence order

When multiple sources define the same theme name, later sources win:

1. Built-in presets (lowest)
2. User themes (`~/.config/kit/themes/`)
3. Project-local themes (`.kit/themes/`)
4. Extension-registered themes (highest)

### Startup theme resolution

At startup, Kit determines which theme to apply:

1. **`.kit.yml` `theme:` key** — explicit config always wins (highest priority)
2. **`~/.config/kit/preferences.yml`** — persisted `/theme` selection
3. **Default `kitt` theme** — fallback

The preferences file is updated automatically whenever you use `/theme` or `ctx.SetTheme()`. It is separate from `.kit.yml` so it never clobbers your config comments or formatting.

Theme changes via `/theme` or `ctx.SetTheme()` take effect immediately on all UI elements, including previously rendered messages.
