package devagents import ( "context" "log/slog" "os" "path/filepath" "time" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/memory" shellknowledge "github.com/enmanuel/agents/shell/knowledge" shellmcp "github.com/enmanuel/agents/shell/mcp" shellskills "github.com/enmanuel/agents/shell/skills" "github.com/enmanuel/agents/shell/ssh" "github.com/enmanuel/agents/tools" toolclock "github.com/enmanuel/agents/tools/clock" toolfile "github.com/enmanuel/agents/tools/file" toolhttp "github.com/enmanuel/agents/tools/http" toolimdb "github.com/enmanuel/agents/tools/imdb" toolknowledge "github.com/enmanuel/agents/tools/knowledgetools" toolmatrix "github.com/enmanuel/agents/tools/matrix" toolmcp "github.com/enmanuel/agents/tools/mcptools" toolmemory "github.com/enmanuel/agents/tools/memorytools" toolskills "github.com/enmanuel/agents/tools/skilltools" toolssh "github.com/enmanuel/agents/tools/ssh" toolweather "github.com/enmanuel/agents/tools/weather" toolwikipedia "github.com/enmanuel/agents/tools/wikipedia" toolexchange "github.com/enmanuel/agents/tools/exchange" "github.com/enmanuel/agents/shell/matrix" ) // toolDeps holds external subsystem instances needed by the tool registry. type toolDeps struct { kStore *shellknowledge.FileStore sharedKStore *shellknowledge.FileStore mcpManager *shellmcp.Manager skillLoader *shellskills.Loader skillExecutor *shellskills.Executor } // initToolDeps initializes knowledge stores, MCP manager, and skills loader // based on the agent config. All results are optional (nil when disabled). func initToolDeps(cfg *config.AgentConfig, dataBase string, logger *slog.Logger) toolDeps { var deps toolDeps // Knowledge store if cfg.Tools.Knowledge.Enabled { knowledgeDir := cfg.Tools.Knowledge.Dir if knowledgeDir == "" { knowledgeDir = filepath.Join("agents", cfg.Agent.ID, "knowledge") } knowledgeDBPath := filepath.Join(dataBase, "knowledge.db") kStore, kErr := shellknowledge.New(knowledgeDir, knowledgeDBPath, logger) if kErr != nil { logger.Error("knowledge_store_init_failed", "err", kErr) } else { if syncErr := kStore.Sync(context.Background()); syncErr != nil { logger.Error("knowledge_sync_failed", "err", syncErr) } deps.kStore = kStore } } // Shared knowledge store if cfg.Tools.SharedKnowledge.Enabled { sharedDir := cfg.Tools.SharedKnowledge.Dir if sharedDir == "" { sharedDir = "knowledges" } sharedDBPath := cfg.Tools.SharedKnowledge.DBPath if sharedDBPath == "" { sharedDBPath = "knowledges/data/knowledge.db" } sharedKStore, skErr := shellknowledge.New(sharedDir, sharedDBPath, logger) if skErr != nil { logger.Error("shared_knowledge_store_init_failed", "err", skErr) } else { if syncErr := sharedKStore.Sync(context.Background()); syncErr != nil { logger.Error("shared_knowledge_sync_failed", "err", syncErr) } logger.Info("shared knowledge enabled", "dir", sharedDir, "db", sharedDBPath) deps.sharedKStore = sharedKStore } } // MCP client manager — connects to external MCP servers if cfg.Tools.MCP.Enabled && len(cfg.Tools.MCP.Servers) > 0 { mcpManager, mcpErr := shellmcp.NewManager(context.Background(), cfg.Tools.MCP.Servers, logger) if mcpErr != nil { logger.Error("mcp_manager_init_failed", "err", mcpErr) } else { logger.Info("mcp manager initialized", "servers", len(cfg.Tools.MCP.Servers)) deps.mcpManager = mcpManager } } // Skills loader if cfg.Skills.Enabled { skillsPath := cfg.Skills.SkillsPath if skillsPath == "" { skillsPath = "skills/" } deps.skillLoader = shellskills.NewLoader(skillsPath) // Skills executor for scripts allowedInterpreters := cfg.Tools.Skills.AllowedInterpreters timeout := cfg.Skills.Timeout if timeout == 0 { timeout = 60 * time.Second } deps.skillExecutor = shellskills.NewExecutor(allowedInterpreters, timeout) logger.Info("skills enabled", "path", skillsPath, "categories", cfg.Skills.Categories) } return deps } // initRateLimiter configures the rate limiter on the tool registry if enabled. func initRateLimiter(cfg *config.AgentConfig, toolReg *tools.Registry, logger *slog.Logger) { if !cfg.Security.ToolRateLimit.Enabled { return } maxCalls := cfg.Security.ToolRateLimit.MaxCallsPerMin if maxCalls <= 0 { maxCalls = 10 } rl := tools.NewRateLimiter(maxCalls, time.Minute) toolReg.SetRateLimiter(rl) cleanupInterval := cfg.Security.ToolRateLimit.CleanupIntervalS if cleanupInterval <= 0 { cleanupInterval = 60 } go func() { ticker := time.NewTicker(time.Duration(cleanupInterval) * time.Second) defer ticker.Stop() for range ticker.C { rl.Cleanup() } }() logger.Info("tool rate limiting enabled", "max_calls_per_min", maxCalls) } // buildToolRegistry creates a Registry with tools enabled in the agent's config. func buildToolRegistry( cfg *config.AgentConfig, sshExec *ssh.Executor, matrixClient *matrix.Client, memStore memory.Store, kStore *shellknowledge.FileStore, sharedKStore *shellknowledge.FileStore, mcpManager *shellmcp.Manager, skillLoader *shellskills.Loader, skillExecutor *shellskills.Executor, roomCtx *toolmemory.RoomContext, logger *slog.Logger, ) *tools.Registry { reg := tools.NewRegistry(logger) if cfg.Tools.HTTP.Enabled { reg.Register(toolhttp.NewHTTPGet(cfg.Tools.HTTP)) reg.Register(toolhttp.NewHTTPPost(cfg.Tools.HTTP)) logger.Debug("registered http tools") } if cfg.Tools.SSH.Enabled { reg.Register(toolssh.NewSSHCommand(cfg.Tools.SSH, sshExec)) logger.Debug("registered ssh tool") } if cfg.Tools.FileOps.Enabled { reg.Register(toolfile.NewReadFile(cfg.Tools.FileOps)) reg.Register(toolfile.NewListDirectory(cfg.Tools.FileOps)) if !cfg.Tools.FileOps.ReadOnly { reg.Register(toolfile.NewWriteFile(cfg.Tools.FileOps)) reg.Register(toolfile.NewAppendFile(cfg.Tools.FileOps)) reg.Register(toolfile.NewDeleteFile(cfg.Tools.FileOps)) } logger.Debug("registered file tools") } // current_time is always available reg.Register(toolclock.NewCurrentTime()) logger.Debug("registered current_time tool") // weather tool is always available reg.Register(toolweather.NewWeather()) logger.Debug("registered weather tool") // wikipedia_search tool is always available reg.Register(toolwikipedia.NewWikipediaSearch()) logger.Debug("registered wikipedia_search tool") // imdb tool (enabled via config) if cfg.Tools.IMDb.Enabled { reg.Register(toolimdb.NewIMDbSearch(cfg.Tools.IMDb)) logger.Debug("registered imdb tool") } // exchange rate tools (enabled via config) if cfg.Tools.ExchangeRate.Enabled { if t, err := toolexchange.NewExchangeRateGet(cfg.Tools.ExchangeRate); err != nil { logger.Warn("exchange_rate_get disabled: API key not configured", "err", err) } else { reg.Register(t) } if t, err := toolexchange.NewExchangeRateConvert(cfg.Tools.ExchangeRate); err != nil { logger.Warn("exchange_rate_convert disabled: API key not configured", "err", err) } else { reg.Register(t) } if t, err := toolexchange.NewExchangeRateList(cfg.Tools.ExchangeRate); err != nil { logger.Warn("exchange_rate_list disabled: API key not configured", "err", err) } else { reg.Register(t) } if t, err := toolexchange.NewExchangeRateHistorical(cfg.Tools.ExchangeRate); err != nil { logger.Warn("exchange_rate_historical disabled: API key not configured", "err", err) } else { reg.Register(t) } logger.Debug("registered exchange rate tools") } // matrix_send is always available reg.Register(toolmatrix.NewMatrixSend(matrixClient, cfg.Tools.Matrix)) logger.Debug("registered matrix tool") // Memory tools (memory_clear_context registered later since it needs the Agent) if cfg.Tools.Memory.Enabled && memStore != nil { reg.Register(toolmemory.NewMemorySave(cfg.Agent.ID, memStore)) reg.Register(toolmemory.NewMemoryRecall(cfg.Agent.ID, memStore)) reg.Register(toolmemory.NewMemoryForget(cfg.Agent.ID, memStore)) reg.Register(toolmemory.NewMemorySummary(cfg.Agent.ID, memStore)) logger.Debug("registered memory tools") } // Knowledge tools if cfg.Tools.Knowledge.Enabled && kStore != nil { reg.Register(toolknowledge.NewKnowledgeSearch(kStore)) reg.Register(toolknowledge.NewKnowledgeRead(kStore)) reg.Register(toolknowledge.NewKnowledgeWrite(kStore)) reg.Register(toolknowledge.NewKnowledgeList(kStore)) logger.Debug("registered knowledge tools") } // Shared knowledge tools if cfg.Tools.SharedKnowledge.Enabled && sharedKStore != nil { sharedTools := toolknowledge.NewSharedKnowledgeTools(sharedKStore) for _, tool := range sharedTools { reg.Register(tool) } logger.Debug("registered shared knowledge tools", "count", len(sharedTools)) } // MCP tools — register tools from all connected MCP servers if mcpManager != nil { for serverName, mcpClient := range mcpManager.AllClients() { // Find the config for this server to get prefix, filter, timeout var serverCfg *config.MCPServerCfg for i := range cfg.Tools.MCP.Servers { if cfg.Tools.MCP.Servers[i].Name == serverName { serverCfg = &cfg.Tools.MCP.Servers[i] break } } if serverCfg == nil { logger.Warn("no config found for MCP server", "name", serverName) continue } // Convert and register MCP tools mcpTools := toolmcp.FromMCPServer(mcpClient, serverCfg.Prefix, serverCfg.Tools, serverCfg.Timeout, logger) for _, tool := range mcpTools { reg.Register(tool) } logger.Debug("registered MCP tools", "server", serverName, "count", len(mcpTools)) } } // Skills tools — register skill search, load, read, and run tools if skillLoader != nil { reg.Register(toolskills.NewSkillSearch(skillLoader, cfg.Skills.Categories)) reg.Register(toolskills.NewSkillLoad(skillLoader)) reg.Register(toolskills.NewSkillReadResource(skillLoader)) if skillExecutor != nil { reg.Register(toolskills.NewSkillRunScript(skillLoader, skillExecutor)) } logger.Debug("registered skills tools") } return reg } // resolveDataBase returns the base directory for agent runtime data. // Priority: config storage.base_path > $AGENTS_DATA_DIR/ > agents//data func resolveDataBase(cfg *config.AgentConfig) string { if cfg.Storage.BasePath != "" { return cfg.Storage.BasePath } if envDir := os.Getenv("AGENTS_DATA_DIR"); envDir != "" { return filepath.Join(envDir, cfg.Agent.ID) } return filepath.Join("agents", cfg.Agent.ID, "data") }