Skip to content

Commit a229590

Browse files
OlegIvanivByteEVM
authored andcommitted
fix(core): Fix AI Agent v3 Tool Execution Issues (n8n-io#21477)
Signed-off-by: Oleg Ivaniv <[email protected]>
1 parent 5bc63ef commit a229590

File tree

3 files changed

+266
-9
lines changed

3 files changed

+266
-9
lines changed

packages/core/src/execution-engine/__tests__/requests-response.test.ts

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { mock } from 'jest-mock-extended';
2-
import type { IExecuteData, IRunData, EngineRequest } from 'n8n-workflow';
2+
import type { IExecuteData, IRunData, EngineRequest, INodeExecutionData } from 'n8n-workflow';
33

44
import { DirectedGraph } from '../partial-execution-utils';
55
import { createNodeData } from '../partial-execution-utils/__tests__/helpers';
@@ -41,4 +41,250 @@ describe('handleRequests', () => {
4141
}),
4242
).toThrowError('Workflow does not contain a node with the name of "does not exist".');
4343
});
44+
45+
test('merges agent input data with tool parameters for expression resolution', () => {
46+
// ARRANGE
47+
const toolNode = createNodeData({ name: 'Gmail Tool', type: types.passThrough });
48+
const agentNode = createNodeData({ name: 'AI Agent', type: types.passThrough });
49+
50+
const workflow = new DirectedGraph()
51+
.addNodes(toolNode, agentNode)
52+
.toWorkflow({ name: '', active: false, nodeTypes });
53+
54+
// Agent received data from previous nodes with workflow context
55+
const agentInputData: INodeExecutionData[] = [
56+
{
57+
json: {
58+
myNewField: 1,
59+
price_total: '344.00',
60+
existingData: 'from workflow',
61+
},
62+
},
63+
];
64+
65+
const executionData: IExecuteData = {
66+
data: {
67+
main: [agentInputData], // Agent's main input at index 0
68+
},
69+
source: {
70+
main: [
71+
{
72+
previousNode: 'Process Quotes',
73+
previousNodeOutput: 0,
74+
previousNodeRun: 0,
75+
},
76+
],
77+
},
78+
node: agentNode,
79+
};
80+
81+
const request: EngineRequest = {
82+
actions: [
83+
{
84+
actionType: 'ExecutionNodeAction',
85+
nodeName: 'Gmail Tool',
86+
input: { subject: 'Test Email', toolParam: 'from LLM' }, // LLM-provided parameters
87+
type: 'ai_tool',
88+
id: 'tool_call_123',
89+
metadata: { itemIndex: 0 },
90+
},
91+
],
92+
metadata: {},
93+
};
94+
95+
const runData: IRunData = {};
96+
97+
// ACT
98+
const result = handleRequest({
99+
workflow,
100+
currentNode: agentNode,
101+
request,
102+
runIndex: 0,
103+
executionData,
104+
runData,
105+
});
106+
107+
// ASSERT
108+
const toolNodeToExecute = result.nodesToBeExecuted.find((n) => n.parentNode === 'AI Agent');
109+
expect(toolNodeToExecute).toBeDefined();
110+
111+
// Verify merged data contains both workflow data and tool parameters
112+
const mergedJson = toolNodeToExecute!.parentOutputData[0][0].json;
113+
expect(mergedJson).toEqual({
114+
myNewField: 1,
115+
price_total: '344.00',
116+
existingData: 'from workflow',
117+
subject: 'Test Email',
118+
toolParam: 'from LLM',
119+
toolCallId: 'tool_call_123',
120+
});
121+
122+
// Verify tool parameters take precedence (override workflow data)
123+
const requestWithOverride: EngineRequest = {
124+
actions: [
125+
{
126+
actionType: 'ExecutionNodeAction',
127+
nodeName: 'Gmail Tool',
128+
input: { existingData: 'overridden by tool' }, // This should override workflow data
129+
type: 'ai_tool',
130+
id: 'tool_call_456',
131+
metadata: { itemIndex: 0 },
132+
},
133+
],
134+
metadata: {},
135+
};
136+
137+
const runData2: IRunData = {};
138+
const result2 = handleRequest({
139+
workflow,
140+
currentNode: agentNode,
141+
request: requestWithOverride,
142+
runIndex: 0,
143+
executionData,
144+
runData: runData2,
145+
});
146+
147+
const toolNodeToExecute2 = result2.nodesToBeExecuted.find((n) => n.parentNode === 'AI Agent');
148+
const mergedJson2 = toolNodeToExecute2!.parentOutputData[0][0].json;
149+
expect(mergedJson2.existingData).toBe('overridden by tool');
150+
});
151+
152+
test('uses output index 0 for tools regardless of agent input connection', () => {
153+
// ARRANGE
154+
const toolNode = createNodeData({ name: 'Gmail Tool', type: types.passThrough });
155+
const agentNode = createNodeData({ name: 'AI Agent', type: types.passThrough });
156+
const switchNode = createNodeData({ name: 'Switch', type: types.passThrough });
157+
158+
const workflow = new DirectedGraph()
159+
.addNodes(toolNode, agentNode, switchNode)
160+
.toWorkflow({ name: '', active: false, nodeTypes });
161+
162+
// Agent is connected to output 2 of the Switch node
163+
const agentInputData: INodeExecutionData[] = [
164+
{
165+
json: {
166+
data: 'from switch output 2',
167+
},
168+
},
169+
];
170+
171+
const executionData: IExecuteData = {
172+
data: {
173+
main: [agentInputData],
174+
},
175+
source: {
176+
main: [
177+
{
178+
previousNode: 'Switch',
179+
previousNodeOutput: 2, // Connected to Switch output 2
180+
previousNodeRun: 0,
181+
},
182+
],
183+
},
184+
node: agentNode,
185+
};
186+
187+
const request: EngineRequest = {
188+
actions: [
189+
{
190+
actionType: 'ExecutionNodeAction',
191+
nodeName: 'Gmail Tool',
192+
input: { message: 'test' },
193+
type: 'ai_tool',
194+
id: 'tool_call_789',
195+
metadata: { itemIndex: 0 },
196+
},
197+
],
198+
metadata: {},
199+
};
200+
201+
const runData: IRunData = {};
202+
203+
// ACT
204+
const result = handleRequest({
205+
workflow,
206+
currentNode: agentNode,
207+
request,
208+
runIndex: 0,
209+
executionData,
210+
runData,
211+
});
212+
213+
// ASSERT
214+
const toolNodeToExecute = result.nodesToBeExecuted.find((n) => n.parentNode === 'AI Agent');
215+
expect(toolNodeToExecute).toBeDefined();
216+
217+
// Verify parentOutputIndex is always 0 for tools (agents have only one main output)
218+
// This prevents "Cannot read properties of undefined (reading 'map')" error
219+
expect(toolNodeToExecute!.parentOutputIndex).toBe(0);
220+
});
221+
222+
test('handles multiple items correctly using itemIndex from metadata', () => {
223+
// ARRANGE
224+
const toolNode = createNodeData({ name: 'Gmail Tool', type: types.passThrough });
225+
const agentNode = createNodeData({ name: 'AI Agent', type: types.passThrough });
226+
227+
const workflow = new DirectedGraph()
228+
.addNodes(toolNode, agentNode)
229+
.toWorkflow({ name: '', active: false, nodeTypes });
230+
231+
// Agent received multiple items from previous nodes
232+
const agentInputData: INodeExecutionData[] = [
233+
{ json: { id: 1, value: 'first item' } },
234+
{ json: { id: 2, value: 'second item' } },
235+
{ json: { id: 3, value: 'third item' } },
236+
];
237+
238+
const executionData: IExecuteData = {
239+
data: {
240+
main: [agentInputData],
241+
},
242+
source: {
243+
main: [
244+
{
245+
previousNode: 'Process Data',
246+
previousNodeOutput: 0,
247+
previousNodeRun: 0,
248+
},
249+
],
250+
},
251+
node: agentNode,
252+
};
253+
254+
const request: EngineRequest = {
255+
actions: [
256+
{
257+
actionType: 'ExecutionNodeAction',
258+
nodeName: 'Gmail Tool',
259+
input: { toolParam: 'for second item' },
260+
type: 'ai_tool',
261+
id: 'tool_call_abc',
262+
metadata: { itemIndex: 1 }, // Processing second item
263+
},
264+
],
265+
metadata: {},
266+
};
267+
268+
const runData: IRunData = {};
269+
270+
// ACT
271+
const result = handleRequest({
272+
workflow,
273+
currentNode: agentNode,
274+
request,
275+
runIndex: 0,
276+
executionData,
277+
runData,
278+
});
279+
280+
// ASSERT
281+
const toolNodeToExecute = result.nodesToBeExecuted.find((n) => n.parentNode === 'AI Agent');
282+
expect(toolNodeToExecute).toBeDefined();
283+
284+
// Verify correct item data is merged (second item with id: 2)
285+
const mergedJson = toolNodeToExecute!.parentOutputData[0][0].json;
286+
expect(mergedJson.id).toBe(2);
287+
expect(mergedJson.value).toBe('second item');
288+
expect(mergedJson.toolParam).toBe('for second item');
289+
});
44290
});

packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ describe('processRunExecutionData', () => {
433433
ai_tool: [
434434
[
435435
{
436-
json: { query: 'test input', toolCallId: 'action_1' },
436+
json: { prompt: 'test prompt', query: 'test input', toolCallId: 'action_1' },
437437
pairedItem: {
438438
input: 0,
439439
item: 0,
@@ -453,7 +453,7 @@ describe('processRunExecutionData', () => {
453453
ai_tool: [
454454
[
455455
{
456-
json: { data: 'another input', toolCallId: 'action_2' },
456+
json: { prompt: 'test prompt', data: 'another input', toolCallId: 'action_2' },
457457
pairedItem: {
458458
input: 0,
459459
item: 0,
@@ -565,7 +565,7 @@ describe('processRunExecutionData', () => {
565565
ai_tool: [
566566
[
567567
{
568-
json: { query: 'test input', toolCallId: 'action_1' },
568+
json: { prompt: 'test prompt', query: 'test input', toolCallId: 'action_1' },
569569
pairedItem: {
570570
input: 0,
571571
item: 0,

packages/core/src/execution-engine/requests-response.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,24 @@ function prepareRequestedNodesForExecution(
6161
const parentOutputIndex = parentSourceData?.previousNodeOutput ?? 0;
6262
const parentRunIndex = parentSourceData?.previousNodeRun ?? 0;
6363
const parentSourceNode = parentSourceData?.previousNode ?? currentNode.name;
64+
65+
// Get the item index from action metadata to access the correct agent input data
66+
const itemIndex = (action.metadata as { itemIndex?: number })?.itemIndex ?? 0;
67+
// Use index 0 for main input as agents have only one main input connection
68+
const agentInputData = executionData.data.main?.[0]?.[itemIndex];
69+
70+
// Merge agent's input data with action.input so tools have access to workflow data
71+
// action.input takes precedence to allow tool-specific parameters to override
72+
const mergedJson = {
73+
...(agentInputData?.json ?? {}),
74+
...action.input,
75+
toolCallId: action.id,
76+
};
77+
6478
const parentOutputData: INodeExecutionData[][] = [
6579
[
6680
{
67-
json: {
68-
...action.input,
69-
toolCallId: action.id,
70-
},
81+
json: mergedJson,
7182
pairedItem: {
7283
item: parentRunIndex,
7384
input: parentOutputIndex,
@@ -96,7 +107,7 @@ function prepareRequestedNodesForExecution(
96107

97108
nodesToBeExecuted.push({
98109
inputConnectionData,
99-
parentOutputIndex,
110+
parentOutputIndex: 0, // Tools connect to agent's output 0 (agents have only one main output)
100111
parentNode,
101112
parentOutputData,
102113
runIndex,

0 commit comments

Comments
 (0)