import type { ToolContext, ToolPlugin, ToolResult } from "../tools/types.js"; import { bashTool } from "../tools/bashTool.js"; import { taskStopTool } from "../tools/taskStopTool.js"; import { editTool } from "../tools/editTool.js"; import { writeTool } from "../tools/writeTool.js"; import { exitPlanModeTool } from "../tools/exitPlanMode.js"; import { enterPlanModeTool } from "../tools/enterPlanMode.js"; import { askUserQuestionTool } from "../tools/askUserQuestion.js"; import { cronCreateTool } from "../tools/cronCreateTool.js"; import { cronDeleteTool } from "../tools/cronDeleteTool.js"; import { cronListTool } from "../tools/cronListTool.js"; import { webFetchTool } from "../tools/webFetchTool.js"; // New tools import { globTool } from "../tools/globTool.js"; import { grepTool } from "../tools/grepTool.js"; import { readTool } from "../tools/readTool.js"; import { lspTool } from "../tools/lspTool.js"; import { agentTool } from "../tools/agentTool.js"; import { skillTool } from "../tools/skillTool.js"; import { taskCreateTool, taskGetTool, taskUpdateTool, taskListTool, } from "../tools/taskManagementTools.js"; import { enterWorktreeTool } from "../tools/enterWorktreeTool.js"; import { exitWorktreeTool } from "../tools/exitWorktreeTool.js"; import { workflowTool } from "../tools/workflowTool.js"; import { McpManager } from "./mcpManager.js"; import { PermissionManager } from "./permissionManager.js"; import { ChatCompletionFunctionTool } from "openai/resources.js"; import type { PermissionMode, PermissionCallback, ILspManager, } from "../types/index.js"; import type { SubagentManager } from "./subagentManager.js"; import type { SkillManager } from "./skillManager.js"; import { ReversionManager } from "./reversionManager.js"; import * as aiService from "../services/aiService.js"; import { Container } from "../utils/container.js"; import { logger } from "../utils/globalLogger.js"; import type { SubagentConfiguration } from "../utils/subagentParser.js"; import type { SkillMetadata } from "../types/skills.js"; import { startToolSpan, endToolSpan } from "../telemetry/sessionTracing.js"; export interface ToolManagerOptions { container: Container; /** Optional list of tool names to enable */ tools?: string[]; /** Custom tools to register alongside built-in tools */ customTools?: ToolPlugin[]; } /** * Tool Manager * * Manages tool registration and execution with optional permission system integration. * Supports both built-in tools and MCP (Model Context Protocol) tools. */ class ToolManager { private toolsRegistry = new Map(); private tools?: string[]; private customTools?: ToolPlugin[]; private container: Container; constructor(options: ToolManagerOptions) { this.container = options.container; this.tools = options.tools; this.customTools = options.customTools; } private get mcpManager(): McpManager { return this.container.get("McpManager")!; } /** * Register a new tool */ public register(tool: ToolPlugin): void { this.toolsRegistry.set(tool.name, tool); } /** * Initialize built-in tools. Can be called with dependencies for tools that require them. * * This method can be called multiple times safely. When called without dependencies, * it registers basic tools (Bash, Read, Write, TaskCreate, etc.). When called with * dependencies, it also registers tools that require managers (Agent, Skill). * * @param deps Optional dependencies for advanced tools * @param deps.subagentManager SubagentManager instance for Agent tool * @param deps.skillManager SkillManager instance for Skill tool * * @example * ```typescript * // Initialize basic tools only * toolManager.initializeBuiltInTools(); * * // Initialize all tools including those requiring dependencies * toolManager.initializeBuiltInTools({ * subagentManager: mySubagentManager, * skillManager: mySkillManager * }); * ``` */ public initializeBuiltInTools(): void { const builtInTools = [ bashTool, taskStopTool, editTool, writeTool, exitPlanModeTool, enterPlanModeTool, askUserQuestionTool, globTool, grepTool, readTool, lspTool, agentTool, skillTool, taskCreateTool, taskGetTool, taskUpdateTool, taskListTool, cronCreateTool, cronDeleteTool, cronListTool, webFetchTool, enterWorktreeTool, exitWorktreeTool, workflowTool, ]; for (const tool of builtInTools) { if (this.shouldEnableTool(tool.name)) { this.toolsRegistry.set(tool.name, tool); } } // Register custom tools for (const tool of this.customTools || []) { if (this.shouldEnableTool(tool.name)) { this.toolsRegistry.set(tool.name, tool); } } } /** * Check if a tool should be enabled based on tools configuration and permission rules */ private shouldEnableTool(name: string): boolean { const permissionManager = this.container.get("PermissionManager"); // If tool is explicitly denied by name in permission rules, filter it out if (permissionManager?.isToolDenied(name)) { return false; } if (!this.tools) { return true; } return this.tools.some( (toolName) => toolName.toLowerCase() === name.toLowerCase(), ); } /** * Execute a tool by name with the provided arguments and context * * Enhances the context with permission-related fields before execution: * - permissionMode: The current permission mode (default or bypassPermissions) * - canUseToolCallback: Custom permission callback if provided * - permissionManager: The PermissionManager instance for permission checks * * @param name - Name of the tool to execute * @param args - Arguments to pass to the tool * @param context - Execution context for the tool * @returns Promise resolving to the tool execution result */ async execute( name: string, args: Record, context: ToolContext, ): Promise { const permissionManager = this.container.get("PermissionManager"); const permissionMode = this.container.has("PermissionMode") ? this.container.get("PermissionMode") : undefined; // Resolve effective permission mode (CLI override > configuration > default) const effectivePermissionMode = permissionManager ? permissionManager.getCurrentEffectiveMode(permissionMode) : permissionMode || "default"; // Enhance context with permission-related fields const canUseToolCallback = this.container.has("CanUseToolCallback") ? this.container.get("CanUseToolCallback") : undefined; const enhancedContext: ToolContext = { ...context, permissionMode: effectivePermissionMode, canUseToolCallback, permissionManager, toolManager: this, taskManager: this.container.get( "TaskManager", )!, reversionManager: this.container.get("ReversionManager")!, backgroundTaskManager: this.container.get< import("./backgroundTaskManager.js").BackgroundTaskManager >("BackgroundTaskManager")!, foregroundTaskManager: this.container.get< import("../types/processes.js").IForegroundTaskManager >("ForegroundTaskManager")!, mcpManager: this.mcpManager, lspManager: this.container.get("LspManager")!, subagentManager: this.container.has("SubagentManager") ? this.container.get("SubagentManager") : undefined, skillManager: this.container.has("SkillManager") ? this.container.get("SkillManager") : undefined, cronManager: this.container.has("CronManager") ? this.container.get( "CronManager", ) : undefined, aiManager: this.container.has("AIManager") ? this.container.get("AIManager") : undefined, aiService: aiService, messageManager: this.container.get( "MessageManager", )!, hookManager: this.container.has("HookManager") ? this.container.get( "HookManager", ) : undefined, workflowManager: this.container.has("WorkflowManager") ? this.container.get( "WorkflowManager", ) : undefined, sessionId: context.sessionId, toolCallId: context.toolCallId, }; // OpenTelemetry: start tool span startToolSpan(name, args); const toolStartTime = Date.now(); logger?.debug("Executing tool with enhanced context", { toolName: name, permissionMode, effectivePermissionMode, hasPermissionManager: !!permissionManager, }); // Check if it's an MCP tool first if (this.mcpManager.isMcpTool(name)) { try { const result = await this.mcpManager.executeMcpToolByRegistry( name, args, enhancedContext, ); endToolSpan({ success: result.success, durationMs: Date.now() - toolStartTime, output: result.content, }); return result; } catch (error) { endToolSpan({ success: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - toolStartTime, }); return { success: false, content: "", error: error instanceof Error ? error.message : String(error), }; } } // Check built-in tools const plugin = this.toolsRegistry.get(name); if (plugin) { try { const result = await plugin.execute(args, enhancedContext); endToolSpan({ success: result.success, durationMs: Date.now() - toolStartTime, output: result.content, }); return result; } catch (error) { logger?.error("Tool execution failed", { toolName: name, error: error instanceof Error ? error.message : String(error), }); endToolSpan({ success: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - toolStartTime, }); return { success: false, content: "", error: error instanceof Error ? error.message : String(error), }; } } logger?.warn("Tool not found", { toolName: name }); endToolSpan({ success: false, error: `Tool '${name}' not found`, durationMs: Date.now() - toolStartTime, }); return { success: false, content: "", error: `Tool '${name}' not found`, }; } list(): ToolPlugin[] { const permissionManager = this.container.get("PermissionManager"); const builtInTools = Array.from(this.toolsRegistry.values()).filter( (tool) => !permissionManager?.isToolDenied(tool.name), ); const mcpTools = this.mcpManager .getMcpToolPlugins() .filter((tool) => !permissionManager?.isToolDenied(tool.name)); return [...builtInTools, ...mcpTools]; } getToolsConfig(options?: { availableSubagents?: SubagentConfiguration[]; availableSkills?: SkillMetadata[]; workdir?: string; isSubagent?: boolean; }): ChatCompletionFunctionTool[] { const permissionManager = this.container.get("PermissionManager"); const builtInToolsConfig = Array.from(this.toolsRegistry.values()) .filter((tool) => { // If tool is explicitly denied by name in permission rules, filter it out if (permissionManager?.isToolDenied(tool.name)) { return false; } return true; }) .map((tool) => { // Create a copy of the tool config to avoid modifying the original const config = { ...tool.config, function: { ...tool.config.function, }, }; // Override description with prompt if available if (tool.prompt) { config.function.description = tool.prompt(options); } return config; }); const mcpToolsConfig = this.mcpManager .getMcpToolsConfig() .filter((tool) => { if (permissionManager?.isToolDenied(tool.function.name)) { return false; } return true; }); return [...builtInToolsConfig, ...mcpToolsConfig]; } /** * Get the list of registered tool plugins */ public getTools(): ToolPlugin[] { return Array.from(this.toolsRegistry.values()); } /** * Check whether a tool is safe to execute in parallel with other tools. * Built-in tools default to safe (parallel); Edit and Write opt out. * MCP tools are conservatively non-safe (can't inspect side effects). */ public isConcurrencySafe(name: string): boolean { // MCP tools are conservatively non-safe if (this.mcpManager.isMcpTool(name)) { return false; } const plugin = this.toolsRegistry.get(name); if (plugin) { return plugin.isConcurrencySafe ?? true; } // Unknown tools are conservatively non-safe return false; } /** * Get the current permission mode */ public getPermissionMode(): PermissionMode { const permissionManager = this.container.get("PermissionManager"); const permissionMode = this.container.has("PermissionMode") ? this.container.get("PermissionMode") : undefined; if (permissionManager) { return permissionManager.getCurrentEffectiveMode(permissionMode); } return permissionMode || "default"; } /** * Set the permission mode * @param mode - The new permission mode */ public setPermissionMode(mode: PermissionMode): void { this.container.register("PermissionMode", mode); } /** * Request a full permission mode transition (planManager + UI callback). * Used by tools like EnterPlanMode in bypass mode where the canUseTool * callback (which normally handles the transition) is not invoked. * In normal mode, the callback already handles the transition. */ public requestPermissionModeChange(mode: PermissionMode): void { if (this.container.has("PermissionModeTransition")) { const transition = this.container.get<(mode: PermissionMode) => void>( "PermissionModeTransition", ); if (transition) { transition(mode); return; } } this.setPermissionMode(mode); } /** * Get the permission manager */ public getPermissionManager(): PermissionManager | undefined { return this.container.get("PermissionManager"); } /** * Get the task manager */ public getTaskManager(): | import("../services/taskManager.js").TaskManager | undefined { return this.container.get( "TaskManager", ); } } // Export tool registry class and types export { ToolManager };