feat(core): Add workflow JSON context trimming for AI workflow builder (no-changelog) (#18828)

This commit is contained in:
Eugene
2025-08-28 16:22:44 +02:00
committed by GitHub
parent 84936848c9
commit 44b686e944
14 changed files with 762 additions and 116 deletions

View File

@@ -47,6 +47,7 @@
"@n8n/di": "workspace:*",
"@n8n_io/ai-assistant-sdk": "catalog:",
"langsmith": "^0.3.45",
"lodash": "catalog:",
"n8n-workflow": "workspace:*",
"picocolors": "catalog:",
"zod": "catalog:"
@@ -54,10 +55,10 @@
"devDependencies": {
"@n8n/typescript-config": "workspace:*",
"@types/cli-progress": "^3.11.5",
"p-limit": "^3.1.0",
"cli-progress": "^3.12.0",
"cli-table3": "^0.6.3",
"jest-mock-extended": "^3.0.4",
"madge": "^8.0.0"
"madge": "^8.0.0",
"p-limit": "^3.1.0"
}
}

View File

@@ -1,3 +1,36 @@
/**
* Maximum length of user prompt message in characters.
* Prevents excessively long messages that could consume too many tokens.
*/
export const MAX_AI_BUILDER_PROMPT_LENGTH = 1000; // characters
/**
* Token limits for the LLM context window.
*/
export const MAX_TOTAL_TOKENS = 200_000; // Total context window size (input + output)
export const MAX_OUTPUT_TOKENS = 16_000; // Reserved tokens for model response
export const MAX_INPUT_TOKENS = MAX_TOTAL_TOKENS - MAX_OUTPUT_TOKENS - 10_000; // Available tokens for input (with some buffer to account for estimation errors)
/**
* Maximum length of individual parameter value that can be retrieved via tool call.
* Prevents tool responses from becoming too large and filling up the context.
*/
export const MAX_PARAMETER_VALUE_LENGTH = 30_000; // Maximum length of individual parameter value that can be retrieved via tool call
/**
* Token threshold for automatically compacting conversation history.
* When conversation exceeds this limit, older messages are summarized to free up space.
*/
export const DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS = 20_000; // Tokens threshold for auto-compacting the conversation
/**
* Maximum token count for workflow JSON after trimming.
* Used to determine when a workflow is small enough to include in context.
*/
export const MAX_WORKFLOW_LENGTH_TOKENS = 30_000; // Tokens
/**
* Average character-to-token ratio for Anthropic models.
* Used for rough token count estimation from character counts.
*/
export const AVG_CHARS_PER_TOKEN_ANTHROPIC = 2.5;

View File

@@ -133,6 +133,27 @@ export class ParameterUpdateError extends OperationalError {
}
}
/**
* Error thrown when parameter value is too large to retrieve
*/
export class ParameterTooLargeError extends OperationalError {
constructor(
message: string,
options?: OperationalErrorOptions & { nodeId?: string; parameter?: string; maxSize?: number },
) {
super(message, {
...options,
tags: {
...options?.tags,
nodeId: options?.nodeId,
parameter: options?.parameter,
maxSize: options?.maxSize,
},
shouldReport: false,
});
}
}
/**
* Error thrown when workflow state is invalid
*/

View File

@@ -1,4 +1,6 @@
// Different LLMConfig type for this file - specific to LLM providers
import { MAX_OUTPUT_TOKENS } from '@/constants';
interface LLMProviderConfig {
apiKey: string;
baseUrl?: string;
@@ -51,7 +53,7 @@ export const anthropicClaudeSonnet4 = async (config: LLMProviderConfig) => {
model: 'claude-sonnet-4-20250514',
apiKey: config.apiKey,
temperature: 0,
maxTokens: 16000,
maxTokens: MAX_OUTPUT_TOKENS,
anthropicApiUrl: config.baseUrl,
clientOptions: {
defaultHeaders: config.headers,

View File

@@ -0,0 +1,157 @@
import { tool } from '@langchain/core/tools';
import type { Logger } from '@n8n/backend-common';
import get from 'lodash/get';
import type { INode, NodeParameterValueType } from 'n8n-workflow';
import { z } from 'zod';
import { MAX_PARAMETER_VALUE_LENGTH } from '@/constants';
import {
createNodeParameterTooLargeError,
getCurrentWorkflow,
getWorkflowState,
} from '@/tools/helpers';
import { ValidationError, ToolExecutionError } from '../errors';
import { createProgressReporter, reportProgress } from './helpers/progress';
import { createSuccessResponse, createErrorResponse } from './helpers/response';
import { validateNodeExists, createNodeNotFoundError } from './helpers/validation';
import type { GetNodeParameterOutput } from '../types/tools';
/**
* Schema for getting specific node parameter
*/
const getNodeParameterSchema = z.object({
nodeId: z.string().describe('The ID of the node to extract parameter value'),
path: z
.string()
.describe('Path to the specific parameter to extract, e.g., "url" or "options.baseUrl"'),
});
function extractParameterValue(node: INode, path: string): NodeParameterValueType | undefined {
const nodeParameters = node.parameters;
return get(nodeParameters, path);
}
function formatNodeParameter(path: string, value: NodeParameterValueType): string {
const parts = [];
parts.push('<node_parameter>');
parts.push('<parameter_path>');
parts.push(path);
parts.push('</parameter_path>');
parts.push('<parameter_value>');
if (typeof value === 'string') {
parts.push(value);
} else {
parts.push(JSON.stringify(value, null, 2));
}
parts.push('</parameter_value>');
parts.push('</node_parameter>');
return parts.join('\n');
}
/**
* Factory function to create the get node parameter tool
*/
export function createGetNodeParameterTool(logger?: Logger) {
const DISPLAY_TITLE = 'Getting node parameter';
const dynamicTool = tool(
(input: unknown, config) => {
const reporter = createProgressReporter(config, 'get_node_parameter', DISPLAY_TITLE);
try {
// Validate input using Zod schema
const validatedInput = getNodeParameterSchema.parse(input);
const { nodeId, path } = validatedInput;
// Report tool start
reporter.start(validatedInput);
// Report progress
logger?.debug(`Looking up parameter ${path} for ${nodeId}...`);
reportProgress(reporter, `Looking up parameter ${path} for ${nodeId}...`);
// Get current state
const state = getWorkflowState();
const workflow = getCurrentWorkflow(state);
// Find the node
const node = validateNodeExists(nodeId, workflow.nodes);
if (!node) {
logger?.debug(`Node with ID ${nodeId} not found`);
const error = createNodeNotFoundError(nodeId);
reporter.error(error);
return createErrorResponse(config, error);
}
// Extract
const parameterValue = extractParameterValue(node, path);
if (parameterValue === undefined) {
logger?.debug(`Parameter path ${path} not found in node ${node.name}`);
const error = new ValidationError(
`Parameter path "${path}" not found in node "${node.name}"`,
{
extra: { nodeId, path },
},
);
reporter.error(error);
return createErrorResponse(config, error);
}
// Report completion
logger?.debug(`Parameter value for path ${path} in node ${node.name} retrieved`);
const formattedParameterValue = formatNodeParameter(path, parameterValue);
if (formattedParameterValue.length > MAX_PARAMETER_VALUE_LENGTH) {
const error = createNodeParameterTooLargeError(nodeId, path, MAX_PARAMETER_VALUE_LENGTH);
reporter.error(error);
return createErrorResponse(config, error);
}
const output: GetNodeParameterOutput = {
message: 'Parameter value retrieved successfully',
};
reporter.complete(output);
// Return success response
return createSuccessResponse(config, formattedParameterValue);
} catch (error) {
// Handle validation or unexpected errors
if (error instanceof z.ZodError) {
const validationError = new ValidationError('Invalid input parameters', {
extra: { errors: error.errors },
});
reporter.error(validationError);
return createErrorResponse(config, validationError);
}
const toolError = new ToolExecutionError(
error instanceof Error ? error.message : 'Unknown error occurred',
{
toolName: 'get_node_parameter',
cause: error instanceof Error ? error : undefined,
},
);
reporter.error(toolError);
return createErrorResponse(config, toolError);
}
},
{
name: 'get_node_parameter',
description:
'Get the value of a specific parameter of a specific node. Use this ONLY to retrieve parameters omitted in the workflow JSON context because of the size.',
schema: getNodeParameterSchema,
},
);
return {
tool: dynamicTool,
displayTitle: DISPLAY_TITLE,
};
}

View File

@@ -4,6 +4,7 @@ import {
ConnectionError,
NodeNotFoundError,
NodeTypeNotFoundError,
ParameterTooLargeError,
ValidationError,
} from '../../errors';
import type { ToolError } from '../../types/tools';
@@ -106,6 +107,27 @@ export function createNodeTypeNotFoundError(nodeTypeName: string): ToolError {
};
}
/**
* Create a node parameter is too large error
*/
export function createNodeParameterTooLargeError(
nodeId: string,
parameter: string,
maxSize: number,
): ToolError {
const error = new ParameterTooLargeError('Parameter value is too large to retrieve', {
parameter,
nodeId,
maxSize,
});
return {
message: error.message,
code: 'NODE_PARAMETER_TOO_LARGE',
details: { nodeId, parameter, maxSize: maxSize.toString() },
};
}
/**
* Check if a workflow has nodes
*/

View File

@@ -370,7 +370,11 @@ Be warm, helpful, and most importantly concise. Focus on actionable information.
const currentWorkflowJson = `
<current_workflow_json>
{workflowJSON}
</current_workflow_json>`;
</current_workflow_json>
<trimmed_workflow_json_note>
Note: Large property values of the nodes in the workflow JSON above may be trimmed to fit within token limits.
Use get_node_parameter tool to get full details when needed.
</trimmed_workflow_json_note>`;
const currentExecutionData = `
<current_simplified_execution_data>

View File

@@ -3,8 +3,11 @@ import { tool } from '@langchain/core/tools';
import type { INode, INodeTypeDescription, INodeParameters, Logger } from 'n8n-workflow';
import { z } from 'zod';
import { trimWorkflowJSON } from '@/utils/trim-workflow-context';
import { createParameterUpdaterChain } from '../chains/parameter-updater';
import { ValidationError, ParameterUpdateError, ToolExecutionError } from '../errors';
import type { UpdateNodeParametersOutput } from '../types/tools';
import { createProgressReporter, reportProgress } from './helpers/progress';
import { createSuccessResponse, createErrorResponse } from './helpers/response';
import { getCurrentWorkflow, getWorkflowState, updateNodeInWorkflow } from './helpers/state';
@@ -20,7 +23,6 @@ import {
updateNodeWithParameters,
fixExpressionPrefixes,
} from './utils/parameter-update.utils';
import type { UpdateNodeParametersOutput } from '../types/tools';
const DISPLAY_TITLE = 'Updating node parameters';
@@ -81,7 +83,7 @@ async function processParameterUpdates(
);
const newParameters = (await parametersChain.invoke({
workflow_json: workflow,
workflow_json: trimWorkflowJSON(workflow),
execution_schema: state.workflowContext?.executionSchema ?? 'NO SCHEMA',
execution_data: state.workflowContext?.executionData ?? 'NO EXECUTION DATA YET',
node_id: nodeId,

View File

@@ -125,3 +125,10 @@ export interface NodeSearchOutput {
totalResults: number;
message: string;
}
/**
* Output type for get node parameter tool
*/
export interface GetNodeParameterOutput {
message: string; // This is only to report success or error, without actual value (we don't need to send it to the frontend)
}

View File

@@ -0,0 +1,287 @@
import type { INode } from 'n8n-workflow';
import { createNode, createWorkflow } from '../../../test/test-utils';
import type { SimpleWorkflow } from '../../types/workflow';
import { trimWorkflowJSON } from '../trim-workflow-context';
describe('trimWorkflowJSON', () => {
describe('Small workflows', () => {
it('should not modify small workflows that fit within token limits', () => {
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
parameters: {
url: 'https://api.example.com/endpoint',
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
}),
]);
const result = trimWorkflowJSON(workflow);
// Workflow should be unchanged
expect(result).toEqual(workflow);
expect(result.nodes[0].parameters).toEqual(workflow.nodes[0].parameters);
});
it('should handle empty workflows', () => {
const workflow: SimpleWorkflow = createWorkflow([]);
const result = trimWorkflowJSON(workflow);
expect(result.nodes).toEqual([]);
expect(result.connections).toEqual({});
});
it('should handle workflows with nodes but no parameters', () => {
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.start',
parameters: {},
}),
]);
const result = trimWorkflowJSON(workflow);
expect(result).toEqual(workflow);
});
});
describe('Large parameter trimming', () => {
it('should trim large string parameters', () => {
const largeString = 'x'.repeat(15000);
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Code Node',
type: 'n8n-nodes-base.code',
parameters: {
jsCode: largeString,
smallParam: 'keep this',
},
}),
]);
const result = trimWorkflowJSON(workflow);
expect(result.nodes[0].parameters.jsCode).toBe('[Large value omitted]');
expect(result.nodes[0].parameters.smallParam).toBe('keep this');
});
it('should trim large arrays', () => {
const largeArray = new Array(5000).fill({ item: 'data' });
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Item Lists',
type: 'n8n-nodes-base.itemLists',
parameters: {
items: largeArray,
operation: 'aggregateItems',
},
}),
]);
const result = trimWorkflowJSON(workflow);
expect(result.nodes[0].parameters.items).toBe('[Large array omitted]');
expect(result.nodes[0].parameters.operation).toBe('aggregateItems');
});
it('should trim large objects', () => {
const largeObject: Record<string, string> = {};
for (let i = 0; i < 1000; i++) {
largeObject[`key_${i}`] = `value_${i}`.repeat(20);
}
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Function',
type: 'n8n-nodes-base.function',
parameters: {
functionCode: 'return items;',
data: largeObject,
},
}),
]);
const result = trimWorkflowJSON(workflow);
expect(result.nodes[0].parameters.data).toBe('[Large object omitted]');
expect(result.nodes[0].parameters.functionCode).toBe('return items;');
});
it('should handle null and undefined parameters correctly', () => {
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Test Node',
type: 'n8n-nodes-base.test',
parameters: {
nullValue: null,
// eslint-disable-next-line no-undefined
undefinedValue: undefined,
validValue: 'test',
},
}),
]);
const result = trimWorkflowJSON(workflow);
expect(result.nodes[0].parameters.nullValue).toBeNull();
expect(result.nodes[0].parameters.undefinedValue).toBeUndefined();
expect(result.nodes[0].parameters.validValue).toBe('test');
});
});
describe('Progressive trimming', () => {
it('should apply progressively more aggressive trimming for very large workflows', () => {
// Create a workflow with parameters at different size levels
const medium = 'x'.repeat(3000); // Will be kept at 10000 and 5000 threshold, trimmed at 2000
const large = 'y'.repeat(7000); // Will be kept at 10000 threshold, trimmed at 5000
const veryLarge = 'z'.repeat(12000); // Will be trimmed at 10000 threshold
const nodes: INode[] = [];
// Add many nodes to create a large workflow
for (let i = 0; i < 20; i++) {
nodes.push(
createNode({
id: `node${i}`,
name: `Node ${i}`,
type: 'n8n-nodes-base.code',
parameters: {
medium,
large,
veryLarge,
},
}),
);
}
const workflow: SimpleWorkflow = createWorkflow(nodes);
const result = trimWorkflowJSON(workflow);
// All veryLarge and large parameters should be trimmed
result.nodes.forEach((node) => {
expect(node.parameters?.veryLarge).toBe('[Large value omitted]');
expect(node.parameters?.large).toBe('[Large value omitted]');
expect(node.parameters?.medium).toBe(medium);
});
});
});
describe('Edge cases', () => {
it('should preserve connections', () => {
const workflow: SimpleWorkflow = {
name: 'Connected Workflow',
nodes: [
createNode({ id: 'node1', name: 'Start' }),
createNode({ id: 'node2', name: 'End' }),
],
connections: {
node1: {
main: [[{ node: 'node2', type: 'main', index: 0 }]],
},
},
};
const result = trimWorkflowJSON(workflow);
// Connections should be preserved
expect(result.connections).toEqual(workflow.connections);
});
it('should handle workflows with mixed parameter types', () => {
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Mixed Node',
type: 'n8n-nodes-base.mixed',
parameters: {
stringValue: 'normal string',
numberValue: 42,
booleanValue: true,
largeString: 'x'.repeat(15000),
array: ['item1', 'item2'],
objectValue: { key: 'value' },
largeArray: new Array(5000).fill('item'),
},
}),
]);
const result = trimWorkflowJSON(workflow);
// Normal parameters should be preserved
expect(result.nodes[0].parameters.stringValue).toBe('normal string');
expect(result.nodes[0].parameters.numberValue).toBe(42);
expect(result.nodes[0].parameters.booleanValue).toBe(true);
expect(result.nodes[0].parameters.array).toEqual(['item1', 'item2']);
expect(result.nodes[0].parameters.objectValue).toEqual({ key: 'value' });
// Large parameters should be trimmed
expect(result.nodes[0].parameters.largeString).toBe('[Large value omitted]');
expect(result.nodes[0].parameters.largeArray).toBe('[Large array omitted]');
});
it('should handle deeply nested parameters', () => {
const workflow: SimpleWorkflow = createWorkflow([
createNode({
id: 'node1',
name: 'Nested Node',
type: 'n8n-nodes-base.nested',
parameters: {
level1: {
level2: {
level3: {
largeData: 'x'.repeat(15000),
},
},
},
},
}),
]);
const result = trimWorkflowJSON(workflow);
// The entire nested object gets evaluated as a whole
// If the stringified nested object is too large, it gets replaced
expect(result.nodes[0].parameters.level1).toBe('[Large object omitted]');
});
it('should return most aggressively trimmed version if nothing fits', () => {
// Create an enormous workflow that won't fit even with aggressive trimming
const nodes: INode[] = [];
for (let i = 0; i < 1000; i++) {
nodes.push(
createNode({
id: `node${i}`,
name: `Node ${i}`,
type: 'n8n-nodes-base.code',
parameters: {
data1: 'x'.repeat(2000),
data2: 'y'.repeat(2000),
data3: 'z'.repeat(2000),
},
}),
);
}
const workflow: SimpleWorkflow = createWorkflow(nodes);
const result = trimWorkflowJSON(workflow);
// All large parameters should be trimmed with the most aggressive threshold (1000)
result.nodes.forEach((node) => {
expect(node.parameters?.data1).toBe('[Large value omitted]');
expect(node.parameters?.data2).toBe('[Large value omitted]');
expect(node.parameters?.data3).toBe('[Large value omitted]');
});
});
});
});

View File

@@ -1,5 +1,8 @@
import type { BaseMessage } from '@langchain/core/messages';
import { AIMessage } from '@langchain/core/messages';
import { AVG_CHARS_PER_TOKEN_ANTHROPIC } from '@/constants';
export type AIMessageWithUsageMetadata = AIMessage & {
response_metadata: {
usage: {
@@ -34,3 +37,32 @@ export function extractLastTokenUsage(messages: unknown[]): TokenUsage | undefin
return lastAiAssistantMessage.response_metadata.usage;
}
function concatenateMessageContent(messages: BaseMessage[]): string {
return messages.reduce((acc: string, message) => {
if (typeof message.content === 'string') {
return acc + message.content;
} else if (Array.isArray(message.content)) {
return (
acc +
message.content.reduce((innerAcc: string, item) => {
if (typeof item === 'object' && item !== null && 'text' in item) {
return innerAcc + item.text;
}
return innerAcc;
}, '')
);
}
return acc;
}, '');
}
export function estimateTokenCountFromString(text: string): number {
return Math.ceil(text.length / AVG_CHARS_PER_TOKEN_ANTHROPIC); // Rough estimate
}
export function estimateTokenCountFromMessages(messages: BaseMessage[]): number {
const entireInput = concatenateMessageContent(messages);
return estimateTokenCountFromString(entireInput);
}

View File

@@ -0,0 +1,104 @@
import type { INodeParameters, NodeParameterValueType } from 'n8n-workflow';
import { MAX_WORKFLOW_LENGTH_TOKENS } from '@/constants';
import type { SimpleWorkflow } from '@/types';
import { estimateTokenCountFromString } from '@/utils/token-usage';
/**
* Thresholds for progressively trimming large parameter values.
* Each iteration uses a more aggressive threshold if the workflow is still too large.
*/
const MAX_PARAMETER_VALUE_LENGTH_THRESHOLDS = [10000, 5000, 2000, 1000];
/**
* Trims a parameter value if it exceeds the specified threshold.
* Replaces large values with placeholders to reduce token usage.
*
* @param value - The parameter value to potentially trim
* @param threshold - The maximum allowed length in characters
* @returns The original value if under threshold, or a placeholder string if too large
*/
function trimParameterValue(
value: NodeParameterValueType,
threshold: number,
): NodeParameterValueType {
// Handle undefined and null values directly without stringification
if (value === undefined || value === null) {
return value;
}
const valueStr = JSON.stringify(value);
if (valueStr.length > threshold) {
// Return type-specific placeholder messages
if (typeof value === 'string') {
return '[Large value omitted]';
} else if (Array.isArray(value)) {
return '[Large array omitted]';
} else if (typeof value === 'object' && value !== null) {
return '[Large object omitted]';
}
}
return value;
}
/**
* Simplifies a workflow by trimming large parameter values of its nodes based on the given threshold.
* Creates a copy of the workflow to avoid mutations.
*
* @param workflow - The workflow to simplify
* @param threshold - The maximum allowed length for parameter values
* @returns A new workflow object with trimmed parameters
*/
function trimWorkflowJsonWithThreshold(
workflow: SimpleWorkflow,
threshold: number,
): SimpleWorkflow {
const simplifiedWorkflow = { ...workflow };
if (simplifiedWorkflow.nodes) {
simplifiedWorkflow.nodes = simplifiedWorkflow.nodes.map((node) => {
const simplifiedNode = { ...node };
// Process each parameter and replace large values with placeholders
if (simplifiedNode.parameters) {
const simplifiedParameters: INodeParameters = {};
for (const [key, value] of Object.entries(simplifiedNode.parameters)) {
simplifiedParameters[key] = trimParameterValue(value, threshold);
}
simplifiedNode.parameters = simplifiedParameters;
}
return simplifiedNode;
});
}
return simplifiedWorkflow;
}
/**
* Trims workflow JSON to fit within token limits by progressively applying more aggressive trimming.
* Iterates through different thresholds until the workflow fits within MAX_WORKFLOW_LENGTH_TOKENS.
*
* @param workflow - The workflow to trim
* @returns A simplified workflow that fits within token limits, or the most aggressively trimmed version
*/
export function trimWorkflowJSON(workflow: SimpleWorkflow): SimpleWorkflow {
// Try progressively more aggressive trimming thresholds
for (const threshold of MAX_PARAMETER_VALUE_LENGTH_THRESHOLDS) {
const simplified = trimWorkflowJsonWithThreshold(workflow, threshold);
const workflowStr = JSON.stringify(simplified);
const estimatedTokens = estimateTokenCountFromString(workflowStr);
// If the workflow fits within the token limit, return it
if (estimatedTokens <= MAX_WORKFLOW_LENGTH_TOKENS) {
return simplified;
}
}
// If even the most aggressive trimming doesn't fit, return the most trimmed version
// This ensures we always return something, even if it still exceeds the limit
return trimWorkflowJsonWithThreshold(
workflow,
MAX_PARAMETER_VALUE_LENGTH_THRESHOLDS[MAX_PARAMETER_VALUE_LENGTH_THRESHOLDS.length - 1],
);
}

View File

@@ -13,10 +13,17 @@ import {
type NodeExecutionSchema,
} from 'n8n-workflow';
import {
DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS,
MAX_AI_BUILDER_PROMPT_LENGTH,
MAX_INPUT_TOKENS,
} from '@/constants';
import { createGetNodeParameterTool } from '@/tools/get-node-parameter.tool';
import { trimWorkflowJSON } from '@/utils/trim-workflow-context';
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 { LLMServiceError, ValidationError, WorkflowStateError } from './errors';
import { createAddNodeTool } from './tools/add-node.tool';
import { createConnectNodesTool } from './tools/connect-nodes.tool';
import { createNodeDetailsTool } from './tools/node-details.tool';
@@ -27,7 +34,7 @@ import { createUpdateNodeParametersTool } from './tools/update-node-parameters.t
import type { SimpleWorkflow } from './types/workflow';
import { processOperations } from './utils/operations-processor';
import { createStreamProcessor, formatMessages, type BuilderTool } from './utils/stream-processor';
import { extractLastTokenUsage } from './utils/token-usage';
import { estimateTokenCountFromMessages, extractLastTokenUsage } from './utils/token-usage';
import { executeToolsInParallel } from './utils/tool-executor';
import { WorkflowState } from './workflow-state';
@@ -86,6 +93,7 @@ export class WorkflowBuilderAgent {
this.logger,
this.instanceUrl,
),
createGetNodeParameterTool(),
];
}
@@ -110,10 +118,20 @@ export class WorkflowBuilderAgent {
const prompt = await mainAgentPrompt.invoke({
...state,
workflowJSON: trimWorkflowJSON(state.workflowJSON),
executionData: state.workflowContext?.executionData ?? {},
executionSchema: state.workflowContext?.executionSchema ?? [],
instanceUrl: this.instanceUrl,
});
const estimatedTokens = estimateTokenCountFromMessages(prompt.messages);
if (estimatedTokens > MAX_INPUT_TOKENS) {
throw new WorkflowStateError(
'The current conversation and workflow state is too large to process. Try to simplify your workflow by breaking it into smaller parts.',
);
}
const response = await this.llmSimpleTask.bindTools(tools).invoke(prompt);
return { messages: [response] };