import type { Toolkit } from '@langchain/classic/agents'; import { StructuredTool, Tool } from '@langchain/core/tools'; import { buildResponseMetadata, processHitlResponses } from '@utils/agent-execution'; import { extractHitlMetadata, hasGatedToolNodeName, } from '@utils/agent-execution/createEngineRequests'; import type { RequestResponseMetadata } from '@utils/agent-execution/types'; import get from 'lodash/get'; import type { EngineRequest, EngineResponse, IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, NodeOutput, } from 'n8n-workflow'; import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; import { executeTool } from './utils/executeTool'; import { convertValueBySchema } from './utils/convertToSchema'; import { ZodObject } from 'zod'; export class ToolExecutor implements INodeType { description: INodeTypeDescription = { displayName: 'Tool Executor', name: 'toolExecutor', version: 1, defaults: { name: 'Tool Executor', }, hidden: true, inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.AiTool], outputs: [NodeConnectionTypes.Main], builderHint: { inputs: { ai_tool: { required: true }, }, }, properties: [ { displayName: 'Query', name: 'query', type: 'json', default: '{}', description: 'Key-value pairs, where key is the name of the tool name and value is the parameters to pass to the tool', }, { displayName: 'Tool Name', name: 'toolName', type: 'string', default: '', description: 'Name of the tool to execute if the connected tool is a toolkit', }, { displayName: 'Node', name: 'node', type: 'string', default: '', description: 'Name of the node that is being executed', }, ], group: ['transform'], description: 'Node to execute tools without an AI Agent', }; async execute( this: IExecuteFunctions, response?: EngineResponse, ): Promise { // Process HITL (Human-in-the-Loop) tool responses before running the agent // If there are approved HITL tools, we need to execute the gated tools first const hitlResult = processHitlResponses(response, 0); if (hitlResult.hasApprovedHitlTools && hitlResult.pendingGatedToolRequest) { // Return the gated tool request immediately // The Agent will resume after the gated tool executes return hitlResult.pendingGatedToolRequest; } const query = this.getNodeParameter('query', 0, {}) as string | object; const toolName = this.getNodeParameter('toolName', 0, '') as string; const node = this.getNodeParameter('node', 0, '') as string; let parsedQuery: Record; try { parsedQuery = typeof query === 'string' ? JSON.parse(query) : query; } catch (error) { throw new NodeOperationError( this.getNode(), `Failed to parse query: ${(error as Error).message}`, ); } const getQueryData = (name: string) => { // node names in query may have underscores in place of spaces, use it for accessing the query data. return (get(parsedQuery, name, null) ?? get(parsedQuery, name.replaceAll(' ', '_'), null)) as | Record | string | null; }; const resultData: INodeExecutionData[] = []; const toolInputs = await this.getInputConnectionData(NodeConnectionTypes.AiTool, 0); if (!toolInputs || !Array.isArray(toolInputs)) { throw new NodeOperationError(this.getNode(), 'No tool inputs found'); } try { for (const tool of toolInputs) { // Handle toolkits if (tool && typeof (tool as Toolkit).getTools === 'function') { const toolsInToolkit = (tool as Toolkit).getTools(); for (const toolkitTool of toolsInToolkit) { if (!(toolkitTool instanceof Tool || toolkitTool instanceof StructuredTool)) { continue; } if (toolName === toolkitTool.name) { if (hasGatedToolNodeName(toolkitTool.metadata) && node) { const toolInput: { toolParameters: unknown } = { toolParameters: getQueryData(toolName) ?? {}, }; const hitlInput = getQueryData(node); if (typeof hitlInput === 'string') { throw new NodeOperationError( this.getNode(), `Invalid hitl input for tool ${toolkitTool.name}`, ); } // handle code tool which uses a string input, but it should be converted to an object const requiresObjectInput = toolkitTool.metadata.originalSchema && toolkitTool.metadata.originalSchema instanceof ZodObject; if (typeof toolInput.toolParameters === 'string' && requiresObjectInput) { toolInput.toolParameters = convertValueBySchema( toolInput.toolParameters, toolkitTool.metadata.originalSchema, ); } const hitlMetadata = extractHitlMetadata( toolkitTool.metadata, toolkitTool.name, toolInput as IDataObject, ); // prepare request for execution engine to execute the HITL node const engineRequest: EngineRequest['actions'] = [ { actionType: 'ExecutionNodeAction' as const, nodeName: node, input: { tool: toolName, toolParameters: toolInput.toolParameters as IDataObject, ...hitlInput, }, type: 'ai_tool', id: crypto.randomUUID(), metadata: { itemIndex: 0, hitl: hitlMetadata, }, }, ]; return { actions: engineRequest, metadata: buildResponseMetadata(response, 0), }; } const result = await executeTool(toolkitTool, getQueryData(toolName) ?? {}); resultData.push(result); } } } else { // Handle single tool if (!toolName || toolName === tool.name) { const toolInput = getQueryData(toolName || tool.name); const result = await executeTool(tool, toolInput ?? {}); resultData.push(result); } } } } catch (error) { throw new NodeOperationError( this.getNode(), `Error executing tool: ${(error as Error).message}`, ); } return [resultData]; } }