kit

Go SDK

SDK guides

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

Installation

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

Basic usage

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.

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
WithProviderWire(string) Options.ProviderWire (wire protocol for auto-routed providers: openai, openai-compat, anthropic, google)
WithProvider(string, ProviderFactory) Adds one entry to Options.Providers (see Custom provider backends)
WithConfigFile(string) Options.ConfigFile (also clears SkipConfig)
WithDebug() Options.Debug = true
WithDebugLogger(DebugLogger) Options.DebugLogger (route engine + MCP debug output into a custom logger; overrides WithDebug when set)
Ephemeral() Options.NoSession = true
Isolated() Turns off ambient discovery and side effects (see Isolated agents)
WithConfig() Options.SkipConfig = false
WithContextFiles() Options.NoContextFiles = false
WithSkills(...string) Options.NoSkills = false; sets Options.Skills when paths are given
WithExtensions() Options.NoExtensions = false
WithAgents() Options.NoAgents = false
WithSessions() Options.NoSession = false
WithCoreTools(...string) Options.DisableCoreTools = false; sets Options.CoreToolList when names are given

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.
kit.NewIsolatedAgent(ctx, ...Option) You embed Kit and want nothing from the host (config, AGENTS.md, skills, extensions, sessions, core tools) unless you turn it on. See Isolated agents.

Isolated agents

By default the SDK discovers things on the host, as the CLI does: .kit.yml (and the MCP servers it declares), AGENTS.md, skills, extensions and named agents. It also writes session files and enables the file and shell tools. An application that embeds Kit usually does not want this.

kit.Isolated() sets SkipConfig, NoContextFiles, NoSkills, NoExtensions, NoAgents, NoSession and DisableCoreTools. kit.NewIsolatedAgent(ctx, ...) is NewAgent with Isolated() applied first. Options apply in order, so the With* opt-ins in the table above turn features back on one at a time:

host, err := kit.NewIsolatedAgent(ctx,
    kit.WithModel("anthropic/claude-sonnet-4-5-20250929"),
    kit.WithCoreTools("read", "grep"), // only these core tools
    kit.WithSessions(),                // persist sessions
)

KIT_* environment variables still apply. Subagents of an isolated Kit are isolated in the same way. Isolated() does not set Options.Bare: Bare is one switch for all discovery, while Isolated() uses the separate fields so you can turn each one back on.

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.

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:

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 (model, tools, thinking level, provider creds)
PromptResult(ctx, message) Returns full TurnResult with usage stats
PromptResultWithOptions(ctx, message, opts) Per-call options variant that returns the full TurnResult
PromptResultWithFiles(ctx, message, files) Multimodal with file attachments
Steer(ctx, instruction) System-level steering without user message
FollowUp(ctx, text) Send a follow-up user message ("Continue." if empty)
ContinueResult(ctx) Generate from saved conversation without adding a user message; returns full TurnResult

ContinueResult uses the installed session’s current branch. Empty or system-only conversations return an error. Normal hooks, events, checkpoints, streaming, and cancellation apply. BeforeTurn receives an empty prompt; prompt replacements have no effect, but explicit message injections still apply. The host must resolve pending tool calls and verify uncertain external effects before continuation. Saved tool calls are not replayed; new model-generated tool calls run normally.

Per-call overrides

PromptOptions scopes configuration to a single call and restores the agent’s prior state afterwards — no need to rebuild a *Kit per request. This suits multi-tenant hosts that resolve the model, credentials, or tool set per request:

result, err := host.PromptResultWithOptions(ctx, "Summarise this ticket", kit.PromptOptions{
    SystemMessage:  "You are a concise triage assistant.", // prepended for this call
    Model:          "anthropic/claude-haiku-3-5-20241022", // overrides the default model
    ThinkingLevel:  "low",                                 // "off" | "low" | "medium" | "high"
    ExtraTools:     []kit.Tool{lookupTool},                // added on top of the core set
    ProviderURL:    "https://proxy.tenant-a/v1",           // per-tenant endpoint
    ProviderAPIKey: tenantKey,                             // per-tenant credential
})

Every field is optional; a zero value means “use the agent’s default.” The prior model, thinking level, provider credentials, and tool set are all restored before the call returns, and concurrent option-driven prompts are serialized so the apply/restore window of one call never races another.

Custom tools

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

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:

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.

Options.ExtraTools fixes the native tool set at construction time. To add or remove native tools on a live host, see Runtime native tools.

Schema-driven tools

When the tool’s input shape isn’t known at compile time — tools sourced from JSON Schema definitions in skill files, MCP server catalogs, or user-supplied definitions — use kit.NewRawTool. It takes a JSON Schema and a handler that receives the decoded arguments as a map[string]any, so no Go input type is required:

schema := map[string]any{
    "type": "object",
    "properties": map[string]any{
        "city": map[string]any{"type": "string", "description": "City name"},
    },
    "required": []any{"city"},
}

weatherTool := kit.NewRawTool("get_weather", "Get current weather for a city", schema,
    func(ctx context.Context, args map[string]any) (kit.ToolOutput, error) {
        return kit.TextResult("72°F, sunny in " + args["city"].(string)), nil
    },
)

The schema is advertised to the model as the tool’s parameter schema. If the model sends arguments that aren’t a valid JSON object, the call short-circuits with an error result before your handler runs.

Halting the agent loop

For structured-result patterns — the model calls a finish(...) tool with a typed argument and the loop should terminate, returning that value to the caller — set Halt and FinalValue on the returned ToolOutput instead of smuggling the value out through a side-channel:

finishTool := kit.NewTool("finish", "Return the final structured answer",
    func(ctx context.Context, input AnswerInput) (kit.ToolOutput, error) {
        return kit.ToolOutput{
            Content:    "done",
            Halt:       true,       // terminate the agent loop after this call
            FinalValue: input,      // surfaced to the caller
        }, nil
    },
)

result, _ := host.PromptResult(ctx, "Extract the order details")
if result.HaltedByTool == "finish" {
    answer := result.FinalValue.(AnswerInput) // the typed value your handler stored
    _ = answer
}

TurnResult.HaltedByTool names the tool that halted the turn (empty if the turn ended for any other reason), and TurnResult.FinalValue carries whatever your handler placed in ToolOutput.FinalValue. Halt/FinalValue work with NewTool, NewParallelTool, and NewRawTool alike.

Generation & provider overrides

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

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",            // model-specific effort; see SupportedThinkingLevels
    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 for the full field reference, including TopP, TopK, FrequencyPenalty, PresencePenalty, and TLSSkipVerify.

Custom provider backends

A kit.ProviderFactory serves a provider name from your own Go code. Model strings name/<model> then go to your factory instead of a built-in provider. Use it to ship an agentic application with its own inference backend (for example an in-process local runtime), or to plug in a test double or a proxy. Kit itself takes no dependency on the backend.

factory := func(ctx context.Context, cfg *kit.ProviderConfig, model string) (*kit.ProviderResult, error) {
    m, err := backend.LanguageModel(ctx, model) // backend is any kit.LLMProvider
    if err != nil {
        return nil, err
    }
    return &kit.ProviderResult{
        Model:  m,       // kit.LLMLanguageModel
        Closer: release, // optional io.Closer
    }, nil
}

// For one Kit instance (its subagents inherit it):
host, err := kit.New(ctx, &kit.Options{
    Model:     "local/qwen3-8b",
    Providers: map[string]kit.ProviderFactory{"local": factory},
})

// Or for every Kit instance in the process:
if err := kit.RegisterProvider("local", factory); err != nil {
    return err
}
host, err = kit.NewAgent(ctx, kit.WithModel("local/qwen3-8b"))
API Scope Notes
Options.Providers / WithProvider One Kit and its subagents Highest precedence. kit.New copies the map.
RegisterProvider / UnregisterProvider / RegisteredProviders Every Kit in the process Wins over the built-in providers. A nil factory removes the name.

Behaviour:

Event system

Subscribe to events for monitoring:

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 and inspect the registry:

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

// Advisory list of known models — each entry is a kit.ModelInfoEntry
// (Provider, ModelID, Name, ContextLimit, OutputLimit, Reasoning, Pricing).
// Models not in the registry can still be used by provider/model string.
models := host.GetAvailableModels()
for _, m := range models {
    if m.Reasoning {
        fmt.Printf("%s/%s context=%d\n", m.Provider, m.ModelID, m.ContextLimit)
    }
}

// Prefer a chain of models; first available wins.
result := kit.ResolveModelChain([]string{
    "anthropic/claude-sonnet-4-5-20250929",
    "openai/gpt-4o",
})
if result.Error == "" {
    _ = host.SetModel(ctx, result.Model)
}

Each ModelInfoEntry and ModelCapabilities carries a Pricing field with per-million-token costs from the registry:

caps, errStr := kit.GetModelCapabilities("anthropic/claude-sonnet-4-5-20250929")
if errStr == "" && caps.Pricing.Known {
    fmt.Printf("in $%.2f/1M  out $%.2f/1M\n", caps.Pricing.Input, caps.Pricing.Output)
}

Check Pricing.Known before using any rate. It is false for local models and custom OpenAI-compatible endpoints, whose rates are all zero — otherwise an unpriced model looks identical to a free one. Cache rates are similarly guarded by HasCacheRead / HasCacheWrite.

Long-context pricing tiers

Many long-context models bill roughly 2x the base rate once a request’s prompt passes a threshold (commonly 200,000 tokens). The registry surfaces this on ModelInfo.Cost.Tiers, and Cost.RatesFor(promptTokens) returns the rates that apply to a given prompt size.

info := kit.LookupModel("google", "gemini-3.1-pro-preview")
if info != nil {
    base := info.Cost.RatesFor(50_000)     // small prompt: base rate
    long := info.Cost.RatesFor(500_000)    // long prompt: tier rate
    fmt.Printf("small: $%.2f/1M in\nlong:  $%.2f/1M in\n", base.Input, long.Input)
}

Pass the whole prompt — including cached tokens — to RatesFor; the tier is selected on the context sent to the model, so leaving cache reads out would under-report the applicable rate. When several tiers match, the highest threshold wins. Models with flat pricing have an empty Tiers slice and RatesFor returns the base rates unchanged.

Kit’s built-in usage tracker applies this automatically for interactive sessions. Reach for RatesFor yourself when computing costs from raw usage counts outside the tracker.

Reasoning support per model

Providers differ on which reasoning levels a model accepts: OpenAI’s gpt-5.x line accepts "none" but not "minimal", the o-series accepts neither, and Anthropic’s budget-based models accept every level. The catalog carries this per model, so consumers can query it rather than guessing from the model name. Catalog choices preserve exact spelling and can include xhigh, max, and future names outside the default ladder. off omits effort; none is a literal provider effort. Explicit unsupported SDK selections on graded models return an error. Custom lists are documented in configuration.

// The default ladder, not an exhaustive catalog vocabulary.
levels := kit.ThinkingLevels()
// ["off", "none", "minimal", "low", "medium", "high", "xhigh"]

// Narrow to what a specific model accepts.
supported := kit.SupportedThinkingLevels("openai", "o3")
// ["off", "low", "medium", "high"] — no "none" or "minimal"

// Predicate form.
ok := kit.IsThinkingLevelSupported("openai", "gpt-5.4", "minimal") // false

// Substitute an unsupported level with the nearest supported one of similar
// cost. Returns the input unchanged when it is already supported, or "off"
// when the model rejects every level.
safe := kit.SuggestThinkingLevel("openai", "o3", "minimal") // "low"

On a live agent, (*Kit).SupportedThinkingLevels() uses the currently selected model:

levels := host.SupportedThinkingLevels()

Unknown-capability models pass named efforts through metadata validation; provider adapters may still reject values they cannot encode. For graded models, SetThinkingLevel requires exact membership and returns an error without changing the current selection. Use SuggestThinkingLevel to choose a supported substitute. Inherited model-switch values and persisted preferences may fall back to off when an adapter cannot represent them; explicit CLI/SDK requests remain strict.

Deprecation status

ModelInfo.Status carries the catalog’s lifecycle marker ("deprecated", "beta", or empty for GA). info.IsDeprecated() is the convenient predicate. Deprecated models usually still answer requests, so this is advisory rather than a block — useful for a model picker that wants to warn or hide retiring models.

for _, m := range host.GetAvailableModels() {
    info := kit.LookupModel(m.Provider, m.ModelID)
    if info != nil && info.IsDeprecated() {
        fmt.Printf("warning: %s/%s is deprecated\n", m.Provider, m.ModelID)
    }
}

kit.SuggestModels now sorts deprecated matches to the end and returns a stable order, so a five-item suggestion list is not spent on models the provider is retiring.

One-shot completions

ExecuteCompletion runs a single LLM call outside the agent loop — useful for summaries, classifiers, or any side request that should not touch the session or tools. When CompleteRequest.Model is empty the current agent model is reused (no extra provider setup); set it to spin up a temporary provider that is closed when the call returns. The model can also name a custom provider backend.

resp, err := host.ExecuteCompletion(ctx, kit.CompleteRequest{
    // Model: "openai/gpt-4o-mini", // optional override
    System: "You are a terse classifier.",
    Prompt: "Is this a bug report? Reply yes or no.\n\n" + userText,
    MaxTokens: 16,
})
if err != nil {
    return err
}
fmt.Println(resp.Text, resp.InputTokens, resp.OutputTokens, resp.Model)

// Streaming: set OnChunk to receive text deltas as they arrive.
_, err = host.ExecuteCompletion(ctx, kit.CompleteRequest{
    Prompt: "Write a haiku about terminals.",
    OnChunk: func(chunk string) { fmt.Print(chunk) },
})

Mid-turn steering

InjectSteer queues a user message that is injected between agent steps while a turn is active (after the current tool finishes, before the next LLM call). The injected message stays in the conversation for all later steps of the turn and is saved to the session. If no turn is running, the message is kept and injected at the first step of the next turn. Use Prompt / Steer to start a new turn from the idle state. Messages that no step consumed (for example, the turn ended with a text-only response) can be reclaimed with DrainSteer.

go func() {
    // e.g. from a UI cancel/redirect button
    if host.IsGenerating() {
        host.InjectSteer("Stop exploring and summarise what you found.")
        // host.InjectSteerWithFiles(msg, []kit.LLMFilePart{...}) for images
    }
}()

// After the turn completes, reclaim anything that arrived too late to inject:
for _, msg := range host.DrainSteer() {
    fmt.Println("unconsumed steer:", msg.Text)
}

Filtering core tools

Options.CoreToolList accepts an explicit allow-list of core tool names. Build it from include/exclude filters with FilterCoreToolNames (nil means “all core tools”).

// Keep only read-only tools:
list, err := kit.FilterCoreToolNames(
    []string{"read", "grep", "find", "ls"}, // include
    nil,                                    // exclude
)
if err != nil {
    return err
}

// Or drop bash and write:
list, err = kit.FilterCoreToolNames(nil, []string{"bash", "write"})

host, err := kit.New(ctx, &kit.Options{
    CoreToolList: list,
})

At most one of include/exclude may be non-empty. Unknown names are skipped with a warning. DisableCoreTools: true is still the right switch for chat-only hosts that need zero core tools.

Dynamic MCP servers

Add and remove MCP servers at runtime:

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

if err := host.RemoveMCPServer("github"); err != nil {
    return err
}
servers := host.ListMCPServers() // []kit.MCPServerStatus

If mandatory tool discovery fails, AddMCPServer returns zero tools and an error. Kit automatically closes and removes only the failed connection; no new registrations remain. Healthy servers stay available, so you do not need Kit.Close to clean up a failed add. You can retry with the same name. Removing an absent server (including one removed by this rollback) returns an error. A successful add with no tools, including fully filtered tools, remains listed with ToolCount: 0 and can be removed. Optional prompts and resources discovery is best-effort and does not fail the add.

In-process MCP servers

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

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 native tools

Options.Tools / Options.ExtraTools freeze the native Go tool set at construction time. For progressive disclosure (loading a domain’s tools only when the model asks for them) or multi-tenant hosts that swap tool catalogs per request, mutate the native tool set on a live host — mirroring the runtime MCP and skill APIs. No host rebuild, so session history, MCP connections, and the system-prompt snapshot all survive.

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
    },
)

// Add tools that persist for the session (visible on the next turn).
host.AddTools(weatherTool)

// Drop tools by name when a domain is no longer needed.
if err := host.RemoveTools("get_weather"); err != nil {
    log.Printf("remove tools: %v", err)
}

// Replace the entire native extra-tool set in one call.
host.SetExtraTools(activeToolsForUser...)

// Inspect the current set (snapshot copy — safe to mutate).
extra := host.GetExtraTools()

Key points:

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. The next prepared request uses the new metadata and activation schema, including changes completed in OnPrepareStep in the same turn — no restart, no file shuffling.

Registered Skill.Content is authoritative, even when empty, pathless, or different from disk. Built-in activation does not re-read Path. Use ReloadSkills or LoadAndAddSkill to select new disk bytes. Explicit /name activation keeps its disk-freshness behavior. Kit copies values, Metadata and Tags on registration and GetSkills reads.

Each request freezes sorted, unique eligible names and selected content/path. Removed and disabled names are hidden. An empty catalogue omits activate_skill; adding a skill from zero restores it on the next request. Stale tool selections return a fresh-request error, never replacement bytes. Repeats of the same name/body/path are acknowledged; a changed version returns its body again in the same turn. Removal does not erase past conversation content. Bundled resources remain filesystem-based and best-effort.

// 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))

// Hide a skill from the model-facing catalog without unloading it — it stays
// available for explicit /<name> activation. EnableSkill reverses this.
host.DisableSkill("refund-policy")
host.EnableSkill("refund-policy")

// 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()
prompt := host.GetSystemPrompt() // the composed prompt the agent sends

Key points:

MCP prompts and resources

Query prompts and resources exposed by connected MCP servers:

// 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 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.

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 for the full surface.

Context and compaction

Monitor and manage context usage:

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

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

Token estimates count every message part (tool-call arguments, tool results, reasoning, file attachments), and after the first turn the real API-reported token count is preferred over the heuristic. Compaction budgets adapt to the model’s context and output limits when left at their zero values; pass a *kit.CompactionOptions as the second argument to Compact to override them — see SDK options → CompactionOptions.

Estimates can still undercount. When a provider call fails with a context-overflow error, the turn loop automatically compacts and replays the turn once before surfacing kit.ErrContextOverflow — see Reactive compaction.

Provider error classification

Provider failures are wrapped with exported sentinels so you can branch on the failure category with errors.Is instead of string-matching the underlying HTTP error. PromptResult / Prompt already return classified errors; you can also classify any provider error yourself with kit.ClassifyProviderError:

_, err := host.PromptResult(ctx, prompt)
switch {
case errors.Is(err, kit.ErrContextOverflow):
    // Kit already compacted and replayed the turn once before surfacing
    // this — reaching here means the conversation cannot fit even after
    // compaction (e.g. start a new session or trim input).
    handleUnrecoverableOverflow()
case errors.Is(err, kit.ErrRateLimit):
    backoffAndRetry()
case errors.Is(err, kit.ErrAuth):
    rePromptForKey()
case errors.Is(err, kit.ErrProviderUnavailable):
    retryLater()
case errors.Is(err, kit.ErrInvalidRequest):
    log.Printf("non-retryable: %v", err)
}
Sentinel Meaning
kit.ErrContextOverflow Request exceeded the model’s context window — surfaced only after Kit’s automatic compact-and-replay recovery also failed
kit.ErrRateLimit Provider throttled the request
kit.ErrAuth Credential / authorization failure
kit.ErrProviderUnavailable Transient upstream failure (5xx, network, timeout)
kit.ErrInvalidRequest Structurally invalid request — retrying won’t help

The original error stays reachable via errors.Is, so you never lose the provider’s detail message.

Graceful shutdown

Close() releases MCP connections, model resources, and the session file handle. When shutdown must be bounded by a deadline, use CloseContext:

shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := host.CloseContext(shutdownCtx); err != nil {
    log.Printf("shutdown: %v", err)
}

Close() is equivalent to CloseContext(context.Background()).

In-process subagents

Spawn child Kit instances without subprocess overhead:

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,
})

Set Agent to a named agent definition (discovered from .agents/agents/*.md, .kit/agents/*.md, ~/.config/kit/agents/*.md, or the built-ins general / explore) to apply its preset system prompt, model, tool allowlist, and timeout:

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

Session-backed runs are linked to the parent: new child sessions record the parent’s session ID in their header, and result.SessionID can be passed back as SubagentConfig.SessionID to resume the child session for follow-up prompts that reuse its accumulated context.

Inspect active and finished child runs with host.SubagentRuns() or look up one execution with host.GetSubagentRun(runID). Each SubagentRun includes status, session links, and retained child events. host.RunningSubagents() still lists only active runs; pass a run ID, not a session ID, to host.KillSubagent(runID) to stop one. See Retained run snapshots for API examples and history limits.

See Subagents for definition file format and discovery precedence.

See Options, Callbacks, and Sessions for more details.

Model modes and code mode are described in SDK options. Isolation switches are not an OS sandbox.