The pkg/kit package lets you embed Kit as a library in your Go applications.
go get github.com/mark3labs/kit/pkg/kit
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)
}
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.
| 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. |
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.
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"
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"
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.
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.
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:
json:"name" — parameter namedescription:"..." — description shown to the LLMenum:"a,b,c" — restrict valid valuesomitempty — marks the parameter as optionalReturn 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.
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.
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.
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.
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:
/ as the model name, so
local/org/model.gguf gives org/model.gguf.openai. Names are
case-insensitive and must not contain /. kit.New rejects a nil factory
and two names that differ only in case.SetModel, for
ExecuteCompletion with a Model, and for subagents. Cache expensive
resources inside the factory.cfg carries the Kit’s generation settings and max tokens (for
ExecuteCompletion, the request’s MaxTokens when it is set). It carries ProviderAPIKey,
ProviderURL and ProviderWire only when those overrides belong to the
factory’s provider.ProviderResult.Closer when the model is replaced, when the Kit
closes, and when the factory returns an error. Release resources that all
models share after you close every Kit.ProviderResult.ProviderOptions if your backend needs options.CompactionOptions.ContextWindow if
you use auto-compaction.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)
})
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.
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.
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.
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.
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) },
})
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)
}
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.
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.
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)
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:
extraTools only. These methods manage the same slice as
Options.ExtraTools. Core tools, MCP tools, and extension-registered tools are
never touched, and GetExtraTools excludes extension tools.AddTools replaces any existing extra tool that
shares a Info().Name, then appends the rest. Duplicate names within a single
call also resolve to the last one provided.RemoveTools is atomic. If any supplied name is not currently registered,
it returns an error listing the missing names (deduped and sorted) and leaves
the tool set unchanged.PromptOptions.ExtraTools
still layers on top for a single call and is reverted afterwards, snapshotting
around whatever persistent set is active.RWMutex.Options.ExtraTools.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:
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.Name; context files dedupe by Path.
Re-adding the same key replaces the entry instead of appending a duplicate.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.RWMutex.Options.Skills, Options.SkillsDir,
Options.SkillsDisable, Options.SkillTrustPrompt, Options.NoSkills, and
Options.NoContextFiles continue to control the startup set; the runtime API
mutates from whatever state New() produced.
See SDK options.Skills/SkillsDir are given,
New() scans four agentskills.io
locations: ~/.agents/skills/, ~/.config/kit/skills/,
<project>/.agents/skills/, and <project>/.kit/skills/. Symlinked skill
directories and files are followed; broken links are skipped. Project-level
skills override user-level skills of the same name. Skills missing a
description are skipped with a logged warning, and a skill’s
disable-model-invocation: true (or Options.SkillsDisable) hides it from
the catalog while keeping it available for explicit activation.kit.Skill exposes BaseDir() (its directory),
Resources() (the files bundled under scripts/, references/, and
assets/, which power the <skill_resources> enumeration shown when a skill
is activated), Scope() ("project" or "user"), and Validate() /
Warnings(), which return []kit.SkillDiagnostic describing agentskills.io
spec deviations (name format, length limits, name/directory mismatch, body
over 500 lines). Only a missing name or description is an "error";
everything else is a "warning" and the skill still loads.kit.LoadUserSkills() and kit.LoadProjectSkills(cwd)
return the raw per-scope sets; kit.CombineSkills(user, project) validates,
drops error-level skills, and resolves name collisions (project wins). This
is what kit skill list uses so it can show scope without prompting for
trust./<skill-name> [args]
passed to Prompt/Run is expanded before the turn starts: the skill file
is re-read, its body wrapped in <skill_content name=… location=…> with a
<skill_resources> listing, and the args appended. kit.ParseSkillCommand
performs the syntactic split if a host wants to do its own routing.fs.FS-backed discovery. The package-level loaders kit.LoadSkill,
kit.LoadSkillsFromDir, and kit.LoadSkills are path-string based;
kit.LoadSkillsFromFS(fsys, root) is the fs.FS-typed counterpart for
embed.FS distribution, fstest.MapFS tests, or per-tenant virtual
filesystems. Feed the result into host.SetSkills(...):
//go:embed skills
var skillsFS embed.FS
loaded, _ := kit.LoadSkillsFromFS(skillsFS, "skills")
host.SetSkills(loaded)
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")
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.
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 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.
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()).
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.