package agent import ( "fmt" "log" "os" "path/filepath" "regexp" "strings" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/routing" "github.com/sipeed/picoclaw/pkg/session" "github.com/sipeed/picoclaw/pkg/tools" ) // AgentInstance represents a fully configured agent with its own workspace, // session manager, context builder, and tool registry. type AgentInstance struct { ID string Name string Model string Fallbacks []string Workspace string MaxIterations int MaxTokens int Temperature float64 ThinkingLevel ThinkingLevel ContextWindow int SummarizeMessageThreshold int SummarizeTokenPercent int Provider providers.LLMProvider Sessions *session.SessionManager ContextBuilder *ContextBuilder Tools *tools.ToolRegistry Subagents *config.SubagentsConfig SkillsFilter []string Candidates []providers.FallbackCandidate } // NewAgentInstance creates an agent instance from config. func NewAgentInstance( agentCfg *config.AgentConfig, defaults *config.AgentDefaults, cfg *config.Config, provider providers.LLMProvider, ) *AgentInstance { workspace := resolveAgentWorkspace(agentCfg, defaults) os.MkdirAll(workspace, 0o755) model := resolveAgentModel(agentCfg, defaults) fallbacks := resolveAgentFallbacks(agentCfg, defaults) restrict := defaults.RestrictToWorkspace readRestrict := restrict && !defaults.AllowReadOutsideWorkspace // Compile path whitelist patterns from config. allowReadPaths := compilePatterns(cfg.Tools.AllowReadPaths) allowWritePaths := compilePatterns(cfg.Tools.AllowWritePaths) toolsRegistry := tools.NewToolRegistry() toolsRegistry.Register(tools.NewReadFileTool(workspace, readRestrict, allowReadPaths)) toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict, allowWritePaths)) toolsRegistry.Register(tools.NewListDirTool(workspace, readRestrict, allowReadPaths)) execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg) if err != nil { log.Fatalf("Critical error: unable to initialize exec tool: %v", err) } toolsRegistry.Register(execTool) toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict, allowWritePaths)) toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict, allowWritePaths)) sessionsDir := filepath.Join(workspace, "sessions") sessionsManager := session.NewSessionManager(sessionsDir) contextBuilder := NewContextBuilder(workspace) agentID := routing.DefaultAgentID agentName := "" var subagents *config.SubagentsConfig var skillsFilter []string if agentCfg != nil { agentID = routing.NormalizeAgentID(agentCfg.ID) agentName = agentCfg.Name subagents = agentCfg.Subagents skillsFilter = agentCfg.Skills } maxIter := defaults.MaxToolIterations if maxIter == 0 { maxIter = 20 } maxTokens := defaults.MaxTokens if maxTokens == 0 { maxTokens = 8192 } temperature := 0.7 if defaults.Temperature != nil { temperature = *defaults.Temperature } var thinkingLevelStr string if mc, err := cfg.GetModelConfig(model); err == nil { thinkingLevelStr = mc.ThinkingLevel } thinkingLevel := parseThinkingLevel(thinkingLevelStr) summarizeMessageThreshold := defaults.SummarizeMessageThreshold if summarizeMessageThreshold == 0 { summarizeMessageThreshold = 20 } summarizeTokenPercent := defaults.SummarizeTokenPercent if summarizeTokenPercent == 0 { summarizeTokenPercent = 75 } // Resolve fallback candidates modelCfg := providers.ModelConfig{ Primary: model, Fallbacks: fallbacks, } resolveFromModelList := func(raw string) (string, bool) { ensureProtocol := func(model string) string { model = strings.TrimSpace(model) if model == "" { return "" } if strings.Contains(model, "/") { return model } return "openai/" + model } raw = strings.TrimSpace(raw) if raw == "" { return "", false } if cfg != nil { if mc, err := cfg.GetModelConfig(raw); err == nil && mc != nil && strings.TrimSpace(mc.Model) != "" { return ensureProtocol(mc.Model), true } for i := range cfg.ModelList { fullModel := strings.TrimSpace(cfg.ModelList[i].Model) if fullModel == "" { continue } if fullModel == raw { return ensureProtocol(fullModel), true } _, modelID := providers.ExtractProtocol(fullModel) if modelID == raw { return ensureProtocol(fullModel), true } } } return "", false } candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList) return &AgentInstance{ ID: agentID, Name: agentName, Model: model, Fallbacks: fallbacks, Workspace: workspace, MaxIterations: maxIter, MaxTokens: maxTokens, Temperature: temperature, ThinkingLevel: thinkingLevel, ContextWindow: maxTokens, SummarizeMessageThreshold: summarizeMessageThreshold, SummarizeTokenPercent: summarizeTokenPercent, Provider: provider, Sessions: sessionsManager, ContextBuilder: contextBuilder, Tools: toolsRegistry, Subagents: subagents, SkillsFilter: skillsFilter, Candidates: candidates, } } // resolveAgentWorkspace determines the workspace directory for an agent. func resolveAgentWorkspace(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string { if agentCfg != nil && strings.TrimSpace(agentCfg.Workspace) != "" { return expandHome(strings.TrimSpace(agentCfg.Workspace)) } if agentCfg == nil || agentCfg.Default || agentCfg.ID == "" || routing.NormalizeAgentID(agentCfg.ID) == "main" { return expandHome(defaults.Workspace) } home, _ := os.UserHomeDir() id := routing.NormalizeAgentID(agentCfg.ID) return filepath.Join(home, ".picoclaw", "workspace-"+id) } // resolveAgentModel resolves the primary model for an agent. func resolveAgentModel(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) string { if agentCfg != nil && agentCfg.Model != nil && strings.TrimSpace(agentCfg.Model.Primary) != "" { return strings.TrimSpace(agentCfg.Model.Primary) } return defaults.GetModelName() } // resolveAgentFallbacks resolves the fallback models for an agent. func resolveAgentFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentDefaults) []string { if agentCfg != nil && agentCfg.Model != nil && agentCfg.Model.Fallbacks != nil { return agentCfg.Model.Fallbacks } return defaults.ModelFallbacks } func compilePatterns(patterns []string) []*regexp.Regexp { compiled := make([]*regexp.Regexp, 0, len(patterns)) for _, p := range patterns { re, err := regexp.Compile(p) if err != nil { fmt.Printf("Warning: invalid path pattern %q: %v\n", p, err) continue } compiled = append(compiled, re) } return compiled } func expandHome(path string) string { if path == "" { return path } if path[0] == '~' { home, _ := os.UserHomeDir() if len(path) > 1 && path[1] == '/' { return home + path[1:] } return home } return path }