package state import ( "context" "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "sync" "time" . "github.com/mudler/LocalAGI/core/agent" sseLib "github.com/mudler/LocalAGI/core/sse" "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/localrag" "github.com/mudler/LocalAGI/pkg/utils" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/mudler/cogito" "github.com/mudler/xlog" ) // SkillsProvider supplies the skills dynamic prompt and MCP session when skills are enabled for an agent. type SkillsProvider interface { GetSkillsPrompt(config *AgentConfig) (DynamicPrompt, error) GetMCPSession(ctx context.Context) (*mcp.ClientSession, error) } // RAGProvider returns a RAGDB and optional compaction client for a collection (e.g. agent name). // effectiveRAGURL/Key are pool/agent defaults; implementation may use them (HTTP) or ignore them (embedded). type RAGProvider func(collectionName, effectiveRAGURL, effectiveRAGKey string) (RAGDB, KBCompactionClient, bool) // NewHTTPRAGProvider returns a RAGProvider that uses the LocalRAG HTTP API. When effective URL/key are empty, baseURL/baseKey are used. func NewHTTPRAGProvider(baseURL, baseKey string) RAGProvider { return func(collectionName, effectiveURL, effectiveKey string) (RAGDB, KBCompactionClient, bool) { url := effectiveURL if url == "" { url = baseURL } key := effectiveKey if key == "" { key = baseKey } wc := localrag.NewWrappedClient(url, key, collectionName) return wc, &wrappedClientCompactionAdapter{WrappedClient: wc}, true } } type AgentPool struct { sync.Mutex file string pooldir string pool AgentPoolData agents map[string]*Agent managers map[string]sseLib.Manager agentStatus map[string]*Status apiURL, defaultModel, defaultMultimodalModel, defaultTTSModel string defaultTranscriptionModel, defaultTranscriptionLanguage string apiKey string ragProvider RAGProvider availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action connectors func(*AgentConfig) []Connector dynamicPrompt func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt filters func(*AgentConfig) types.JobFilters timeout string conversationLogs string skillsService SkillsProvider } // SetRAGProvider sets the single RAG provider (HTTP or embedded). Must be called after pool creation. func (a *AgentPool) SetRAGProvider(fn RAGProvider) { a.Lock() defer a.Unlock() a.ragProvider = fn } type Status struct { ActionResults []types.ActionState } func (s *Status) addResult(result types.ActionState) { // If we have more than 10 results, remove the oldest one if len(s.ActionResults) > 10 { s.ActionResults = s.ActionResults[1:] } s.ActionResults = append(s.ActionResults, result) } func (s *Status) Results() []types.ActionState { return s.ActionResults } type AgentPoolData map[string]AgentConfig func loadPoolFromFile(path string) (*AgentPoolData, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } poolData := &AgentPoolData{} err = json.Unmarshal(data, poolData) return poolData, err } func NewAgentPool( defaultModel, defaultMultimodalModel, defaultTranscriptionModel, defaultTranscriptionLanguage, defaultTTSModel, apiURL, apiKey, directory string, availableActions func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []types.Action, connectors func(*AgentConfig) []Connector, promptBlocks func(*AgentConfig) func(ctx context.Context, pool *AgentPool) []DynamicPrompt, filters func(*AgentConfig) types.JobFilters, timeout string, withLogs bool, skillsService SkillsProvider, ) (*AgentPool, error) { // if file exists, try to load an existing pool. // if file does not exist, create a new pool. poolfile := filepath.Join(directory, "pool.json") conversationPath := "" if withLogs { conversationPath = filepath.Join(directory, "conversations") } if _, err := os.Stat(poolfile); err != nil { // file does not exist, create a new pool return &AgentPool{ file: poolfile, pooldir: directory, apiURL: apiURL, defaultModel: defaultModel, defaultMultimodalModel: defaultMultimodalModel, defaultTranscriptionModel: defaultTranscriptionModel, defaultTranscriptionLanguage: defaultTranscriptionLanguage, defaultTTSModel: defaultTTSModel, apiKey: apiKey, agents: make(map[string]*Agent), pool: make(map[string]AgentConfig), agentStatus: make(map[string]*Status), managers: make(map[string]sseLib.Manager), connectors: connectors, availableActions: availableActions, dynamicPrompt: promptBlocks, filters: filters, timeout: timeout, conversationLogs: conversationPath, skillsService: skillsService, }, nil } poolData, err := loadPoolFromFile(poolfile) if err != nil { bakPath := poolfile + ".bak" poolData, err = loadPoolFromFile(bakPath) if err != nil { xlog.Warn("Pool file invalid and backup missing or invalid, starting with empty pool", "poolfile", poolfile, "error", err) poolData = &AgentPoolData{} } else { xlog.Info("Recovered pool from backup, repairing main file", "poolfile", poolfile) if repairData, _ := json.MarshalIndent(poolData, "", " "); len(repairData) > 0 { _ = os.WriteFile(poolfile, repairData, 0644) } } } return &AgentPool{ file: poolfile, apiURL: apiURL, pooldir: directory, defaultModel: defaultModel, defaultMultimodalModel: defaultMultimodalModel, defaultTranscriptionModel: defaultTranscriptionModel, defaultTranscriptionLanguage: defaultTranscriptionLanguage, defaultTTSModel: defaultTTSModel, apiKey: apiKey, agents: make(map[string]*Agent), managers: make(map[string]sseLib.Manager), agentStatus: map[string]*Status{}, pool: *poolData, connectors: connectors, dynamicPrompt: promptBlocks, filters: filters, availableActions: availableActions, timeout: timeout, conversationLogs: conversationPath, skillsService: skillsService, }, nil } func replaceInvalidChars(s string) string { s = strings.ReplaceAll(s, "/", "_") return strings.ReplaceAll(s, " ", "_") } // StartAgentStandalone starts an agent without saving it to the pool registry. // It is intended for running a single agent from the CLI without the web server. func (a *AgentPool) StartAgentStandalone(name string, agentConfig *AgentConfig) error { a.Lock() defer a.Unlock() return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil) } // CreateAgent adds a new agent to the pool // and starts it. // It also saves the state to the file. func (a *AgentPool) CreateAgent(name string, agentConfig *AgentConfig) error { a.Lock() defer a.Unlock() name = replaceInvalidChars(name) agentConfig.Name = name if _, ok := a.pool[name]; ok { return fmt.Errorf("agent %s already exists", name) } a.pool[name] = *agentConfig if err := a.save(); err != nil { return err } return a.startAgentWithConfig(name, a.pooldir, agentConfig, nil) } func (a *AgentPool) RecreateAgent(name string, agentConfig *AgentConfig) error { a.Lock() defer a.Unlock() oldAgent := a.agents[name] var o *types.Observable var obs Observer if oldAgent != nil { obs = oldAgent.Observer() if obs != nil { o = obs.NewObservable() o.Name = "Restarting Agent" o.Icon = "sync" o.Creation = &types.Creation{} obs.Update(*o) } stateFile, characterFile := a.stateFiles(name) os.Remove(stateFile) os.Remove(characterFile) oldAgent.Stop() } a.pool[name] = *agentConfig delete(a.agents, name) if err := a.save(); err != nil { if obs != nil { o.Completion = &types.Completion{Error: err.Error()} obs.Update(*o) } return err } if err := a.startAgentWithConfig(name, a.pooldir, agentConfig, obs); err != nil { if obs != nil { o.Completion = &types.Completion{Error: err.Error()} obs.Update(*o) } return err } if obs != nil { o.Completion = &types.Completion{} obs.Update(*o) } return nil } func (a *AgentPool) List() []string { a.Lock() defer a.Unlock() var agents []string for agent := range a.pool { agents = append(agents, agent) } // return a sorted list sort.SliceStable(agents, func(i, j int) bool { return agents[i] < agents[j] }) return agents } func (a *AgentPool) GetStatusHistory(name string) *Status { a.Lock() defer a.Unlock() return a.agentStatus[name] } func (a *AgentPool) startAgentWithConfig(name, pooldir string, config *AgentConfig, obs Observer) error { var manager sseLib.Manager if m, ok := a.managers[name]; ok { manager = m } else { manager = sseLib.NewManager(5) } ctx := context.Background() model := a.defaultModel multimodalModel := a.defaultMultimodalModel transcriptionModel := a.defaultTranscriptionModel transcriptionLanguage := a.defaultTranscriptionLanguage ttsModel := a.defaultTTSModel if config.MultimodalModel != "" { multimodalModel = config.MultimodalModel } if config.TranscriptionModel != "" { transcriptionModel = config.TranscriptionModel } if config.TranscriptionLanguage != "" { transcriptionLanguage = config.TranscriptionLanguage } if config.TTSModel != "" { ttsModel = config.TTSModel } if config.Model != "" { model = config.Model } else { config.Model = model } if config.PeriodicRuns == "" { config.PeriodicRuns = "10m" } if config.SchedulerPollInterval == "" { config.SchedulerPollInterval = "30s" } // Use agent-specific config when set, otherwise pool defaults. Do not update pool from agent config. effectiveAPIURL := a.apiURL if config.APIURL != "" { effectiveAPIURL = config.APIURL } else { config.APIURL = a.apiURL } effectiveAPIKey := a.apiKey if config.APIKey != "" { effectiveAPIKey = config.APIKey } else { config.APIKey = a.apiKey } effectiveLocalRAGAPI := config.LocalRAGURL effectiveLocalRAGKey := config.LocalRAGAPIKey connectors := a.connectors(config) promptBlocks := a.dynamicPrompt(config)(ctx, a) if a.skillsService != nil && config.EnableSkills { if prompt, err := a.skillsService.GetSkillsPrompt(config); err == nil && prompt != nil { promptBlocks = append(promptBlocks, prompt) } } actions := a.availableActions(config)(ctx, a) filters := a.filters(config) stateFile, characterFile := a.stateFiles(name) actionsLog := []string{} for _, action := range actions { actionsLog = append(actionsLog, action.Definition().Name.String()) } connectorLog := []string{} for _, connector := range connectors { connectorLog = append(connectorLog, fmt.Sprintf("%+v", connector)) } filtersLog := []string{} for _, filter := range filters { filtersLog = append(filtersLog, filter.Name()) } xlog.Info( "Creating agent", "name", name, "model", model, "api_url", effectiveAPIURL, "actions", actionsLog, "connectors", connectorLog, "filters", filtersLog, ) // dynamicPrompts := []map[string]string{} // for _, p := range config.DynamicPrompts { // dynamicPrompts = append(dynamicPrompts, p.ToMap()) // } if obs == nil { obs = NewSSEObserver(name, manager) } opts := []Option{ WithSchedulerStorePath(filepath.Join(pooldir, fmt.Sprintf("scheduler-%s.json", name))), WithModel(model), WithLLMAPIURL(effectiveAPIURL), WithContext(ctx), WithMCPServers(config.MCPServers...), WithTranscriptionModel(transcriptionModel), WithTranscriptionLanguage(transcriptionLanguage), WithTTSModel(ttsModel), WithPeriodicRuns(config.PeriodicRuns), WithSchedulerPollInterval(config.SchedulerPollInterval), WithPermanentGoal(config.PermanentGoal), WithMCPSTDIOServers(config.MCPSTDIOServers...), WithPrompts(promptBlocks...), WithJobFilters(filters...), WithMCPPrepareScript(config.MCPPrepareScript), // WithDynamicPrompts(dynamicPrompts...), WithCharacter(Character{ Name: name, }), WithActions( actions..., ), WithStateFile(stateFile), WithCharacterFile(characterFile), WithLLMAPIKey(effectiveAPIKey), WithTimeout(a.timeout), WithAgentReasoningCallback(func(state types.ActionCurrentState) bool { var actionName types.ActionDefinitionName if state.Action != nil { actionName = state.Action.Definition().Name } xlog.Info( "Agent is thinking", "agent", name, "reasoning", state.Reasoning, "action", actionName, "params", state.Params, ) manager.Send( sseLib.NewMessage( fmt.Sprintf(`Thinking: %s`, utils.HTMLify(state.Reasoning)), ).WithEvent("status"), ) for _, c := range connectors { if !c.AgentReasoningCallback()(state) { return false } } return true }), WithSystemPrompt(config.SystemPrompt), WithInnerMonologueTemplate(config.InnerMonologueTemplate), WithSchedulerTaskTemplate(config.SchedulerTaskTemplate), WithMultimodalModel(multimodalModel), WithLastMessageDuration(config.LastMessageDuration), WithAgentResultCallback(func(state types.ActionState) { a.Lock() if _, ok := a.agentStatus[name]; !ok { a.agentStatus[name] = &Status{} } a.agentStatus[name].addResult(state) a.Unlock() xlog.Debug( "Calling agent result callback", ) var actionName types.ActionDefinitionName if state.ActionCurrentState.Action != nil { actionName = state.ActionCurrentState.Action.Definition().Name } text := fmt.Sprintf(`Reasoning: %s Action taken: %+v Parameters: %+v Result: %s`, state.Reasoning, actionName, state.ActionCurrentState.Params, state.Result) manager.Send( sseLib.NewMessage( utils.HTMLify( text, ), ).WithEvent("status"), ) for _, c := range connectors { c.AgentResultCallback()(state) } }), WithObserver(obs), } if config.HUD { opts = append(opts, EnableHUD) } if a.conversationLogs != "" { opts = append(opts, WithConversationsPath(a.conversationLogs)) } if config.StandaloneJob { opts = append(opts, EnableStandaloneJob) } if config.LongTermMemory { opts = append(opts, EnableLongTermMemory) } if config.SummaryLongTermMemory { opts = append(opts, EnableSummaryMemory) } if config.ConversationStorageMode != "" { opts = append(opts, WithConversationStorageMode(ConversationStorageMode(config.ConversationStorageMode))) } if config.CanStopItself { opts = append(opts, CanStopItself) } if config.CanPlan { opts = append(opts, EnablePlanning) } if config.PlanReviewerModel != "" { opts = append(opts, WithPlanReviewerLLM(config.PlanReviewerModel)) } if config.DisableSinkState { opts = append(opts, DisableSinkState) } if config.InitiateConversations { opts = append(opts, EnableInitiateConversations) } if config.RandomIdentity { if config.IdentityGuidance != "" { opts = append(opts, WithRandomIdentity(config.IdentityGuidance)) } else { opts = append(opts, WithRandomIdentity()) } } if a.skillsService != nil && config.EnableSkills { if session, err := a.skillsService.GetMCPSession(ctx); err == nil && session != nil { opts = append(opts, WithMCPSession(session)) } } var ragDB RAGDB var compactionClient KBCompactionClient if config.EnableKnowledgeBase && a.ragProvider != nil { if db, comp, ok := a.ragProvider(name, effectiveLocalRAGAPI, effectiveLocalRAGKey); ok && db != nil { ragDB = db compactionClient = comp } } if ragDB != nil { opts = append(opts, WithRAGDB(ragDB), EnableKnowledgeBase) kbAutoSearch := config.KBAutoSearch if !config.KBAutoSearch && !config.KBAsTools { kbAutoSearch = true } opts = append(opts, WithKBAutoSearch(kbAutoSearch)) if config.KBAsTools { kbResults := config.KnowledgeBaseResults if kbResults <= 0 { kbResults = 5 } searchAction, addAction := NewKBWrapperActions(ragDB, kbResults) opts = append(opts, WithActions(searchAction, addAction)) } } if config.EnableReasoning { opts = append(opts, EnableForceReasoning) } if config.EnableGuidedTools { opts = append(opts, EnableGuidedTools) } if config.StripThinkingTags { opts = append(opts, EnableStripThinkingTags) } if config.EnableAutoCompaction { opts = append(opts, EnableAutoCompaction) } if config.AutoCompactionThreshold > 0 { opts = append(opts, WithAutoCompactionThreshold(config.AutoCompactionThreshold)) } if config.KnowledgeBaseResults > 0 { opts = append(opts, EnableKnowledgeBaseWithResults(config.KnowledgeBaseResults)) } if config.ParallelJobs > 0 { opts = append(opts, WithParallelJobs(config.ParallelJobs)) } if config.CancelPreviousOnNewMessage != nil { opts = append(opts, WithCancelPreviousOnNewMessage(*config.CancelPreviousOnNewMessage)) } else { opts = append(opts, WithCancelPreviousOnNewMessage(true)) } if config.EnableEvaluation { opts = append(opts, EnableEvaluation()) } if config.MaxEvaluationLoops > 0 { opts = append(opts, WithMaxEvaluationLoops(config.MaxEvaluationLoops)) } if config.MaxAttempts > 0 { opts = append(opts, WithMaxAttempts(config.MaxAttempts)) } if config.LoopDetection > 0 { opts = append(opts, WithLoopDetection(config.LoopDetection)) } if config.EnableForceReasoningTool { opts = append(opts, EnableForceReasoningTool) } // Wire cogito streaming events into the SSE manager for live token delivery opts = append(opts, WithStreamCallback(func(ev cogito.StreamEvent) { switch ev.Type { case cogito.StreamEventReasoning: data, _ := json.Marshal(map[string]interface{}{ "type": "reasoning", "content": ev.Content, "timestamp": time.Now().Format(time.RFC3339), }) manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event")) case cogito.StreamEventContent: data, _ := json.Marshal(map[string]interface{}{ "type": "content", "content": ev.Content, "timestamp": time.Now().Format(time.RFC3339), }) manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event")) case cogito.StreamEventToolCall: data, _ := json.Marshal(map[string]interface{}{ "type": "tool_call", "tool_name": ev.ToolName, "tool_args": ev.ToolArgs, "timestamp": time.Now().Format(time.RFC3339), }) manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event")) case cogito.StreamEventDone: data, _ := json.Marshal(map[string]interface{}{ "type": "done", "timestamp": time.Now().Format(time.RFC3339), }) manager.Send(sseLib.NewMessage(string(data)).WithEvent("stream_event")) } })) xlog.Info("Starting agent", "name", name, "config", config) agent, err := New(opts...) if err != nil { return err } a.agents[name] = agent a.managers[name] = manager go func() { if err := agent.Run(); err != nil { xlog.Error("Agent stopped", "error", err.Error(), "name", name) } }() if config.EnableKnowledgeBase && config.EnableKBCompaction && compactionClient != nil { go runCompactionTicker(ctx, compactionClient, config, effectiveAPIURL, effectiveAPIKey, model) } xlog.Info("Starting connectors", "name", name, "config", config) for _, c := range connectors { go c.Start(agent) } go func() { for { time.Sleep(1 * time.Second) // Send a message every seconds manager.Send(sseLib.NewMessage( utils.HTMLify(agent.State().String()), ).WithEvent("hud")) } }() xlog.Info("Agent started", "name", name) return nil } // Starts all the agents in the pool func (a *AgentPool) StartAll() error { a.Lock() defer a.Unlock() for name, config := range a.pool { if a.agents[name] != nil { // Agent already started continue } if err := a.startAgentWithConfig(name, a.pooldir, &config, nil); err != nil { xlog.Error("Failed to start agent", "name", name, "error", err) } } return nil } func (a *AgentPool) StopAll() { a.Lock() defer a.Unlock() for _, agent := range a.agents { agent.Stop() } } func (a *AgentPool) Stop(name string) { a.Lock() defer a.Unlock() a.stop(name) } func (a *AgentPool) stop(name string) { if agent, ok := a.agents[name]; ok { agent.Stop() } } func (a *AgentPool) Start(name string) error { a.Lock() defer a.Unlock() if agent, ok := a.agents[name]; ok { err := agent.Run() if err != nil { return fmt.Errorf("agent %s failed to start: %w", name, err) } xlog.Info("Agent started", "name", name) return nil } if config, ok := a.pool[name]; ok { return a.startAgentWithConfig(name, a.pooldir, &config, nil) } return fmt.Errorf("agent %s not found", name) } // CreateOnly creates the agent instance without calling Run(). // This is used in distributed mode where the agent is executed statelessly // via AskDirect() — the persistent Run() loop is not needed. func (a *AgentPool) CreateOnly(name string) error { a.Lock() defer a.Unlock() if _, ok := a.agents[name]; ok { return nil // already created } if config, ok := a.pool[name]; ok { return a.createAgentWithoutRun(name, a.pooldir, &config) } return fmt.Errorf("agent %s not found", name) } // createAgentWithoutRun is like startAgentWithConfig but skips Run(), connectors, and HUD. func (a *AgentPool) createAgentWithoutRun(name, pooldir string, config *AgentConfig) error { var manager sseLib.Manager if m, ok := a.managers[name]; ok { manager = m } else { manager = sseLib.NewManager(5) } ctx := context.Background() model := a.defaultModel multimodalModel := a.defaultMultimodalModel transcriptionModel := a.defaultTranscriptionModel transcriptionLanguage := a.defaultTranscriptionLanguage ttsModel := a.defaultTTSModel if config.MultimodalModel != "" { multimodalModel = config.MultimodalModel } if config.TranscriptionModel != "" { transcriptionModel = config.TranscriptionModel } if config.TranscriptionLanguage != "" { transcriptionLanguage = config.TranscriptionLanguage } if config.TTSModel != "" { ttsModel = config.TTSModel } if config.Model != "" { model = config.Model } else { config.Model = model } effectiveAPIURL := a.apiURL if config.APIURL != "" { effectiveAPIURL = config.APIURL } else { config.APIURL = a.apiURL } effectiveAPIKey := a.apiKey if config.APIKey != "" { effectiveAPIKey = config.APIKey } else { config.APIKey = a.apiKey } promptBlocks := a.dynamicPrompt(config)(ctx, a) if a.skillsService != nil && config.EnableSkills { if prompt, err := a.skillsService.GetSkillsPrompt(config); err == nil && prompt != nil { promptBlocks = append(promptBlocks, prompt) } } actions := a.availableActions(config)(ctx, a) stateFile, characterFile := a.stateFiles(name) obs := NewSSEObserver(name, manager) opts := []Option{ WithSchedulerStorePath(filepath.Join(pooldir, fmt.Sprintf("scheduler-%s.json", name))), WithModel(model), WithLLMAPIURL(effectiveAPIURL), WithContext(ctx), WithTranscriptionModel(transcriptionModel), WithTranscriptionLanguage(transcriptionLanguage), WithTTSModel(ttsModel), WithPrompts(promptBlocks...), WithActions(actions...), WithObserver(obs), WithMultimodalModel(multimodalModel), WithCharacterFile(characterFile), WithStateFile(stateFile), WithSystemPrompt(config.SystemPrompt), } if effectiveAPIKey != "" { opts = append(opts, WithLLMAPIKey(effectiveAPIKey)) } xlog.Info("Creating agent (no Run)", "name", name, "model", model, "api_url", effectiveAPIURL) agent, err := New(opts...) if err != nil { return err } a.agents[name] = agent a.managers[name] = manager // Start the conversation consumer so ConversationAction doesn't deadlock. // This is normally started by Run(), but we skip Run() in distributed mode. agent.StartConversationConsumer() xlog.Info("Agent created (no Run)", "name", name) return nil } func (a *AgentPool) stateFiles(name string) (string, string) { stateFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.state.json", name)) characterFile := filepath.Join(a.pooldir, fmt.Sprintf("%s.character.json", name)) return stateFile, characterFile } func (a *AgentPool) Remove(name string) error { a.Lock() defer a.Unlock() // Cleanup character and state stateFile, characterFile := a.stateFiles(name) os.Remove(stateFile) os.Remove(characterFile) a.stop(name) delete(a.agents, name) delete(a.pool, name) if err := a.save(); err != nil { return err } return nil } func (a *AgentPool) Save() error { a.Lock() defer a.Unlock() return a.save() } func (a *AgentPool) save() error { data, err := json.MarshalIndent(a.pool, "", " ") if err != nil { return err } tmpPath := a.file + ".tmp" if err := os.WriteFile(tmpPath, data, 0644); err != nil { os.Remove(tmpPath) return err } if err := os.Rename(tmpPath, a.file); err != nil { os.Remove(tmpPath) return err } bakPath := a.file + ".bak" if err := os.WriteFile(bakPath, data, 0644); err != nil { // best-effort; main file is already good xlog.Warn("Failed to write pool backup", "path", bakPath, "error", err) } return nil } func (a *AgentPool) GetAgent(name string) *Agent { a.Lock() defer a.Unlock() return a.agents[name] } func (a *AgentPool) AllAgents() []string { a.Lock() defer a.Unlock() var agents []string for agent := range a.agents { agents = append(agents, agent) } return agents } func (a *AgentPool) GetConfig(name string) *AgentConfig { a.Lock() defer a.Unlock() agent, exists := a.pool[name] if !exists { return nil } return &agent } func (a *AgentPool) GetManager(name string) sseLib.Manager { a.Lock() defer a.Unlock() return a.managers[name] }