mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
7a04bdfeba
* 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
179 lines
6.9 KiB
Go
179 lines
6.9 KiB
Go
package kit
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/mark3labs/kit/internal/config"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// defaultSystemPrompt is the built-in system prompt used when no custom
|
|
// prompt is configured. It describes the available core tools and provides
|
|
// usage guidelines.
|
|
//
|
|
// NOTE: Keep this in sync with the CLI default in cmd/root.go (search for
|
|
// defaultSystemPrompt or system-prompt flag default). Changes here should
|
|
// generally be reflected there, and vice versa.
|
|
const defaultSystemPrompt = `You are an expert coding assistant operating inside kit, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
|
|
|
|
Available tools:
|
|
- read: Read file or directory contents (supports pagination via offset/limit)
|
|
- write: Create or overwrite files
|
|
- edit: Make surgical edits to files (find exact text and replace)
|
|
- bash: Execute bash commands with timeout support
|
|
- grep: Search file contents using regex patterns (respects .gitignore)
|
|
- find: Search for files by glob pattern (respects .gitignore)
|
|
- ls: List directory contents
|
|
|
|
In addition to the tools above, you may have access to other custom tools from MCP servers and extensions.
|
|
|
|
Guidelines:
|
|
- Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)
|
|
- Use read to examine files before editing
|
|
- Use edit for precise changes (old text must match exactly, including whitespace)
|
|
- Use write only for new files or complete rewrites
|
|
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
|
- Be concise in your responses
|
|
- Show file paths clearly when working with files`
|
|
|
|
// sdkDefaultMaxTokens is the last-resort ceiling applied when the SDK caller
|
|
// has not configured max-tokens via Options, env, config, or a per-model
|
|
// default. It matches the CLI's --max-tokens cobra default so SDK and CLI
|
|
// callers see the same base value before per-model right-sizing runs.
|
|
// It is intentionally applied on the *models.ProviderConfig struct
|
|
// (not via viper) so that viper.IsSet("max-tokens") remains false and the
|
|
// right-sizing + per-model-default paths continue to work.
|
|
const sdkDefaultMaxTokens = 8192
|
|
|
|
// setSDKDefaults registers viper defaults that match the CLI's cobra flag
|
|
// defaults for keys where SetDefault does not interfere with downstream
|
|
// viper.IsSet() checks.
|
|
//
|
|
// Keys that participate in "explicit vs unset" precedence downstream —
|
|
// max-tokens, temperature, top-p, top-k, frequency-penalty, presence-penalty,
|
|
// thinking-level — are deliberately NOT registered here. viper.SetDefault
|
|
// causes viper.IsSet() to return true, which would suppress per-model
|
|
// defaults (ApplyModelSettings) and automatic right-sizing (rightSizeMaxTokens)
|
|
// for every SDK-created Kit. Those defaults are instead applied:
|
|
//
|
|
// - max-tokens: as a last-resort struct-level floor (sdkDefaultMaxTokens)
|
|
// in kit.New() after BuildProviderConfig returns, when the resolved
|
|
// value is still zero.
|
|
// - thinking-level: handled implicitly by models.ParseThinkingLevel("")
|
|
// which returns models.ThinkingOff.
|
|
// - sampling params (temperature, top-p, top-k, frequency/presence-penalty):
|
|
// left as nil pointers so provider libraries apply their own defaults.
|
|
func setSDKDefaults(v *viper.Viper) {
|
|
v.SetDefault("model", "anthropic/claude-sonnet-4-5-20250929")
|
|
v.SetDefault("system-prompt", defaultSystemPrompt)
|
|
v.SetDefault("stream", true)
|
|
v.SetDefault("num-gpu-layers", -1)
|
|
v.SetDefault("main-gpu", 0)
|
|
}
|
|
|
|
// InitConfig initializes the process-global viper configuration system.
|
|
// It searches for config files in standard locations and loads them with
|
|
// environment variable substitution.
|
|
//
|
|
// configFile: explicit config file path (empty = search defaults).
|
|
// debug: if true, print warnings about missing configs to stderr.
|
|
//
|
|
// This wraps [initConfig] using the process-global store and is retained for
|
|
// the CLI, which binds its flags to the global viper.
|
|
func InitConfig(configFile string, debug bool) error {
|
|
return initConfig(viper.GetViper(), configFile, debug)
|
|
}
|
|
|
|
// initConfig loads configuration into the supplied per-instance store. When v
|
|
// is nil the process-global store is used.
|
|
func initConfig(v *viper.Viper, configFile string, debug bool) error {
|
|
if v == nil {
|
|
v = viper.GetViper()
|
|
}
|
|
|
|
// Configure KIT_* environment overrides unconditionally, before any file
|
|
// is loaded, so that an explicit config file does not disable env support.
|
|
// Map hyphenated config keys (e.g. "max-tokens") to underscored env var
|
|
// names (e.g. KIT_MAX_TOKENS); without this AutomaticEnv looks for
|
|
// KIT_MAX-TOKENS and silently misses valid overrides. Precedence is
|
|
// resolved at read time, so calling these before ReadConfig is fine.
|
|
v.SetEnvPrefix("KIT")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
|
v.AutomaticEnv()
|
|
|
|
if configFile != "" {
|
|
return loadConfigWithEnvSubstitution(v, configFile)
|
|
}
|
|
|
|
// Ensure a config file exists (create default if none found).
|
|
if err := config.EnsureConfigExists(); err != nil {
|
|
if debug {
|
|
fmt.Fprintf(os.Stderr, "Warning: Could not create default config file: %v\n", err)
|
|
}
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("error finding home directory: %w", err)
|
|
}
|
|
|
|
// Current directory has higher priority than home directory.
|
|
v.AddConfigPath(".")
|
|
v.AddConfigPath(home)
|
|
|
|
configLoaded := false
|
|
|
|
v.SetConfigName(".kit")
|
|
if err := v.ReadInConfig(); err == nil {
|
|
configPath := v.ConfigFileUsed()
|
|
if err := loadConfigWithEnvSubstitution(v, configPath); err != nil {
|
|
if strings.Contains(err.Error(), "environment variable substitution failed") {
|
|
return fmt.Errorf("error reading config file '%s': %w", configPath, err)
|
|
}
|
|
} else {
|
|
configLoaded = true
|
|
}
|
|
}
|
|
|
|
if !configLoaded && debug {
|
|
fmt.Fprintf(os.Stderr, "No config file found in current directory or home directory\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadConfigWithEnvSubstitution loads a config file with ${ENV_VAR} expansion
|
|
// into the process-global viper store.
|
|
func LoadConfigWithEnvSubstitution(configPath string) error {
|
|
return loadConfigWithEnvSubstitution(viper.GetViper(), configPath)
|
|
}
|
|
|
|
// loadConfigWithEnvSubstitution loads a config file with ${ENV_VAR} expansion
|
|
// into the supplied per-instance store (or the global store when v is nil).
|
|
func loadConfigWithEnvSubstitution(v *viper.Viper, configPath string) error {
|
|
if v == nil {
|
|
v = viper.GetViper()
|
|
}
|
|
rawContent, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
substituter := &config.EnvSubstituter{}
|
|
processedContent, err := substituter.SubstituteEnvVars(string(rawContent))
|
|
if err != nil {
|
|
return fmt.Errorf("config env substitution failed: %w", err)
|
|
}
|
|
|
|
configType := "yaml"
|
|
if strings.HasSuffix(configPath, ".json") {
|
|
configType = "json"
|
|
}
|
|
|
|
config.SetConfigPath(configPath)
|
|
v.SetConfigType(configType)
|
|
return v.ReadConfig(strings.NewReader(processedContent))
|
|
}
|