feat: Simplify builder tool calls (no-changelog) (#18798)

This commit is contained in:
Mutasem Aldmour
2025-08-28 10:14:03 +02:00
committed by GitHub
parent ff56c95605
commit d244b99484
36 changed files with 2190 additions and 391 deletions

View File

@@ -19,22 +19,24 @@ import {
} from '@/workflow-builder-agent';
jest.mock('@/tools/add-node.tool', () => ({
createAddNodeTool: jest.fn().mockReturnValue({ name: 'add_node' }),
createAddNodeTool: jest.fn().mockReturnValue({ tool: { name: 'add_node' } }),
}));
jest.mock('@/tools/connect-nodes.tool', () => ({
createConnectNodesTool: jest.fn().mockReturnValue({ name: 'connect_nodes' }),
createConnectNodesTool: jest.fn().mockReturnValue({ tool: { name: 'connect_nodes' } }),
}));
jest.mock('@/tools/node-details.tool', () => ({
createNodeDetailsTool: jest.fn().mockReturnValue({ name: 'node_details' }),
createNodeDetailsTool: jest.fn().mockReturnValue({ tool: { name: 'node_details' } }),
}));
jest.mock('@/tools/node-search.tool', () => ({
createNodeSearchTool: jest.fn().mockReturnValue({ name: 'node_search' }),
createNodeSearchTool: jest.fn().mockReturnValue({ tool: { name: 'node_search' } }),
}));
jest.mock('@/tools/remove-node.tool', () => ({
createRemoveNodeTool: jest.fn().mockReturnValue({ name: 'remove_node' }),
createRemoveNodeTool: jest.fn().mockReturnValue({ tool: { name: 'remove_node' } }),
}));
jest.mock('@/tools/update-node-parameters.tool', () => ({
createUpdateNodeParametersTool: jest.fn().mockReturnValue({ name: 'update_node_parameters' }),
createUpdateNodeParametersTool: jest
.fn()
.mockReturnValue({ tool: { name: 'update_node_parameters' } }),
}));
jest.mock('@/tools/prompts/main-agent.prompt', () => ({
mainAgentPrompt: {

View File

@@ -13,6 +13,8 @@ import { findNodeType } from './helpers/validation';
import type { AddedNode } from '../types/nodes';
import type { AddNodeOutput, ToolError } from '../types/tools';
const DISPLAY_TITLE = 'Adding node';
/**
* Schema for node creation input
*/
@@ -64,13 +66,32 @@ function buildResponseMessage(addedNode: AddedNode, nodeTypes: INodeTypeDescript
return `Successfully added "${addedNode.name}" (${addedNode.displayName ?? addedNode.type})${nodeTypeInfo} with ID ${addedNode.id}`;
}
function getCustomNodeTitle(
input: Record<string, unknown>,
nodeTypes: INodeTypeDescription[],
): string {
if ('nodeType' in input && typeof input['nodeType'] === 'string') {
const nodeType = nodeTypes.find((type) => type.name === input.nodeType);
if (nodeType) {
return `Adding ${nodeType.displayName} node`;
}
}
return DISPLAY_TITLE;
}
/**
* Factory function to create the add node tool
*/
export function createAddNodeTool(nodeTypes: INodeTypeDescription[]) {
return tool(
const dynamicTool = tool(
async (input, config) => {
const reporter = createProgressReporter(config, 'add_nodes');
const reporter = createProgressReporter(
config,
'add_nodes',
DISPLAY_TITLE,
getCustomNodeTitle(input, nodeTypes),
);
try {
// Validate input using Zod schema
@@ -194,4 +215,10 @@ Think through the connectionParametersReasoning FIRST, then set connectionParame
schema: nodeCreationSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
getCustomDisplayTitle: (input: Record<string, unknown>) => getCustomNodeTitle(input, nodeTypes),
};
}

View File

@@ -49,10 +49,12 @@ export const nodeConnectionSchema = z.object({
* Factory function to create the connect nodes tool
*/
export function createConnectNodesTool(nodeTypes: INodeTypeDescription[], logger?: Logger) {
return tool(
const DISPLAY_TITLE = 'Connecting nodes';
const dynamicTool = tool(
// eslint-disable-next-line complexity
(input, config) => {
const reporter = createProgressReporter(config, 'connect_nodes');
const reporter = createProgressReporter(config, 'connect_nodes', DISPLAY_TITLE);
try {
// Validate input using Zod schema
@@ -316,4 +318,9 @@ CONNECTION EXAMPLES:
schema: nodeConnectionSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -10,22 +10,36 @@ import type {
/**
* Create a progress reporter for a tool execution
*
* @param config
* @param toolName
* @param displayTitle the general tool action name, for example "Searching for nodes"
* @param customTitle custom title per tool call, for example "Searching for OpenAI"
*/
export function createProgressReporter<TToolName extends string = string>(
config: ToolRunnableConfig & LangGraphRunnableConfig,
toolName: TToolName,
displayTitle: string,
customTitle?: string,
): ProgressReporter {
const toolCallId = config.toolCall?.id;
let customDisplayTitle = customTitle;
const emit = (message: ToolProgressMessage<TToolName>): void => {
config.writer?.(message);
};
const start = <T>(input: T): void => {
const start = <T>(input: T, options?: { customDisplayTitle: string }): void => {
if (options?.customDisplayTitle) {
customDisplayTitle = options.customDisplayTitle;
}
emit({
type: 'tool',
toolName,
toolCallId,
displayTitle,
customDisplayTitle,
status: 'running',
updates: [
{
@@ -41,6 +55,8 @@ export function createProgressReporter<TToolName extends string = string>(
type: 'tool',
toolName,
toolCallId,
displayTitle,
customDisplayTitle,
status: 'running',
updates: [
{
@@ -56,6 +72,8 @@ export function createProgressReporter<TToolName extends string = string>(
type: 'tool',
toolName,
toolCallId,
displayTitle,
customDisplayTitle,
status: 'completed',
updates: [
{
@@ -71,6 +89,8 @@ export function createProgressReporter<TToolName extends string = string>(
type: 'tool',
toolName,
toolCallId,
displayTitle,
customDisplayTitle,
status: 'error',
updates: [
{

View File

@@ -0,0 +1,436 @@
import type { ToolRunnableConfig } from '@langchain/core/tools';
import type { LangGraphRunnableConfig } from '@langchain/langgraph';
import type { ToolError } from '../../../types/tools';
import {
createProgressReporter,
reportStart,
reportProgress,
reportComplete,
reportError,
createBatchProgressReporter,
} from '../progress';
describe('progress helpers', () => {
let mockWriter: jest.MockedFunction<(chunk: unknown) => void>;
let mockConfig: ToolRunnableConfig & LangGraphRunnableConfig;
beforeEach(() => {
mockWriter = jest.fn();
mockConfig = {
writer: mockWriter,
toolCall: {
id: 'test-tool-call-id',
name: 'test-tool',
args: {},
},
};
});
describe('createProgressReporter', () => {
it('should create a progress reporter with all methods', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
expect(reporter).toHaveProperty('start');
expect(reporter).toHaveProperty('progress');
expect(reporter).toHaveProperty('complete');
expect(reporter).toHaveProperty('error');
expect(reporter).toHaveProperty('createBatchReporter');
expect(typeof reporter.start).toBe('function');
expect(typeof reporter.progress).toBe('function');
expect(typeof reporter.complete).toBe('function');
expect(typeof reporter.error).toBe('function');
expect(typeof reporter.createBatchReporter).toBe('function');
});
it('should emit start message with input data', () => {
const reporter = createProgressReporter(
mockConfig,
'add_node',
'Adding Node',
'Adding Code Node',
);
const input = { nodeType: 'code', name: 'Test Node' };
reporter.start(input);
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'add_node',
toolCallId: 'test-tool-call-id',
displayTitle: 'Adding Node',
customDisplayTitle: 'Adding Code Node',
status: 'running',
updates: [
{
type: 'input',
data: input,
},
],
});
});
it('should emit progress message with string message', () => {
const reporter = createProgressReporter(mockConfig, 'connect_nodes', 'Connecting Nodes');
reporter.progress('Connecting node A to node B');
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'connect_nodes',
toolCallId: 'test-tool-call-id',
displayTitle: 'Connecting Nodes',
customDisplayTitle: undefined,
status: 'running',
updates: [
{
type: 'progress',
data: { message: 'Connecting node A to node B' },
},
],
});
});
it('should emit progress message with custom data', () => {
const reporter = createProgressReporter(mockConfig, 'search_nodes', 'Searching Nodes');
const customData = { found: 5, query: 'http' };
reporter.progress('Found nodes', customData);
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'search_nodes',
toolCallId: 'test-tool-call-id',
displayTitle: 'Searching Nodes',
customDisplayTitle: undefined,
status: 'running',
updates: [
{
type: 'progress',
data: customData,
},
],
});
});
it('should emit complete message with output data', () => {
const reporter = createProgressReporter(mockConfig, 'remove_node', 'Removing Node');
const output = { nodeId: 'node123', success: true };
reporter.complete(output);
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'remove_node',
toolCallId: 'test-tool-call-id',
displayTitle: 'Removing Node',
customDisplayTitle: undefined,
status: 'completed',
updates: [
{
type: 'output',
data: output,
},
],
});
});
it('should emit error message with error details', () => {
const reporter = createProgressReporter(mockConfig, 'update_node', 'Updating Node');
const error: ToolError = {
message: 'Node not found',
code: 'NODE_NOT_FOUND',
details: { nodeId: 'missing-node' },
};
reporter.error(error);
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'update_node',
toolCallId: 'test-tool-call-id',
displayTitle: 'Updating Node',
customDisplayTitle: undefined,
status: 'error',
updates: [
{
type: 'error',
data: {
message: 'Node not found',
code: 'NODE_NOT_FOUND',
details: { nodeId: 'missing-node' },
},
},
],
});
});
it('should handle missing writer gracefully', () => {
const configWithoutWriter = { ...mockConfig, writer: undefined };
const reporter = createProgressReporter(configWithoutWriter, 'test_tool', 'Test Tool');
expect(() => reporter.start({ test: 'data' })).not.toThrow();
expect(() => reporter.progress('test message')).not.toThrow();
expect(() => reporter.complete({ result: 'success' })).not.toThrow();
expect(() => reporter.error({ message: 'test error' })).not.toThrow();
});
it('should handle missing toolCallId', () => {
const configWithoutToolCallId = { ...mockConfig, toolCall: undefined };
const reporter = createProgressReporter(configWithoutToolCallId, 'test_tool', 'Test Tool');
reporter.start({ test: 'data' });
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
toolCallId: undefined,
}),
);
});
});
describe('batch reporter', () => {
it('should create batch reporter with correct interface', () => {
const reporter = createProgressReporter(mockConfig, 'batch_tool', 'Batch Tool');
const batchReporter = reporter.createBatchReporter('Processing items');
expect(batchReporter).toHaveProperty('init');
expect(batchReporter).toHaveProperty('next');
expect(batchReporter).toHaveProperty('complete');
expect(typeof batchReporter.init).toBe('function');
expect(typeof batchReporter.next).toBe('function');
expect(typeof batchReporter.complete).toBe('function');
});
it('should track batch progress correctly', () => {
const reporter = createProgressReporter(mockConfig, 'batch_tool', 'Batch Tool');
const batchReporter = reporter.createBatchReporter('Processing nodes');
batchReporter.init(3);
batchReporter.next('First node');
batchReporter.next('Second node');
batchReporter.next('Third node');
batchReporter.complete();
expect(mockWriter).toHaveBeenCalledTimes(4);
expect(mockWriter).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Processing nodes: Processing item 1 of 3: First node' },
},
],
}),
);
expect(mockWriter).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Processing nodes: Processing item 2 of 3: Second node' },
},
],
}),
);
expect(mockWriter).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Processing nodes: Processing item 3 of 3: Third node' },
},
],
}),
);
expect(mockWriter).toHaveBeenNthCalledWith(
4,
expect.objectContaining({
updates: [
{ type: 'progress', data: { message: 'Processing nodes: Completed all 3 items' } },
],
}),
);
});
it('should reset counter when init is called again', () => {
const reporter = createProgressReporter(mockConfig, 'batch_tool', 'Batch Tool');
const batchReporter = reporter.createBatchReporter('Testing reset');
batchReporter.init(2);
batchReporter.next('Item 1');
batchReporter.init(1); // Reset
batchReporter.next('New item');
expect(mockWriter).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Testing reset: Processing item 1 of 2: Item 1' },
},
],
}),
);
expect(mockWriter).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Testing reset: Processing item 1 of 1: New item' },
},
],
}),
);
});
});
describe('helper functions', () => {
it('should call reporter.start through reportStart', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const input = { test: 'input' };
reportStart(reporter, input);
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
status: 'running',
updates: [{ type: 'input', data: input }],
}),
);
});
it('should call reporter.progress through reportProgress', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
reportProgress(reporter, 'Test message');
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
status: 'running',
updates: [{ type: 'progress', data: { message: 'Test message' } }],
}),
);
});
it('should call reporter.progress with custom data through reportProgress', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const customData = { step: 2, total: 5 };
reportProgress(reporter, 'Processing step', customData);
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
status: 'running',
updates: [{ type: 'progress', data: customData }],
}),
);
});
it('should call reporter.complete through reportComplete', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const output = { result: 'success' };
reportComplete(reporter, output);
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
status: 'completed',
updates: [{ type: 'output', data: output }],
}),
);
});
it('should call reporter.error through reportError', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const error: ToolError = { message: 'Test error', code: 'TEST_ERROR' };
reportError(reporter, error);
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
status: 'error',
updates: [
{
type: 'error',
data: { message: 'Test error', code: 'TEST_ERROR', details: undefined },
},
],
}),
);
});
it('should create batch reporter through createBatchProgressReporter', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const batchReporter = createBatchProgressReporter(reporter, 'Batch operation');
batchReporter.init(1);
batchReporter.next('Test item');
expect(mockWriter).toHaveBeenCalledWith(
expect.objectContaining({
updates: [
{
type: 'progress',
data: { message: 'Batch operation: Processing item 1 of 1: Test item' },
},
],
}),
);
});
it('should update customDisplayTitle when provided in start options', () => {
const reporter = createProgressReporter(mockConfig, 'test_tool', 'Test Tool');
const input = { test: 'data' };
reporter.start(input, { customDisplayTitle: 'Custom Title from Options' });
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'test_tool',
toolCallId: 'test-tool-call-id',
displayTitle: 'Test Tool',
customDisplayTitle: 'Custom Title from Options',
status: 'running',
updates: [
{
type: 'input',
data: input,
},
],
});
});
it('should preserve initial custom title when start is called without options', () => {
const reporter = createProgressReporter(
mockConfig,
'test_tool',
'Test Tool',
'Initial Custom Title',
);
const input = { test: 'data' };
reporter.start(input);
expect(mockWriter).toHaveBeenCalledWith({
type: 'tool',
toolName: 'test_tool',
toolCallId: 'test-tool-call-id',
displayTitle: 'Test Tool',
customDisplayTitle: 'Initial Custom Title',
status: 'running',
updates: [
{
type: 'input',
data: input,
},
],
});
});
});
});

View File

@@ -126,9 +126,11 @@ function extractNodeDetails(nodeType: INodeTypeDescription): NodeDetails {
* Factory function to create the node details tool
*/
export function createNodeDetailsTool(nodeTypes: INodeTypeDescription[]) {
return tool(
const DISPLAY_TITLE = 'Getting node details';
const dynamicTool = tool(
(input: unknown, config) => {
const reporter = createProgressReporter(config, 'get_node_details');
const reporter = createProgressReporter(config, 'get_node_details', DISPLAY_TITLE);
try {
// Validate input using Zod schema
@@ -194,4 +196,9 @@ export function createNodeDetailsTool(nodeTypes: INodeTypeDescription[]) {
schema: nodeDetailsSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -112,9 +112,11 @@ function buildResponseMessage(
* Factory function to create the node search tool
*/
export function createNodeSearchTool(nodeTypes: INodeTypeDescription[]) {
return tool(
(input: unknown, config) => {
const reporter = createProgressReporter(config, 'search_nodes');
const DISPLAY_TITLE = 'Searching nodes';
const dynamicTool = tool(
(input, config) => {
const reporter = createProgressReporter(config, 'search_nodes', DISPLAY_TITLE);
try {
// Validate input using Zod schema
@@ -210,4 +212,9 @@ You can search for multiple different criteria at once by providing an array of
schema: nodeSearchSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -73,9 +73,11 @@ function buildResponseMessage(
* Factory function to create the remove node tool
*/
export function createRemoveNodeTool(_logger?: Logger) {
return tool(
const DISPLAY_TITLE = 'Removing node';
const dynamicTool = tool(
(input, config) => {
const reporter = createProgressReporter(config, 'remove_node');
const reporter = createProgressReporter(config, 'remove_node', DISPLAY_TITLE);
try {
// Validate input using Zod schema
@@ -152,4 +154,9 @@ export function createRemoveNodeTool(_logger?: Logger) {
schema: removeNodeSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -38,7 +38,7 @@ jest.mock('crypto', () => ({
describe('AddNodeTool', () => {
let nodeTypesList: INodeTypeDescription[];
let addNodeTool: ReturnType<typeof createAddNodeTool>;
let addNodeTool: ReturnType<typeof createAddNodeTool>['tool'];
const mockGetCurrentTaskInput = getCurrentTaskInput as jest.MockedFunction<
typeof getCurrentTaskInput
>;
@@ -47,7 +47,7 @@ describe('AddNodeTool', () => {
jest.clearAllMocks();
nodeTypesList = [nodeTypes.code, nodeTypes.httpRequest, nodeTypes.webhook, nodeTypes.agent];
addNodeTool = createAddNodeTool(nodeTypesList);
addNodeTool = createAddNodeTool(nodeTypesList).tool;
});
afterEach(() => {
@@ -298,4 +298,23 @@ describe('AddNodeTool', () => {
expect(addedNode?.position?.[1]).toBeGreaterThan(100);
});
});
describe('getCustomDisplayTitle', () => {
it('should return node display name when nodeType exists', () => {
const tool = createAddNodeTool(nodeTypesList);
const result = tool.getCustomDisplayTitle?.({
nodeType: 'n8n-nodes-base.code',
name: 'My Code',
});
expect(result).toBe('Adding Code node');
});
it('should return default title when nodeType not found or missing', () => {
const tool = createAddNodeTool(nodeTypesList);
expect(tool.getCustomDisplayTitle?.({ nodeType: 'unknown.node' })).toBe('Adding node');
expect(tool.getCustomDisplayTitle?.({ name: 'Some Node' })).toBe('Adding node');
});
});
});

View File

@@ -30,7 +30,7 @@ jest.mock('@langchain/langgraph', () => ({
describe('ConnectNodesTool', () => {
let nodeTypesList: INodeTypeDescription[];
let connectNodesTool: ReturnType<typeof createConnectNodesTool>;
let connectNodesTool: ReturnType<typeof createConnectNodesTool>['tool'];
const mockGetCurrentTaskInput = getCurrentTaskInput as jest.MockedFunction<
typeof getCurrentTaskInput
>;
@@ -39,7 +39,7 @@ describe('ConnectNodesTool', () => {
jest.clearAllMocks();
nodeTypesList = [nodeTypes.code, nodeTypes.httpRequest, nodeTypes.webhook, nodeTypes.agent];
connectNodesTool = createConnectNodesTool(nodeTypesList);
connectNodesTool = createConnectNodesTool(nodeTypesList).tool;
});
afterEach(() => {
@@ -112,7 +112,7 @@ describe('ConnectNodesTool', () => {
// Update node types list
nodeTypesList = [nodeTypes.code, nodeTypes.httpRequest, agentNodeType, toolNodeType];
connectNodesTool = createConnectNodesTool(nodeTypesList);
connectNodesTool = createConnectNodesTool(nodeTypesList).tool;
const existingWorkflow = createWorkflow([
createNode({ id: 'agent1', name: 'AI Agent', type: '@n8n/n8n-nodes-langchain.agent' }),
@@ -176,7 +176,7 @@ describe('ConnectNodesTool', () => {
// Replace the agent node type in the list
nodeTypesList = nodeTypesList.filter((nt) => nt.name !== '@n8n/n8n-nodes-langchain.agent');
nodeTypesList.push(agentNodeType, languageModelNodeType);
connectNodesTool = createConnectNodesTool(nodeTypesList);
connectNodesTool = createConnectNodesTool(nodeTypesList).tool;
const existingWorkflow = createWorkflow([
createNode({
@@ -374,7 +374,7 @@ describe('ConnectNodesTool', () => {
});
nodeTypesList.push(multiOutputNode);
connectNodesTool = createConnectNodesTool(nodeTypesList);
connectNodesTool = createConnectNodesTool(nodeTypesList).tool;
const existingWorkflow = createWorkflow([
createNode({ id: 'multi1', name: 'Multi Output', type: 'test.multiOutput' }),

View File

@@ -27,7 +27,7 @@ jest.mock('@langchain/langgraph', () => ({
describe('NodeDetailsTool', () => {
let nodeTypesList: INodeTypeDescription[];
let nodeDetailsTool: ReturnType<typeof createNodeDetailsTool>;
let nodeDetailsTool: ReturnType<typeof createNodeDetailsTool>['tool'];
beforeEach(() => {
jest.clearAllMocks();
@@ -43,7 +43,7 @@ describe('NodeDetailsTool', () => {
nodeTypes.mergeNode,
nodeTypes.vectorStoreNode,
];
nodeDetailsTool = createNodeDetailsTool(nodeTypesList);
nodeDetailsTool = createNodeDetailsTool(nodeTypesList).tool;
});
afterEach(() => {
@@ -296,7 +296,7 @@ describe('NodeDetailsTool', () => {
});
const testNodeTypes = [...nodeTypesList, nodeWithManyProps];
const testTool = createNodeDetailsTool(testNodeTypes);
const testTool = createNodeDetailsTool(testNodeTypes).tool;
const mockConfig = createToolConfig('get_node_details', 'test-call-11');
@@ -373,7 +373,7 @@ describe('NodeDetailsTool', () => {
});
const testNodeTypes = [...nodeTypesList, complexNode];
const testTool = createNodeDetailsTool(testNodeTypes);
const testTool = createNodeDetailsTool(testNodeTypes).tool;
const mockConfig = createToolConfig('get_node_details', 'test-call-13');
@@ -436,7 +436,7 @@ describe('NodeDetailsTool', () => {
});
const testNodeTypes = [...nodeTypesList, noOutputNode];
const testTool = createNodeDetailsTool(testNodeTypes);
const testTool = createNodeDetailsTool(testNodeTypes).tool;
const mockConfig = createToolConfig('get_node_details', 'test-call-15');

View File

@@ -24,7 +24,7 @@ jest.mock('@langchain/langgraph', () => ({
describe('NodeSearchTool', () => {
let nodeTypesList: INodeTypeDescription[];
let nodeSearchTool: ReturnType<typeof createNodeSearchTool>;
let nodeSearchTool: ReturnType<typeof createNodeSearchTool>['tool'];
beforeEach(() => {
jest.clearAllMocks();
@@ -75,7 +75,7 @@ describe('NodeSearchTool', () => {
// Expression-based node
nodeTypes.vectorStoreNode,
];
nodeSearchTool = createNodeSearchTool(nodeTypesList);
nodeSearchTool = createNodeSearchTool(nodeTypesList).tool;
});
afterEach(() => {
@@ -222,7 +222,7 @@ describe('NodeSearchTool', () => {
{
queries: [
{
// @ts-expect-error Testing invalid input
// @ts-expect-error testing invalid query type
queryType: 'invalid',
query: 'test',
},
@@ -342,7 +342,7 @@ describe('NodeSearchTool', () => {
}),
);
const testNodeTypes = [...nodeTypesList, ...manyHttpNodes];
const testTool = createNodeSearchTool(testNodeTypes);
const testTool = createNodeSearchTool(testNodeTypes).tool;
const mockConfig = createToolConfig('search_nodes', 'test-call-13');

View File

@@ -26,14 +26,14 @@ jest.mock('@langchain/langgraph', () => ({
}));
describe('RemoveNodeTool', () => {
let removeNodeTool: ReturnType<typeof createRemoveNodeTool>;
let removeNodeTool: ReturnType<typeof createRemoveNodeTool>['tool'];
const mockGetCurrentTaskInput = getCurrentTaskInput as jest.MockedFunction<
typeof getCurrentTaskInput
>;
beforeEach(() => {
jest.clearAllMocks();
removeNodeTool = createRemoveNodeTool();
removeNodeTool = createRemoveNodeTool().tool;
});
afterEach(() => {

View File

@@ -36,7 +36,7 @@ jest.mock('../../../src/chains/parameter-updater', () => ({
describe('UpdateNodeParametersTool', () => {
let nodeTypesList: INodeTypeDescription[];
let updateNodeParametersTool: ReturnType<typeof createUpdateNodeParametersTool>;
let updateNodeParametersTool: ReturnType<typeof createUpdateNodeParametersTool>['tool'];
const mockGetCurrentTaskInput = getCurrentTaskInput as jest.MockedFunction<
typeof getCurrentTaskInput
>;
@@ -59,7 +59,7 @@ describe('UpdateNodeParametersTool', () => {
parameterUpdaterModule.createParameterUpdaterChain.mockReturnValue(mockChain);
nodeTypesList = [nodeTypes.code, nodeTypes.httpRequest, nodeTypes.webhook, nodeTypes.setNode];
updateNodeParametersTool = createUpdateNodeParametersTool(nodeTypesList, mockLLM);
updateNodeParametersTool = createUpdateNodeParametersTool(nodeTypesList, mockLLM).tool;
});
afterEach(() => {
@@ -468,7 +468,7 @@ describe('UpdateNodeParametersTool', () => {
};
// Create tool with custom node type
const customTool = createUpdateNodeParametersTool([customNodeType], mockLLM);
const customTool = createUpdateNodeParametersTool([customNodeType], mockLLM).tool;
const existingWorkflow = createWorkflow([
createNode({

View File

@@ -22,6 +22,8 @@ import {
} from './utils/parameter-update.utils';
import type { UpdateNodeParametersOutput } from '../types/tools';
const DISPLAY_TITLE = 'Updating node parameters';
/**
* Schema for update node parameters input
*/
@@ -111,6 +113,17 @@ async function processParameterUpdates(
return fixExpressionPrefixes(newParameters.parameters) as INodeParameters;
}
function getCustomNodeTitle(input: Record<string, unknown>, nodes?: INode[]): string {
if ('nodeId' in input && typeof input['nodeId'] === 'string') {
const targetNode = nodes?.find((node) => node.id === input.nodeId);
if (targetNode) {
return `Updating "${targetNode.name}" node parameters`;
}
}
return DISPLAY_TITLE;
}
/**
* Factory function to create the update node parameters tool
*/
@@ -120,22 +133,24 @@ export function createUpdateNodeParametersTool(
logger?: Logger,
instanceUrl?: string,
) {
return tool(
const dynamicTool = tool(
async (input, config) => {
const reporter = createProgressReporter(config, 'update_node_parameters');
const reporter = createProgressReporter(config, 'update_node_parameters', DISPLAY_TITLE);
try {
// Validate input using Zod schema
const validatedInput = updateNodeParametersSchema.parse(input);
const { nodeId, changes } = validatedInput;
// Report tool start
reporter.start(validatedInput);
// Get current state
const state = getWorkflowState();
const workflow = getCurrentWorkflow(state);
// Report tool start
reporter.start(validatedInput, {
customDisplayTitle: getCustomNodeTitle(input, workflow.nodes),
});
// Find the node
const node = validateNodeExists(nodeId, workflow.nodes);
if (!node) {
@@ -233,4 +248,9 @@ export function createUpdateNodeParametersTool(
schema: updateNodeParametersSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -26,6 +26,8 @@ export interface ToolProgressMessage<TToolName extends string = string> {
toolCallId?: string;
status: 'running' | 'completed' | 'error';
updates: ProgressUpdate[];
displayTitle?: string; // Name of tool action in UI, for example "Adding nodes"
customDisplayTitle?: string; // Custom name for tool action in UI, for example "Adding Gmail node"
}
/**
@@ -41,7 +43,7 @@ export interface ToolError {
* Progress reporter interface for tools
*/
export interface ProgressReporter {
start: <T>(input: T) => void;
start: <T>(input: T, options?: { customDisplayTitle: string }) => void;
progress: (message: string, data?: Record<string, unknown>) => void;
complete: <T>(output: T) => void;
error: (error: ToolError) => void;

View File

@@ -1,4 +1,6 @@
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
import type { ToolCall } from '@langchain/core/messages/tool';
import type { DynamicStructuredTool } from '@langchain/core/tools';
import type {
AgentMessageChunk,
@@ -7,6 +9,12 @@ import type {
StreamOutput,
} from '../types/streaming';
export interface BuilderTool {
tool: DynamicStructuredTool;
displayTitle: string;
getCustomDisplayTitle?: (values: Record<string, unknown>) => string;
}
/**
* Tools which should trigger canvas updates
*/
@@ -129,78 +137,135 @@ export async function* createStreamProcessor(
}
}
/**
* Format a HumanMessage into the expected output format
*/
function formatHumanMessage(msg: HumanMessage): Record<string, unknown> {
return {
role: 'user',
type: 'message',
text: msg.content,
};
}
/**
* Process array content from AIMessage and return formatted text messages
*/
function processArrayContent(content: unknown[]): Array<Record<string, unknown>> {
const textMessages = content.filter(
(c): c is { type: string; text: string } =>
typeof c === 'object' && c !== null && 'type' in c && c.type === 'text' && 'text' in c,
);
return textMessages.map((textMessage) => ({
role: 'assistant',
type: 'message',
text: textMessage.text,
}));
}
/**
* Process AIMessage content and return formatted messages
*/
function processAIMessageContent(msg: AIMessage): Array<Record<string, unknown>> {
if (!msg.content) {
return [];
}
if (Array.isArray(msg.content)) {
return processArrayContent(msg.content);
}
return [
{
role: 'assistant',
type: 'message',
text: msg.content,
},
];
}
/**
* Create a formatted tool call message
*/
function createToolCallMessage(
toolCall: ToolCall,
builderTool?: BuilderTool,
): Record<string, unknown> {
return {
id: toolCall.id,
toolCallId: toolCall.id,
role: 'assistant',
type: 'tool',
toolName: toolCall.name,
displayTitle: builderTool?.displayTitle,
customDisplayTitle: toolCall.args && builderTool?.getCustomDisplayTitle?.(toolCall.args),
status: 'completed',
updates: [
{
type: 'input',
data: toolCall.args || {},
},
],
};
}
/**
* Process tool calls from AIMessage and return formatted tool messages
*/
function processToolCalls(
toolCalls: ToolCall[],
builderTools?: BuilderTool[],
): Array<Record<string, unknown>> {
return toolCalls.map((toolCall) => {
const builderTool = builderTools?.find((bt) => bt.tool.name === toolCall.name);
return createToolCallMessage(toolCall, builderTool);
});
}
/**
* Process a ToolMessage and add its output to the corresponding tool call
*/
function processToolMessage(
msg: ToolMessage,
formattedMessages: Array<Record<string, unknown>>,
): void {
const toolCallId = msg.tool_call_id;
// Find the tool message by ID (search backwards for efficiency)
for (let i = formattedMessages.length - 1; i >= 0; i--) {
const m = formattedMessages[i];
if (m.type === 'tool' && m.id === toolCallId) {
// Add output to updates array
m.updates ??= [];
(m.updates as Array<Record<string, unknown>>).push({
type: 'output',
data: typeof msg.content === 'string' ? { result: msg.content } : msg.content,
});
break;
}
}
}
export function formatMessages(
messages: Array<AIMessage | HumanMessage | ToolMessage>,
builderTools?: BuilderTool[],
): Array<Record<string, unknown>> {
const formattedMessages: Array<Record<string, unknown>> = [];
for (const msg of messages) {
if (msg instanceof HumanMessage) {
formattedMessages.push({
role: 'user',
type: 'message',
text: msg.content,
});
formattedMessages.push(formatHumanMessage(msg));
} else if (msg instanceof AIMessage) {
// Add the AI message content if it exists
if (msg.content) {
if (Array.isArray(msg.content)) {
// Handle array content (multi-part messages)
const textMessages = msg.content.filter((c) => c.type === 'text');
// Add AI message content
formattedMessages.push(...processAIMessageContent(msg));
textMessages.forEach((textMessage) => {
if (textMessage.type !== 'text') {
return;
}
formattedMessages.push({
role: 'assistant',
type: 'message',
text: textMessage.text,
});
});
} else {
formattedMessages.push({
role: 'assistant',
type: 'message',
text: msg.content,
});
}
}
// Handle tool calls in AI messages
if (msg.tool_calls && msg.tool_calls.length > 0) {
// Add tool messages for each tool call
for (const toolCall of msg.tool_calls) {
formattedMessages.push({
id: toolCall.id,
toolCallId: toolCall.id,
role: 'assistant',
type: 'tool',
toolName: toolCall.name,
status: 'completed',
updates: [
{
type: 'input',
data: toolCall.args || {},
},
],
});
}
// Add tool calls if present
if (msg.tool_calls?.length) {
formattedMessages.push(...processToolCalls(msg.tool_calls, builderTools));
}
} else if (msg instanceof ToolMessage) {
// Find the tool message by ID and add the output
const toolCallId = msg.tool_call_id;
for (let i = formattedMessages.length - 1; i >= 0; i--) {
const m = formattedMessages[i];
if (m.type === 'tool' && m.id === toolCallId) {
// Add output to updates array
m.updates ??= [];
(m.updates as Array<Record<string, unknown>>).push({
type: 'output',
data: typeof msg.content === 'string' ? { result: msg.content } : msg.content,
});
break;
}
}
processToolMessage(msg, formattedMessages);
}
}

View File

@@ -1,4 +1,5 @@
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
import type { DynamicStructuredTool } from '@langchain/core/tools';
import type {
AgentMessageChunk,
@@ -471,5 +472,430 @@ describe('stream-processor', () => {
data: {},
});
});
it('should handle AIMessage with array content (multi-part messages)', () => {
const message = new AIMessage('');
// Manually set the content to array format since LangChain constructor might not accept arrays directly
message.content = [
{ type: 'text', text: 'First part' },
{ type: 'text', text: 'Second part' },
{ type: 'image', url: 'http://example.com/image.png' },
];
const messages = [message];
const result = formatMessages(messages);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
role: 'assistant',
type: 'message',
text: 'First part',
});
expect(result[1]).toEqual({
role: 'assistant',
type: 'message',
text: 'Second part',
});
});
it('should handle AIMessage with array content containing no text', () => {
const message = new AIMessage('');
message.content = [
{ type: 'image', url: 'http://example.com/image.png' },
{ type: 'video', url: 'http://example.com/video.mp4' },
];
const messages = [message];
const result = formatMessages(messages);
expect(result).toHaveLength(0);
});
it('should handle AIMessage with empty array content', () => {
const message = new AIMessage('');
message.content = [];
const messages = [message];
const result = formatMessages(messages);
expect(result).toHaveLength(0);
});
it('should handle AIMessage with empty string content', () => {
const messages = [new AIMessage('')];
const result = formatMessages(messages);
expect(result).toHaveLength(0);
});
it('should handle AIMessage with null content', () => {
const message = new AIMessage('');
// Test the function's robustness by simulating a corrupted message
Object.defineProperty(message, 'content', { value: null, writable: true });
const messages = [message];
const result = formatMessages(messages);
expect(result).toHaveLength(0);
});
it('should use builder tool display titles', () => {
const builderTools = [
{
tool: { name: 'add_nodes' } as DynamicStructuredTool,
displayTitle: 'Add Node',
},
{
tool: { name: 'connect_nodes' } as DynamicStructuredTool,
displayTitle: 'Connect Nodes',
},
];
const aiMessage = new AIMessage('');
aiMessage.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'n8n-nodes-base.code' },
type: 'tool_call',
},
];
const messages = [aiMessage];
const result = formatMessages(messages, builderTools);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: 'call-1',
toolCallId: 'call-1',
role: 'assistant',
type: 'tool',
toolName: 'add_nodes',
displayTitle: 'Add Node',
status: 'completed',
updates: [
{
type: 'input',
data: { nodeType: 'n8n-nodes-base.code' },
},
],
});
});
it('should use custom display titles from builder tools', () => {
const builderTools = [
{
tool: { name: 'add_nodes' } as DynamicStructuredTool,
displayTitle: 'Add Node',
getCustomDisplayTitle: (values: Record<string, unknown>) =>
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Add ${values.nodeType} Node`,
},
];
const aiMessage = new AIMessage('');
aiMessage.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'Code' },
type: 'tool_call',
},
];
const messages = [aiMessage];
const result = formatMessages(messages, builderTools);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: 'call-1',
toolCallId: 'call-1',
role: 'assistant',
type: 'tool',
toolName: 'add_nodes',
displayTitle: 'Add Node',
customDisplayTitle: 'Add Code Node',
status: 'completed',
updates: [
{
type: 'input',
data: { nodeType: 'Code' },
},
],
});
});
it('should handle custom display title when args is null/undefined', () => {
const builderTools = [
{
tool: { name: 'clear_workflow' } as DynamicStructuredTool,
displayTitle: 'Clear Workflow',
getCustomDisplayTitle: (values: Record<string, unknown>) =>
`Custom: ${Object.keys(values).length} args`,
},
];
const aiMessage = new AIMessage('');
const toolCall = {
id: 'call-1',
name: 'clear_workflow',
args: {} as Record<string, unknown>,
type: 'tool_call' as const,
};
// Simulate a corrupted tool call with null args
Object.defineProperty(toolCall, 'args', { value: null, writable: true });
aiMessage.tool_calls = [toolCall];
const messages = [aiMessage];
const result = formatMessages(messages, builderTools);
expect(result[0].customDisplayTitle).toBeNull();
});
it('should handle tool call with undefined args', () => {
const aiMessage = new AIMessage('');
const toolCall = {
id: 'call-1',
name: 'clear_workflow',
args: {} as Record<string, unknown>,
type: 'tool_call' as const,
};
// Simulate a corrupted tool call with undefined args
Object.defineProperty(toolCall, 'args', { value: undefined, writable: true });
aiMessage.tool_calls = [toolCall];
const messages = [aiMessage];
const result = formatMessages(messages);
// @ts-expect-error Lnagchain types are not propagated
expect(result[0].updates?.[0]).toEqual({
type: 'input',
data: {},
});
});
it('should handle ToolMessage with no matching tool call', () => {
const toolMessage = new ToolMessage({
content: 'Orphaned tool result',
tool_call_id: 'non-existent-call',
});
const messages = [toolMessage];
const result = formatMessages(messages);
expect(result).toHaveLength(0);
});
it('should handle multiple ToolMessages for the same tool call', () => {
const aiMessage = new AIMessage('');
aiMessage.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'n8n-nodes-base.code' },
type: 'tool_call',
},
];
const toolMessage1 = new ToolMessage({
content: 'First result',
tool_call_id: 'call-1',
});
const toolMessage2 = new ToolMessage({
content: 'Second result',
tool_call_id: 'call-1',
});
const messages = [aiMessage, toolMessage1, toolMessage2];
const result = formatMessages(messages);
expect(result).toHaveLength(1);
expect(result[0].updates).toHaveLength(3);
// @ts-expect-error Lnagchain types are not propagated
expect(result[0].updates?.[1]).toEqual({
type: 'output',
data: { result: 'First result' },
});
// @ts-expect-error Lnagchain types are not propagated
expect(result[0].updates?.[2]).toEqual({
type: 'output',
data: { result: 'Second result' },
});
});
it('should handle ToolMessage appearing before corresponding AIMessage tool call', () => {
const toolMessage = new ToolMessage({
content: 'Tool result',
tool_call_id: 'call-1',
});
const aiMessage = new AIMessage('');
aiMessage.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'n8n-nodes-base.code' },
type: 'tool_call',
},
];
const messages = [toolMessage, aiMessage];
const result = formatMessages(messages);
// When ToolMessage comes before AIMessage, the ToolMessage cannot find the tool call to attach to
// so it gets ignored, and only the tool call from AIMessage is processed
expect(result).toHaveLength(1);
expect(result[0].updates).toHaveLength(1); // Only the input, no output since ToolMessage came before
// @ts-expect-error Lnagchain types are not propagated
expect(result[0].updates?.[0]).toEqual({
type: 'input',
data: { nodeType: 'n8n-nodes-base.code' },
});
});
it('should handle empty messages array', () => {
const result = formatMessages([]);
expect(result).toHaveLength(0);
});
it('should handle messages with unknown message type', () => {
// Create an object that doesn't match any of the expected message types
const unknownMessage = {
content: 'Unknown message type',
type: 'unknown',
};
const result = formatMessages([
unknownMessage as unknown as AIMessage | HumanMessage | ToolMessage,
]);
expect(result).toHaveLength(0);
});
it('should preserve initialization of updates array when undefined', () => {
const aiMessage = new AIMessage('');
aiMessage.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'n8n-nodes-base.code' },
type: 'tool_call',
},
];
const toolMessage = new ToolMessage({
content: 'Tool result',
tool_call_id: 'call-1',
});
const messages = [aiMessage, toolMessage];
const result = formatMessages(messages);
expect(result[0].updates).toBeDefined();
expect(Array.isArray(result[0].updates)).toBe(true);
expect(result[0].updates).toHaveLength(2);
});
it('should handle complex scenario with multiple message types and builder tools', () => {
const builderTools = [
{
tool: { name: 'add_nodes' } as DynamicStructuredTool,
displayTitle: 'Add Node',
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
getCustomDisplayTitle: (values: Record<string, unknown>) => `Add ${values.nodeType} Node`,
},
{
tool: { name: 'connect_nodes' } as DynamicStructuredTool,
displayTitle: 'Connect Nodes',
},
];
const humanMessage = new HumanMessage('Please create a workflow');
const aiMessage1 = new AIMessage('');
aiMessage1.content = [
{ type: 'text', text: 'I will help you create a workflow.' },
{ type: 'text', text: 'Let me add some nodes.' },
];
const aiMessage2 = new AIMessage('');
aiMessage2.tool_calls = [
{
id: 'call-1',
name: 'add_nodes',
args: { nodeType: 'Code' },
type: 'tool_call',
},
{
id: 'call-2',
name: 'connect_nodes',
args: { source: 'node1', target: 'node2' },
type: 'tool_call',
},
];
const toolMessage1 = new ToolMessage({
content: 'Node added successfully',
tool_call_id: 'call-1',
});
const toolMessage2 = new ToolMessage({
// @ts-expect-error Lnagchain types are not propagated
content: { success: true, connectionId: 'conn-1' },
tool_call_id: 'call-2',
});
const messages = [humanMessage, aiMessage1, aiMessage2, toolMessage1, toolMessage2];
const result = formatMessages(messages, builderTools);
expect(result).toHaveLength(5); // 1 user + 2 text messages + 2 tool calls
expect(result[0]).toEqual({
role: 'user',
type: 'message',
text: 'Please create a workflow',
});
expect(result[1]).toEqual({
role: 'assistant',
type: 'message',
text: 'I will help you create a workflow.',
});
expect(result[2]).toEqual({
role: 'assistant',
type: 'message',
text: 'Let me add some nodes.',
});
expect(result[3].toolName).toBe('add_nodes');
expect(result[3].displayTitle).toBe('Add Node');
expect(result[3].customDisplayTitle).toBe('Add Code Node');
expect(result[3].updates).toHaveLength(2);
expect(result[4].toolName).toBe('connect_nodes');
expect(result[4].displayTitle).toBe('Connect Nodes');
expect(result[4].customDisplayTitle).toBeUndefined();
expect(result[4].updates).toHaveLength(2);
// @ts-expect-error Lnagchain types are not propagated
expect(result[4].updates?.[1]).toEqual({
type: 'output',
data: { success: true, connectionId: 'conn-1' },
});
});
});
});

View File

@@ -13,10 +13,9 @@ import {
type NodeExecutionSchema,
} from 'n8n-workflow';
import { workflowNameChain } from '@/chains/workflow-name';
import { DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS, MAX_AI_BUILDER_PROMPT_LENGTH } from '@/constants';
import { conversationCompactChain } from './chains/conversation-compact';
import { workflowNameChain } from './chains/workflow-name';
import { DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS, MAX_AI_BUILDER_PROMPT_LENGTH } from './constants';
import { LLMServiceError, ValidationError } from './errors';
import { createAddNodeTool } from './tools/add-node.tool';
import { createConnectNodesTool } from './tools/connect-nodes.tool';
@@ -27,7 +26,7 @@ import { createRemoveNodeTool } from './tools/remove-node.tool';
import { createUpdateNodeParametersTool } from './tools/update-node-parameters.tool';
import type { SimpleWorkflow } from './types/workflow';
import { processOperations } from './utils/operations-processor';
import { createStreamProcessor, formatMessages } from './utils/stream-processor';
import { createStreamProcessor, formatMessages, type BuilderTool } from './utils/stream-processor';
import { extractLastTokenUsage } from './utils/token-usage';
import { executeToolsInParallel } from './utils/tool-executor';
import { WorkflowState } from './workflow-state';
@@ -74,8 +73,8 @@ export class WorkflowBuilderAgent {
this.instanceUrl = config.instanceUrl;
}
private createWorkflow() {
const tools = [
private getBuilderTools(): BuilderTool[] {
return [
createNodeSearchTool(this.parsedNodeTypes),
createNodeDetailsTool(this.parsedNodeTypes),
createAddNodeTool(this.parsedNodeTypes),
@@ -88,6 +87,13 @@ export class WorkflowBuilderAgent {
this.instanceUrl,
),
];
}
private createWorkflow() {
const builderTools = this.getBuilderTools();
// Extract just the tools for LLM binding
const tools = builderTools.map((bt) => bt.tool);
// Create a map for quick tool lookup
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
@@ -485,7 +491,7 @@ export class WorkflowBuilderAgent {
sessions.push({
sessionId: threadId,
messages: formatMessages(messages),
messages: formatMessages(messages, this.getBuilderTools()),
lastUpdated: checkpoint.checkpoint.ts,
});
}