Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
1628 lines
40 KiB
TypeScript
1628 lines
40 KiB
TypeScript
import type { EngineResponse } from 'n8n-workflow';
|
|
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
|
|
import { buildSteps } from '../buildSteps';
|
|
import type { RequestResponseMetadata } from '../types';
|
|
|
|
describe('buildSteps', () => {
|
|
const itemIndex = 0;
|
|
|
|
describe('Basic functionality', () => {
|
|
it('should return empty array when response is undefined', () => {
|
|
const result = buildSteps(undefined, itemIndex);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should build steps from engine response', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: '2+2',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toMatchObject({
|
|
action: {
|
|
tool: 'Calculator',
|
|
toolInput: { input: '2+2' },
|
|
toolCallId: 'call_123',
|
|
type: 'tool_call',
|
|
},
|
|
observation: JSON.stringify([{ result: '4' }]),
|
|
});
|
|
});
|
|
|
|
it('should handle multiple tool responses', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: '2+2',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Search',
|
|
input: {
|
|
id: 'call_124',
|
|
input: 'TypeScript',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_124',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { results: ['result1', 'result2'] } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].action.tool).toBe('Calculator');
|
|
expect(result[1].action.tool).toBe('Search');
|
|
});
|
|
|
|
it('should filter out responses for different item indexes', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: '2+2',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Search',
|
|
input: {
|
|
id: 'call_124',
|
|
input: 'TypeScript',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_124',
|
|
metadata: {
|
|
itemIndex: 1, // Different item index
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { results: ['result1', 'result2'] } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, 0);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.tool).toBe('Calculator');
|
|
});
|
|
|
|
it('should handle responses with minimal toolInput', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
// Missing input property - will result in empty toolInput
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
// Even with minimal input, a step is created with empty toolInput
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.toolInput).toEqual({});
|
|
});
|
|
|
|
describe('Previous requests handling', () => {
|
|
it('should include previous requests from metadata', () => {
|
|
const previousRequests = [
|
|
{
|
|
action: {
|
|
tool: 'previous_tool',
|
|
toolInput: { input: 'previous' },
|
|
log: 'Previous log',
|
|
toolCallId: 'call_prev',
|
|
type: 'tool_call',
|
|
},
|
|
observation: 'previous result',
|
|
},
|
|
];
|
|
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {
|
|
previousRequests,
|
|
},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toEqual(previousRequests[0]);
|
|
expect(result[1].action.tool).toBe('Calculator');
|
|
});
|
|
|
|
it('should not duplicate steps that already exist in previousRequests', () => {
|
|
const previousRequests = [
|
|
{
|
|
action: {
|
|
tool: 'calculator',
|
|
toolInput: { expression: '2+2' },
|
|
log: 'Previous log',
|
|
toolCallId: 'call_123',
|
|
type: 'tool_call',
|
|
},
|
|
observation: '4',
|
|
},
|
|
];
|
|
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123', // Same ID as in previousRequests
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {
|
|
previousRequests,
|
|
},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual(previousRequests[0]);
|
|
});
|
|
});
|
|
|
|
describe('Synthetic AI message creation', () => {
|
|
it('should create synthetic AI message with tool calls', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.messageLog).toBeDefined();
|
|
expect(result[0].action.messageLog).toHaveLength(1);
|
|
|
|
const message = result[0].action.messageLog![0];
|
|
expect(message).toHaveProperty('tool_calls');
|
|
expect(message.tool_calls).toHaveLength(1);
|
|
expect(message.tool_calls?.[0]).toMatchObject({
|
|
id: 'call_123',
|
|
name: 'Calculator_Node',
|
|
type: 'tool_call',
|
|
});
|
|
});
|
|
|
|
it('should use custom log if provided', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
log: 'Custom log message',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.log).toBe('Custom log message');
|
|
});
|
|
|
|
it('should use custom type if provided', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
type: 'custom_type',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.type).toBe('custom_type');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('should handle tool responses with error data structure', () => {
|
|
// When a tool fails, the data structure contains error information
|
|
// instead of the normal ai_tool data structure
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Call n8n Workflow Tool',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { test: {} },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
// Error data structure - no ai_tool property
|
|
error: {
|
|
message: 'Workflow execution failed',
|
|
name: 'NodeOperationError',
|
|
} as any, // Cast to satisfy ExecutionError type
|
|
executionStatus: 'error',
|
|
executionTime: 100,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
// Should still produce a step with error information in the observation
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].observation).toContain('error');
|
|
expect(result[0].observation).not.toBe('""');
|
|
});
|
|
|
|
it('should handle tool responses with executionError passed directly', () => {
|
|
// This simulates when WorkflowToolService passes ExecutionError directly
|
|
// to addOutputData instead of wrapping it in the expected format
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Call n8n Workflow Tool',
|
|
input: {
|
|
id: 'call_456',
|
|
input: { query: 'test' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_456',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
// When error is passed, data might have different structure
|
|
data: {
|
|
data: {
|
|
// ai_tool might be missing or empty when there's an error
|
|
},
|
|
error: {
|
|
message: 'Sub-workflow execution failed',
|
|
name: 'NodeOperationError',
|
|
} as any, // Cast to satisfy ExecutionError type
|
|
executionStatus: 'error',
|
|
executionTime: 50,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
// Should handle missing ai_tool gracefully and include error info
|
|
expect(result).toHaveLength(1);
|
|
// The observation should contain meaningful error information, not just empty string
|
|
expect(result[0].observation).not.toBe('""');
|
|
});
|
|
|
|
it('should preserve error information when building observation from error response', () => {
|
|
const errorMessage = 'Received tool input did not match expected schema';
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Call n8n Workflow Tool',
|
|
input: {
|
|
id: 'call_789',
|
|
input: { test: 'invalid' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_789',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {},
|
|
error: {
|
|
message: errorMessage,
|
|
name: 'ToolInputParsingException',
|
|
} as any, // Cast to satisfy ExecutionError type
|
|
executionStatus: 'error',
|
|
executionTime: 10,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// The observation should include the error message so the agent knows what went wrong
|
|
expect(result[0].observation).toContain(errorMessage);
|
|
});
|
|
|
|
it('should handle error responses with ai_tool containing error wrapper', () => {
|
|
// When WorkflowToolService correctly wraps error in INodeExecutionData format
|
|
const errorMessage = 'Workflow execution failed';
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Call n8n Workflow Tool',
|
|
input: {
|
|
id: 'call_error',
|
|
input: { test: {} },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_error',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { error: `There was an error: "${errorMessage}"` } }]],
|
|
},
|
|
executionStatus: 'error',
|
|
executionTime: 100,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// This test passes when error is properly wrapped - validates the expected format
|
|
expect(result[0].observation).toContain(errorMessage);
|
|
});
|
|
});
|
|
|
|
describe('Observation formatting', () => {
|
|
it('should stringify tool result data correctly', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [
|
|
[
|
|
{ json: { result: '4', status: 'success' } },
|
|
{ json: { metadata: { timestamp: 123456789 } } },
|
|
],
|
|
],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].observation).toBe(
|
|
JSON.stringify([
|
|
{ result: '4', status: 'success' },
|
|
{ metadata: { timestamp: 123456789 } },
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('should handle empty ai_tool data', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].observation).toBe('""');
|
|
});
|
|
});
|
|
|
|
describe('Anthropic thinking blocks reconstruction', () => {
|
|
it('should reconstruct AIMessage with thinking content blocks', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
anthropic: {
|
|
thinkingContent: 'I need to calculate 2+2 using the calculator tool.',
|
|
thinkingType: 'thinking',
|
|
thinkingSignature: 'test_signature_123',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.messageLog).toBeDefined();
|
|
expect(result[0].action.messageLog).toHaveLength(1);
|
|
|
|
const message = result[0].action.messageLog![0];
|
|
const content = message.content;
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toHaveLength(2);
|
|
// First block should be thinking
|
|
expect(content[0]).toEqual({
|
|
type: 'thinking',
|
|
thinking: 'I need to calculate 2+2 using the calculator tool.',
|
|
signature: 'test_signature_123',
|
|
});
|
|
// Second block should be tool_use
|
|
expect(content[1]).toMatchObject({
|
|
type: 'tool_use',
|
|
id: 'call_123',
|
|
name: 'Calculator',
|
|
});
|
|
});
|
|
|
|
it('should reconstruct AIMessage with redacted_thinking content blocks', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Search',
|
|
input: {
|
|
id: 'call_456',
|
|
input: { query: 'sensitive search' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_456',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
anthropic: {
|
|
thinkingContent: 'This thinking was redacted by safety systems.',
|
|
thinkingType: 'redacted_thinking',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { results: [] } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.messageLog).toBeDefined();
|
|
expect(result[0].action.messageLog).toHaveLength(1);
|
|
|
|
const message = result[0].action.messageLog![0];
|
|
const content = message.content;
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toHaveLength(2);
|
|
// First block should be redacted_thinking
|
|
expect(content[0]).toEqual({
|
|
type: 'redacted_thinking',
|
|
data: 'This thinking was redacted by safety systems.',
|
|
});
|
|
// Second block should be tool_use
|
|
expect(content[1]).toMatchObject({
|
|
type: 'tool_use',
|
|
id: 'call_456',
|
|
name: 'Search',
|
|
});
|
|
});
|
|
|
|
it('should use string content when no thinking blocks present', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
// No thinking blocks
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.messageLog).toBeDefined();
|
|
expect(result[0].action.messageLog).toHaveLength(1);
|
|
|
|
const message = result[0].action.messageLog![0];
|
|
expect(typeof message.content).toBe('string');
|
|
expect(message.content).toContain('Calling Calculator');
|
|
expect(message).toHaveProperty('tool_calls');
|
|
});
|
|
|
|
it('should handle thinking content without thinkingType', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
anthropic: {
|
|
thinkingContent: 'Some thinking content',
|
|
// Missing thinkingType
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Should fall back to string content when thinkingType is missing
|
|
expect(typeof message.content).toBe('string');
|
|
});
|
|
|
|
it('should work alongside Gemini thought_signature', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
google: {
|
|
thoughtSignature: 'Gemini thought signature',
|
|
},
|
|
anthropic: {
|
|
thinkingContent: 'Anthropic thinking content',
|
|
thinkingType: 'thinking',
|
|
thinkingSignature: 'anthropic_sig_456',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Should use Anthropic thinking blocks in content
|
|
const content = message.content;
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toHaveLength(2);
|
|
// First block should be thinking
|
|
expect(content[0]).toEqual({
|
|
type: 'thinking',
|
|
thinking: 'Anthropic thinking content',
|
|
signature: 'anthropic_sig_456',
|
|
});
|
|
// Second block should be tool_use
|
|
expect(content[1]).toMatchObject({
|
|
type: 'tool_use',
|
|
id: 'call_123',
|
|
name: 'Calculator',
|
|
});
|
|
// When thinking blocks are present, tool_calls is not used (everything is in content array)
|
|
// Note: Anthropic thinking and Gemini thought_signature are mutually exclusive
|
|
});
|
|
});
|
|
|
|
describe('Tool name resolution', () => {
|
|
it('should use HITL toolName when HITL metadata is present', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'HITL Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { query: 'test' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
hitl: {
|
|
toolName: 'custom_search_tool',
|
|
gatedToolNodeName: 'Search API',
|
|
originalInput: { query: 'test' },
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { results: ['result1'] } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// Should use the toolName from HITL metadata
|
|
expect(result[0].action.tool).toBe('custom_search_tool');
|
|
});
|
|
|
|
it('should convert nodeName to toolName when HITL metadata is not present', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// Should convert node name using nodeNameToToolName
|
|
expect(result[0].action.tool).toBe('Calculator_Node');
|
|
});
|
|
|
|
it('should use HITL toolName in message content', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'HITL Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { query: 'test' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
hitl: {
|
|
toolName: 'custom_tool',
|
|
gatedToolNodeName: 'Custom Tool',
|
|
originalInput: { query: 'test' },
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: 'success' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Message content should use the HITL toolName
|
|
expect(message.content).toContain('Calling custom_tool');
|
|
expect(message.content).not.toContain('HITL Node');
|
|
// Tool call should also use the HITL toolName
|
|
expect(message.tool_calls?.[0].name).toBe('custom_tool');
|
|
});
|
|
|
|
it('should use converted nodeName in message content when HITL metadata is absent', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'My Custom Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { data: 'test' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: 'success' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Message content should use the converted tool name
|
|
expect(message.content).toContain('Calling My_Custom_Node');
|
|
expect(message.tool_calls?.[0].name).toBe('My_Custom_Node');
|
|
});
|
|
|
|
it('should handle HITL toolName in Anthropic thinking blocks', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'HITL Node',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { query: 'test' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
hitl: {
|
|
toolName: 'hitl_tool',
|
|
gatedToolNodeName: 'Gated Tool',
|
|
originalInput: { query: 'test' },
|
|
},
|
|
anthropic: {
|
|
thinkingContent: 'I will use the HITL tool',
|
|
thinkingType: 'thinking',
|
|
thinkingSignature: 'sig_123',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: 'approved' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
const content = message.content;
|
|
// Content should be an array with thinking and tool_use blocks
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toHaveLength(2);
|
|
// Tool use block should use the HITL toolName
|
|
expect(content[1]).toMatchObject({
|
|
type: 'tool_use',
|
|
id: 'call_123',
|
|
name: 'hitl_tool',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Tool input extraction', () => {
|
|
it('should extract toolInput as object with string input property', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: '5*343',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { response: '1715' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// toolInput is always an object for type consistency with LangChain ToolCall
|
|
expect(result[0].action.toolInput).toEqual({ input: '5*343' });
|
|
});
|
|
|
|
it('should extract toolInput with multiple arguments (new format)', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'save_result',
|
|
input: {
|
|
id: 'call_456',
|
|
result: 1715,
|
|
equation: '5*343',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_456',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { equation: '5*343', result: 1715 } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.toolInput).toEqual({
|
|
result: 1715,
|
|
equation: '5*343',
|
|
});
|
|
// Should not include id, which is metadata
|
|
expect(result[0].action.toolInput).not.toHaveProperty('id');
|
|
});
|
|
|
|
it('should exclude metadata fields (id, log, type) from toolInput', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'custom_tool',
|
|
input: {
|
|
id: 'call_789',
|
|
log: 'Custom log message',
|
|
type: 'tool_call',
|
|
arg1: 'value1',
|
|
arg2: 'value2',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_789',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { success: true } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].action.toolInput).toEqual({
|
|
arg1: 'value1',
|
|
arg2: 'value2',
|
|
});
|
|
// Metadata fields should be excluded from toolInput
|
|
expect(result[0].action.toolInput).not.toHaveProperty('id');
|
|
expect(result[0].action.toolInput).not.toHaveProperty('log');
|
|
expect(result[0].action.toolInput).not.toHaveProperty('type');
|
|
});
|
|
|
|
it('should handle tools with non-string input property as full object', () => {
|
|
// When input property exists but is not a string (e.g., object), extract all properties
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'hybrid_tool',
|
|
input: {
|
|
id: 'call_mixed',
|
|
input: { expression: '2+2' },
|
|
extra: 'should be included',
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_mixed',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: 4 } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
// Should extract all properties except metadata (id, log, type)
|
|
expect(result[0].action.toolInput).toEqual({
|
|
input: { expression: '2+2' },
|
|
extra: 'should be included',
|
|
});
|
|
expect(result[0].action.toolInput).not.toHaveProperty('id');
|
|
});
|
|
});
|
|
|
|
describe('Gemini thought_signature in additional_kwargs', () => {
|
|
it('should include thought_signature in AIMessage additional_kwargs', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
google: {
|
|
thoughtSignature: 'gemini_thought_sig_abc123',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Verify additional_kwargs contains the thought signature
|
|
expect(message.additional_kwargs).toBeDefined();
|
|
expect(message.additional_kwargs.__gemini_function_call_thought_signatures__).toEqual({
|
|
call_123: 'gemini_thought_sig_abc123',
|
|
});
|
|
});
|
|
|
|
it('should group parallel tool calls into shared AIMessage with Gemini signature', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_1',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_1',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
google: {
|
|
thoughtSignature: 'shared_gemini_sig',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Weather',
|
|
input: {
|
|
id: 'call_2',
|
|
input: { location: 'NYC' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_2',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
google: {
|
|
thoughtSignature: 'shared_gemini_sig',
|
|
},
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { temp: '72F' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(2);
|
|
|
|
// First step should have the shared AIMessage with ALL tool calls
|
|
const firstMessage = result[0].action.messageLog![0];
|
|
expect(firstMessage).toBeDefined();
|
|
expect(firstMessage.tool_calls).toHaveLength(2);
|
|
expect(firstMessage.tool_calls![0].name).toBe('Calculator');
|
|
expect(firstMessage.tool_calls![1].name).toBe('Weather');
|
|
|
|
// Signature should only be on the first tool call in the map
|
|
const sigMap = firstMessage.additional_kwargs
|
|
.__gemini_function_call_thought_signatures__ as Record<string, string>;
|
|
expect(sigMap).toEqual({ call_1: 'shared_gemini_sig' });
|
|
|
|
// Signatures array should have: ['', 'sig', ''] (text part, first func call, second func call)
|
|
const sigArray = firstMessage.additional_kwargs.signatures as string[];
|
|
expect(sigArray).toEqual(['', 'shared_gemini_sig', '']);
|
|
|
|
// Second step should have EMPTY messageLog (no duplicate AIMessage)
|
|
expect(result[1].action.messageLog).toEqual([]);
|
|
|
|
// Both steps should have correct observations
|
|
expect(result[0].observation).toBe(JSON.stringify([{ result: '4' }]));
|
|
expect(result[1].observation).toBe(JSON.stringify([{ temp: '72F' }]));
|
|
});
|
|
|
|
it('should NOT group parallel tool calls without Gemini signature', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_1',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_1',
|
|
metadata: { itemIndex: 0 },
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Weather',
|
|
input: {
|
|
id: 'call_2',
|
|
input: { location: 'NYC' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_2',
|
|
metadata: { itemIndex: 0 },
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { temp: '72F' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(2);
|
|
|
|
// Without Gemini signature, each step should have its own AIMessage
|
|
expect(result[0].action.messageLog).toHaveLength(1);
|
|
expect(result[0].action.messageLog![0].tool_calls).toHaveLength(1);
|
|
expect(result[1].action.messageLog).toHaveLength(1);
|
|
expect(result[1].action.messageLog![0].tool_calls).toHaveLength(1);
|
|
});
|
|
|
|
it('should not include additional_kwargs when no thought_signature present', () => {
|
|
const response: EngineResponse<RequestResponseMetadata> = {
|
|
actionResponses: [
|
|
{
|
|
action: {
|
|
actionType: 'ExecutionNodeAction',
|
|
nodeName: 'Calculator',
|
|
input: {
|
|
id: 'call_123',
|
|
input: { expression: '2+2' },
|
|
},
|
|
type: NodeConnectionTypes.AiTool,
|
|
id: 'call_123',
|
|
metadata: {
|
|
itemIndex: 0,
|
|
},
|
|
},
|
|
data: {
|
|
data: {
|
|
ai_tool: [[{ json: { result: '4' } }]],
|
|
},
|
|
executionTime: 0,
|
|
startTime: 0,
|
|
executionIndex: 0,
|
|
source: [],
|
|
},
|
|
},
|
|
],
|
|
metadata: {},
|
|
};
|
|
|
|
const result = buildSteps(response, itemIndex);
|
|
|
|
expect(result).toHaveLength(1);
|
|
const message = result[0].action.messageLog![0];
|
|
// Should not have __gemini_function_call_thought_signatures__ when no signature
|
|
expect(
|
|
message.additional_kwargs?.__gemini_function_call_thought_signatures__,
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
});
|