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 (`.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 conditionals content := `Hi ClaudeHi there` 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" ```