diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index ff1a4f92dc..1c1e2cf4b9 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -546,6 +546,10 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to const areToolsEnabled = checkCapability(AgentCapabilities.tools); const actionsEnabled = checkCapability(AgentCapabilities.actions); const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools); + const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools); + const codeExecutionEnabled = + agent.tools?.includes(Tools.execute_code) === true && + enabledCapabilities.has(AgentCapabilities.execute_code); const filteredTools = agent.tools?.filter((tool) => { if (tool === Tools.file_search) { @@ -721,6 +725,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to tools: filteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, + programmaticToolsEnabled, + codeExecutionEnabled, }, { isBuiltInTool, @@ -775,6 +781,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to tools: filteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, + programmaticToolsEnabled, + codeExecutionEnabled, }, { isBuiltInTool, @@ -1003,6 +1011,10 @@ async function loadAgentTools({ /** Build tool registry from MCP tools and create PTC/tool search tools if configured */ const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools); + const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools); + const codeExecutionEnabled = + agent.tools?.includes(Tools.execute_code) === true && + enabledCapabilities.has(AgentCapabilities.execute_code); const { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools } = await buildToolClassification({ loadedTools, @@ -1010,6 +1022,8 @@ async function loadAgentTools({ agentId: agent.id, agentToolOptions: agent.tool_options, deferredToolsEnabled, + programmaticToolsEnabled, + codeExecutionEnabled, authHeaders: () => getCodeApiAuthHeaders(req), }); @@ -1256,17 +1270,26 @@ async function loadToolsForExecution({ const allLoadedTools = []; const configurable = { userMCPAuthMap }; - if (actionsEnabled === undefined) { - const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id); - actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions); - } - const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH); const ptcToolNames = [ AgentConstants.BASH_PROGRAMMATIC_TOOL_CALLING, AgentConstants.PROGRAMMATIC_TOOL_CALLING, ].filter((name) => toolNames.includes(name)); - const isPTC = ptcToolNames.length > 0; + const isPTCRequested = ptcToolNames.length > 0; + + let enabledCapabilities; + if (actionsEnabled === undefined || isPTCRequested) { + enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id); + } + if (actionsEnabled === undefined) { + actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions); + } + + const isPTC = + isPTCRequested && + enabledCapabilities.has(AgentCapabilities.programmatic_tools) && + enabledCapabilities.has(AgentCapabilities.execute_code) && + agent?.tools?.includes(Tools.execute_code) === true; logger.debug( `[loadToolsForExecution] isToolSearch: ${isToolSearch}, toolRegistry: ${toolRegistry?.size ?? 'undefined'}`, diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js index 506a21c143..be066d6792 100644 --- a/api/server/services/__tests__/ToolService.spec.js +++ b/api/server/services/__tests__/ToolService.spec.js @@ -300,14 +300,20 @@ describe('ToolService - Action Capability Gating', () => { const actionToolName = `get_weather${actionDelimiter}api_example_com`; const regularTool = Tools.web_search; - it('loads bash PTC under the legacy programmatic tool name for event-driven compatibility', async () => { - const req = createMockReq([]); + it('loads bash PTC under the legacy programmatic tool name when code capabilities are enabled', async () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.programmatic_tools, + AgentCapabilities.execute_code, + ]; + const req = createMockReq(capabilities); const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); const result = await loadToolsForExecution({ req, res: {}, - agent: { id: 'agent_ptc' }, + agent: { id: 'agent_ptc', tools: [Tools.execute_code] }, toolNames: [Constants.PROGRAMMATIC_TOOL_CALLING], toolRegistry, actionsEnabled: false, @@ -320,6 +326,50 @@ describe('ToolService - Action Capability Gating', () => { expect(result.configurable.ptcToolMap.size).toBe(0); }); + it('does not load PTC when programmatic tools capability is disabled', async () => { + const capabilities = [AgentCapabilities.tools, AgentCapabilities.execute_code]; + const req = createMockReq(capabilities); + const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + const result = await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_ptc', tools: [Tools.execute_code] }, + toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING], + toolRegistry, + actionsEnabled: false, + }); + + expect(result.loadedTools.map((tool) => tool.name)).toEqual([]); + expect(result.configurable.toolRegistry).toBeUndefined(); + expect(result.configurable.ptcToolMap).toBeUndefined(); + }); + + it('does not load PTC when agent did not request execute_code', async () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.programmatic_tools, + AgentCapabilities.execute_code, + ]; + const req = createMockReq(capabilities); + const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + const result = await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_ptc', tools: [] }, + toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING], + toolRegistry, + actionsEnabled: false, + }); + + expect(result.loadedTools.map((tool) => tool.name)).toEqual([]); + expect(result.configurable.toolRegistry).toBeUndefined(); + expect(result.configurable.ptcToolMap).toBeUndefined(); + }); + it('should skip action tool loading when actionsEnabled=false', async () => { const req = createMockReq([]); req.config = {}; diff --git a/packages/api/src/tools/classification.spec.ts b/packages/api/src/tools/classification.spec.ts index 690d6e7be1..66cdfd4611 100644 --- a/packages/api/src/tools/classification.spec.ts +++ b/packages/api/src/tools/classification.spec.ts @@ -308,6 +308,8 @@ describe('classification.ts', () => { agentId: 'agent1', agentToolOptions, deferredToolsEnabled: true, + programmaticToolsEnabled: true, + codeExecutionEnabled: true, definitionsOnly: true, }); @@ -327,6 +329,8 @@ describe('classification.ts', () => { agentId: 'agent1', agentToolOptions, deferredToolsEnabled: true, + programmaticToolsEnabled: true, + codeExecutionEnabled: true, definitionsOnly: true, }); @@ -334,7 +338,7 @@ describe('classification.ts', () => { expect(result.toolRegistry?.has('tool_search')).toBe(true); }); - it('should still add PTC definition when definitionsOnly=true and has programmatic tools', async () => { + it('should add PTC definition when definitionsOnly=true and capabilities allow programmatic tools', async () => { const loadedTools: GenericTool[] = [createMCPTool('tool1')]; const agentToolOptions: AgentToolOptions = { @@ -347,6 +351,8 @@ describe('classification.ts', () => { agentId: 'agent1', agentToolOptions, deferredToolsEnabled: true, + programmaticToolsEnabled: true, + codeExecutionEnabled: true, definitionsOnly: true, }); @@ -355,7 +361,7 @@ describe('classification.ts', () => { expect(result.additionalTools.length).toBe(0); }); - it('should create bash PTC tool when agent has programmatic tools', async () => { + it('should create bash PTC tool when capabilities allow programmatic tools', async () => { const loadedTools: GenericTool[] = [createMCPTool('tool1')]; const agentToolOptions: AgentToolOptions = { @@ -367,6 +373,8 @@ describe('classification.ts', () => { userId: 'user1', agentId: 'agent1', agentToolOptions, + programmaticToolsEnabled: true, + codeExecutionEnabled: true, }); expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(true); @@ -374,6 +382,42 @@ describe('classification.ts', () => { expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true); }); + it('should not add PTC when programmatic tools capability is disabled', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions: { + tool1: { allowed_callers: ['code_execution'] }, + }, + codeExecutionEnabled: true, + }); + + expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false); + expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false); + expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false); + }); + + it('should not add PTC when code execution is not enabled for the agent', async () => { + const loadedTools: GenericTool[] = [createMCPTool('tool1')]; + + const result = await buildToolClassification({ + loadedTools, + userId: 'user1', + agentId: 'agent1', + agentToolOptions: { + tool1: { allowed_callers: ['code_execution'] }, + }, + programmaticToolsEnabled: true, + }); + + expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false); + expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false); + expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false); + }); + it('should create tool instances when definitionsOnly=false (default)', async () => { const loadedTools: GenericTool[] = [createMCPTool('tool1')]; diff --git a/packages/api/src/tools/classification.ts b/packages/api/src/tools/classification.ts index 9d539e0e23..eaf4ff0953 100644 --- a/packages/api/src/tools/classification.ts +++ b/packages/api/src/tools/classification.ts @@ -185,6 +185,10 @@ export interface BuildToolClassificationParams { agentToolOptions?: AgentToolOptions; /** Whether the deferred_tools capability is enabled (from agent config) */ deferredToolsEnabled?: boolean; + /** Whether the programmatic_tools capability is enabled (from agent config) */ + programmaticToolsEnabled?: boolean; + /** Whether code execution is enabled and requested by this agent */ + codeExecutionEnabled?: boolean; /** When true, skip creating tool instances (for event-driven mode) */ definitionsOnly?: boolean; /** Optional host-supplied Code API auth headers for remote programmatic execution. */ @@ -238,7 +242,7 @@ export function agentHasDeferredTools(toolRegistry: LCToolRegistry): boolean { * 1. Filters loaded tools for MCP tools * 2. Extracts tool definitions and builds the registry from agent's tool_options * 3. Cleans up temporary mcpJsonSchema properties - * 4. Creates PTC tool only if agent has tools configured for programmatic calling + * 4. Creates PTC tool only if capabilities allow the agent's programmatic tools * 5. Creates tool search tool only if agent has deferred tools * * @param params - Parameters including loaded tools, userId, agentId, agentToolOptions, and dependencies @@ -253,6 +257,8 @@ export async function buildToolClassification( agentToolOptions, definitionsOnly = false, deferredToolsEnabled = true, + programmaticToolsEnabled = false, + codeExecutionEnabled = false, authHeaders, } = params; const additionalTools: GenericTool[] = []; @@ -275,10 +281,11 @@ export async function buildToolClassification( /** * Check if this agent actually has tools configured for these features. - * Only enable PTC if the agent has programmatic tools. + * Only enable PTC if code/programmatic capabilities allow the agent's programmatic tools. * Only enable tool search if the agent has deferred tools AND the capability is enabled. */ - const hasProgrammaticTools = agentHasProgrammaticTools(toolRegistry); + const hasProgrammaticTools = + programmaticToolsEnabled && codeExecutionEnabled && agentHasProgrammaticTools(toolRegistry); const hasDeferredTools = deferredToolsEnabled && agentHasDeferredTools(toolRegistry); /** Clear defer_loading if capability disabled */ diff --git a/packages/api/src/tools/definitions.spec.ts b/packages/api/src/tools/definitions.spec.ts index e297024ddf..4dc1acdf83 100644 --- a/packages/api/src/tools/definitions.spec.ts +++ b/packages/api/src/tools/definitions.spec.ts @@ -309,6 +309,66 @@ describe('definitions.ts', () => { }); }); + describe('programmatic tool calling capability gate', () => { + const mcpToolName = 'run_report_mcp_server_one'; + const serverTools = { + [mcpToolName]: { + function: { + name: mcpToolName, + description: 'Run report', + parameters: { type: 'object', properties: {} }, + }, + }, + }; + + it('does not add Bash PTC definitions unless both code and programmatic capabilities are enabled', async () => { + mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools); + + const result = await loadToolDefinitions( + { + userId: 'user-123', + agentId: 'agent-123', + tools: [mcpToolName], + toolOptions: { + [mcpToolName]: { allowed_callers: ['code_execution'] }, + }, + codeExecutionEnabled: true, + }, + { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + }, + ); + + expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false); + expect(result.toolRegistry.has('run_tools_with_bash')).toBe(false); + }); + + it('adds Bash PTC definitions when code and programmatic capabilities are enabled', async () => { + mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools); + + const result = await loadToolDefinitions( + { + userId: 'user-123', + agentId: 'agent-123', + tools: [mcpToolName], + toolOptions: { + [mcpToolName]: { allowed_callers: ['code_execution'] }, + }, + programmaticToolsEnabled: true, + codeExecutionEnabled: true, + }, + { + getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools, + isBuiltInTool: mockIsBuiltInTool, + }, + ); + + expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true); + expect(result.toolRegistry.has('run_tools_with_bash')).toBe(true); + }); + }); + describe('MCP tool definitions with server name variants', () => { it('should load MCP tools with underscored server names (server_one)', async () => { const mockServerTools = { diff --git a/packages/api/src/tools/definitions.ts b/packages/api/src/tools/definitions.ts index d56c299304..7f82bc06ec 100644 --- a/packages/api/src/tools/definitions.ts +++ b/packages/api/src/tools/definitions.ts @@ -35,6 +35,10 @@ export interface LoadToolDefinitionsParams { toolOptions?: AgentToolOptions; /** Whether deferred tools feature is enabled */ deferredToolsEnabled?: boolean; + /** Whether programmatic tool calling is enabled */ + programmaticToolsEnabled?: boolean; + /** Whether code execution is enabled and requested by this agent */ + codeExecutionEnabled?: boolean; } export interface ActionToolDefinition { @@ -71,7 +75,15 @@ export async function loadToolDefinitions( params: LoadToolDefinitionsParams, deps: LoadToolDefinitionsDeps, ): Promise { - const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params; + const { + userId, + agentId, + tools, + toolOptions = {}, + deferredToolsEnabled = false, + programmaticToolsEnabled = false, + codeExecutionEnabled = false, + } = params; const { getOrFetchMCPServerTools, isBuiltInTool, getActionToolDefinitions } = deps; const emptyResult: LoadToolDefinitionsResult = { @@ -191,6 +203,8 @@ export async function loadToolDefinitions( agentId, loadedTools, deferredToolsEnabled, + programmaticToolsEnabled, + codeExecutionEnabled, definitionsOnly: true, agentToolOptions: toolOptions, });