Files
kit/pkg/kit/README.md
T
Ed Zynda 7a04bdfeba feat(kit): isolate viper config per Kit instance + add NewAgent (#42)
* feat(kit): isolate viper config per Kit instance + add NewAgent (#40)

- Give each kit.New()/NewAgent() call an isolated *viper.Viper store so
  multiple Kit instances in one process no longer clobber each other's
  config; runtime mutators (SetModel, SetThinkingLevel) touch only the
  owning instance, making subagent spawning and multi-Kit embedding
  race-free
- Thread the per-instance store through internal/config, internal/models
  (ProviderConfig.ConfigStore), internal/kitsetup, and the extension
  runner, with a nil -> process-global fallback so the CLI is unaffected
- Share the global store when Options.CLI != nil to preserve cobra flag
  bindings (also opted in for internal/acpserver)
- Remove viperInitMu; preserve the tri-state IsSet precedence contract
  and sdkDefaultMaxTokens floor
- Add ergonomic NewAgent + functional options (WithModel, WithStreaming,
  Ephemeral, etc.); NewAgent defaults streaming on, opt out via
  WithStreaming(false). New(ctx, *Options) behavior is unchanged
- Add config-isolation regression test and NewAgent/option coverage;
  document NewAgent and per-instance isolation in README

Fixes #40

* docs(sdk): document NewAgent options and per-instance config isolation

- Add "Functional options (NewAgent)" and "Per-instance config isolation"
  sections to the docs site SDK overview, with an options table and a
  "when to use which" constructor comparison
- Cross-reference NewAgent from the SDK options page and correct the now
  per-instance ProviderAPIKey precedence wording
- Document NewAgent + With* helpers and config isolation in pkg/kit/README
  and list NewAgent/Option in the API reference
- Show the NewAgent constructor in the SDK examples getting-started snippet

* fix(kit): correct config loading and isolate ACP sessions

- Isolate each ACP session's config store instead of sharing the global
  viper, preventing per-session SetModel/SetThinkingLevel races; seed the
  root-command flag values (model, thinking-level, provider URL/key) so
  `kit acp -m <model>` is still honored
- Run initConfig for isolated SDK stores by gating on opts.CLI instead of
  v.GetString("model"), which setSDKDefaults always populates and thus
  skipped .kit.yml / KIT_* loading for SDK callers
- Configure KIT_* env overrides unconditionally in initConfig so passing an
  explicit config file no longer disables environment variable support
- Wrap config unmarshal/validate errors with %w to preserve the error chain

* fix(kit): make Options.Streaming a *bool to honor unset

- Change Options.Streaming from bool to *bool so a zero-valued Options no
  longer forces stream=false; New only sets the key when non-nil, letting
  streaming resolve through the precedence chain (env -> config -> default
  true). This also fixes the CLI path, which never set the field
- Mirror the existing sampling-parameter pointer pattern instead of adding
  a separate StreamingSet sentinel, keeping Options internally consistent
- Update WithStreaming/NewAgent, subagent, and ACP callers to the pointer
  form; add regression tests for the nil-default and explicit opt-out paths
- Update SDK docs (README, pkg/kit/README, options page) with the ptrBool
  helper and *bool semantics

* fix(kit): inherit parent provider config in subagents

- Copy the parent's effective provider/runtime config (API key, URL,
  TLS, thinking level, max-tokens, samplers) onto child Options in
  Kit.Subagent. After the per-instance viper isolation, the child's
  isolated store only re-loaded .kit.yml / KIT_*, silently dropping
  config the parent set via programmatic Options or runtime setters
  like SetThinkingLevel
- Preserve the IsSet tri-state for max-tokens and samplers so per-model
  defaults still apply on the child when the parent left them unset
- Add TestInheritProviderConfig covering propagation, unset keys, and
  nil-safety
2026-06-02 14:41:35 +03:00

14 KiB

KIT SDK

The KIT SDK (pkg/kit) lets you embed Kit's full agent capabilities — LLM interactions, tool execution, session management, streaming, hooks — into any Go application.

Installation

go get github.com/mark3labs/kit

Basic Usage

package main

import (
    "context"
    "fmt"
    "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 func() { _ = host.Close() }()

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

    fmt.Println(response)
}

Configuration

The SDK behaves identically to the CLI:

  • Loads configuration from ~/.kit.yml by default
  • Creates default configuration if none exists
  • Respects all environment variables (KIT_*)
  • Uses the same defaults as the CLI

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 without external synchronization.

Functional options (NewAgent)

For simple programmatic setups, kit.NewAgent is 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
)

Helpers: WithModel, WithSystemPrompt, WithStreaming, WithMaxTokens, WithThinkingLevel, WithTools, WithExtraTools, WithProviderAPIKey, WithProviderURL, WithConfigFile, WithDebug, and Ephemeral. Option is a plain func(*Options), so you can define your own. For fields without a With* helper (MCPConfig, InProcessMCPServers, SessionManager, MCP task tuning) construct an Options value and call kit.New.

Options

You can override specific settings:

host, err := kit.New(ctx, &kit.Options{
    Model:        "ollama/llama3",            // Override model
    SystemPrompt: "You are a helpful bot",    // Override system prompt
    ConfigFile:   "/path/to/config.yml",      // Use specific config file
    MaxSteps:     10,                         // Override max steps
    Streaming:    ptrBool(true),               // *bool: nil = unset (default true), &false = off
    Quiet:        true,                       // Suppress debug output

    // Session options
    SessionPath:  "./session.jsonl",          // Open specific session
    Continue:     true,                       // Resume most recent session
    NoSession:    true,                       // Ephemeral mode

    // Tool options
    Tools:            []kit.Tool{kit.NewBashTool()}, // Replace default tool set
    ExtraTools:       []kit.Tool{myTool},            // Add alongside defaults
    DisableCoreTools: true,                        // Use no core tools (0 tools)

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

    // Compaction
    AutoCompact:  true,                       // Auto-compact near context limit

    // In-process MCP servers (map name → *kit.MCPServer)
    InProcessMCPServers: map[string]*kit.MCPServer{
        "docs": mcpSrv,
    },
})

Advanced Usage

With Tool Callbacks

Monitor tool execution in real-time:

unsub := host.OnToolCall(func(e kit.ToolCallEvent) {
    fmt.Printf("Calling tool: %s\n", e.ToolName)
})
defer unsub()

unsub2 := host.OnToolResult(func(e kit.ToolResultEvent) {
    if e.IsError {
        fmt.Printf("Tool %s failed: %s\n", e.ToolName, e.Result)
    } else {
        fmt.Printf("Tool %s succeeded\n", e.ToolName)
    }
})
defer unsub2()

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

response, err := host.Prompt(
    ctx,
    "List files in the current directory",
)

Dynamic MCP Server Management

Add, remove, and list MCP servers at runtime:

// Add an MCP server at runtime
n, err := host.AddMCPServer(ctx, "github", kit.MCPServerConfig{
    Command: "npx",
    Args:    []string{"-y", "@modelcontextprotocol/server-github"},
})
fmt.Printf("Loaded %d tools from MCP server\n", n)

// List connected MCP servers
for _, s := range host.ListMCPServers() {
    fmt.Printf("%s: %d tools\n", s.Name, s.ToolCount)
}

// Disconnect a server and remove its tools
host.RemoveMCPServer("github")

In-Process MCP Servers

Register mcp-go servers that run in the same process — no subprocess spawning, no network I/O. This is ideal for custom tool servers implemented in Go:

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

// Create an mcp-go server with tools
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)

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

// Option 2: At runtime
n, err := host.AddInProcessMCPServer(ctx, "docs", mcpSrv)
fmt.Printf("Loaded %d tools from in-process server\n", n)

Kit does not take ownership of the server's lifecycle — the caller is responsible for any cleanup. In-process server tools are prefixed the same way as external MCP servers (e.g. "docs__search_docs").

MCP Prompts

MCP servers can expose prompt templates via the MCP prompts capability. Kit exposes these through the SDK:

// List prompts from all connected MCP servers
prompts := host.ListMCPPrompts()
for _, p := range prompts {
    fmt.Printf("%s/%s: %s\n", p.Server, p.Name, p.Description)
}

// Get a specific prompt with arguments
msg, err := host.GetMCPPrompt(ctx, "server-name", "prompt-name", map[string]string{
    "topic": "concurrency",
})

MCP Tasks (long-running tools)

Kit advertises MCP task support during initialize. Cooperating servers can respond to tools/call with a taskId immediately; Kit then polls tasks/get / tasks/result until the task reaches a terminal state, and best-effort tasks/cancels on context cancellation. Servers that don't advertise the capability keep their previous synchronous behaviour.

host, _ := kit.New(ctx, &kit.Options{
    // Per-server mode: auto (default), never, or always.
    MCPTaskMode: map[string]kit.MCPTaskMode{
        "build-server": kit.MCPTaskModeAlways,
    },
    MCPTaskTimeout:  15 * time.Minute, // total wall-clock cap
    MCPTaskProgress: func(p kit.MCPTaskProgress) {
        log.Printf("%s/%s: %s", p.Server, p.TaskID, p.Status)
    },
})

// Inspect / cancel in-flight tasks
tasks, _ := host.ListMCPTasks(ctx, "build-server")
t, _    := host.GetMCPTask(ctx, "build-server", tasks[0].TaskID)
if !t.Status.IsTerminal() {
    _, _ = host.CancelMCPTask(ctx, "build-server", t.TaskID)
}

The progress handler fires once when a task is accepted and again on every observed status transition; the final invocation always carries a terminal status (MCPTaskStatusCompleted, MCPTaskStatusFailed, or MCPTaskStatusCancelled). Don't block in the handler — dispatch long work on a goroutine.

Session Management

Maintain conversation context:

// First message
host.Prompt(ctx, "My name is Alice")

// Second message (remembers context)
response, _ := host.Prompt(ctx, "What's my name?")
// Response: "Your name is Alice"

// Clear conversation history
host.ClearSession()

Runtime Skills and Context Files

For multi-tenant chatbots, web services, or any host that needs per-user or per-session instructions, the SDK lets you add, remove, and replace skills and project context files (e.g. AGENTS.md) after Kit construction. Every mutation recomposes the system prompt and applies it to the agent so the next turn picks up the new instructions — no restart required.

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

// Swap per-user AGENTS.md content fetched from your database.
host.AddContextFileContent(
    fmt.Sprintf("session://%s/AGENTS.md", userID),
    rulesFromDB,
)

// Tear down session-specific state when the user logs off.
host.RemoveSkill("polite-french")
host.RemoveContextFile(fmt.Sprintf("session://%s/AGENTS.md", userID))

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

Readers (GetSkills, GetContextFiles) return snapshots, and every mutator is safe to call concurrently from multiple goroutines.

Re-exported Types

The SDK re-exports message/session/MCP types so you don't need direct internal imports. Agent-configuration types are Kit-owned (not aliases) and use only SDK types in their signatures, so consumers never need to import the underlying LLM-provider package.

// Message types
kit.Message, kit.MessageRole, kit.ContentPart
kit.TextContent, kit.ReasoningContent, kit.ToolCall, kit.ToolResult, kit.Finish
kit.RoleUser, kit.RoleAssistant, kit.RoleTool, kit.RoleSystem

// LLM types — Kit-owned `LLM*` aliases over the underlying provider types,
// so consumers never import the provider package directly
kit.LLMMessage      // {Role LLMMessageRole, Content string}
kit.LLMMessageRole  // "user" | "assistant" | "system" | "tool"
kit.LLMUsage        // {InputTokens, OutputTokens, TotalTokens, ...}
kit.LLMResponse     // {Content, FinishReason, Usage}
kit.LLMFilePart     // {Filename, Data []byte, MediaType}

// Agent configuration — concrete Kit-owned structs and function types.
// All fields use SDK types (e.g. `[]kit.Tool`), so consumers can construct
// these without importing any LLM-provider package.
kit.AgentConfig              // Lower-level agent config — prefer Options unless you need direct control
kit.DebugLogger              // Interface: LogDebug(string) / IsDebugEnabled() bool
kit.MCPTaskConfig            // Task-aware MCP tools/call config (modes, polling, progress)
kit.ToolCallHandler          // func(toolCallID, toolName, toolArgs string)
kit.ToolExecutionHandler     // func(toolCallID, toolName, toolArgs string, isStarting bool)
kit.ToolResultHandler        // func(toolCallID, toolName, toolArgs, result, metadata string, isError bool)
kit.ResponseHandler          // func(content string)
kit.StreamingResponseHandler // func(content string)
kit.ToolCallContentHandler   // func(content string)
kit.SpinnerFunc              // func(fn func() error) error

// MCP OAuth types
kit.MCPServer            // *server.MCPServer for in-process MCP transport
kit.MCPServerConfig      // Configuration for an MCP server (stdio, SSE, or in-process)
kit.MCPAuthHandler       // Interface: handles user-facing OAuth authorization
kit.DefaultMCPAuthHandler // Port + callback-server mechanics; set OnAuthURL for presentation
kit.CLIMCPAuthHandler    // CLI wrapper: opens browser, prints status
kit.MCPTokenStore        // Persists OAuth tokens for a single MCP server
kit.MCPToken             // OAuth token (access token, refresh token, expiry)
kit.MCPTokenStoreFactory // Creates an MCPTokenStore for a given server URL

// Conversion helpers
msgs := kit.ConvertToLLMMessages(&msg)   // SDK Message → []LLMMessage
msg  := kit.ConvertFromLLMMessage(lMsg)  // LLMMessage  → SDK Message

API Reference

Types

  • Kit - Main SDK type
  • Options - Configuration options
  • Option - Functional option (func(*Options)) for NewAgent
  • Message - Conversation message with typed content parts
  • Tool - Agent tool interface
  • TurnResult - Full result from a prompt including usage stats

Key Methods

  • New(ctx, opts) - Create new Kit instance
  • NewAgent(ctx, ...Option) - Create a Kit via functional options (streaming on by default)
  • Prompt(ctx, message) - Send message and get response string
  • PromptResult(ctx, message) - Send message and get full TurnResult
  • PromptWithOptions(ctx, message, opts) - Prompt with per-call options
  • Steer(ctx, instruction) - System-level steering
  • FollowUp(ctx, text) - Continue without new user input
  • SetModel(ctx, model) - Switch model at runtime
  • GetModelString() - Get current model string
  • GetModelInfo() - Get model capabilities and limits
  • ClearSession() - Clear conversation history
  • GetSessionPath() - Get session file path
  • GetSessionID() - Get session UUID
  • AddSkill(*Skill) / LoadAndAddSkill(path) / RemoveSkill(name) / SetSkills([]) - Manage skills at runtime
  • AddContextFile(*ContextFile) / AddContextFileContent(path, content) / LoadAndAddContextFile(path) / RemoveContextFile(path) / SetContextFiles([]) - Manage AGENTS.md-style context files at runtime
  • RefreshSystemPrompt() - Re-apply the composed system prompt to the agent
  • Close() - Clean up resources

Options

Key Options fields for SDK usage:

Field Description
Model Override model (e.g., "anthropic/claude-sonnet-4-5-20250929")
SystemPrompt Override system prompt
ConfigFile Load specific config file (empty = search defaults)
SkipConfig Skip .kit.yml loading (defaults + env vars still apply)
Tools Replace core tools with custom set
ExtraTools Add tools alongside defaults
DisableCoreTools Use no core tools (0 tools, for chat-only)
NoSession Ephemeral mode (no session persistence)
SessionPath Open specific session file
Continue Resume most recent session
InProcessMCPServers Map of name → *kit.MCPServer for in-process MCP servers
Debug Enable debug logging

Environment Variables

All CLI environment variables work with the SDK:

  • KIT_MODEL - Override model
  • ANTHROPIC_API_KEY - Anthropic API key
  • OPENAI_API_KEY - OpenAI API key
  • GEMINI_API_KEY - Google API key
  • etc.

License

Same as KIT CLI