Skip to content

Commit 794a8d6

Browse files
authored
feat(ai-builder): Planning mode (#25498)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
1 parent 0b56ad5 commit 794a8d6

66 files changed

Lines changed: 7222 additions & 475 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/@n8n/ai-workflow-builder.ee/evaluations/__tests__/feedback.test.ts

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { langsmithMetricKey } from '../harness/feedback';
1+
import { langsmithMetricKey, toLangsmithEvaluationResult } from '../harness/feedback';
22
import type { Feedback } from '../harness/harness-types';
33

44
describe('langsmithMetricKey()', () => {
@@ -41,20 +41,20 @@ describe('langsmithMetricKey()', () => {
4141
it('should prefix metrics evaluator with evaluator name', () => {
4242
const discoveryLatency: Feedback = {
4343
evaluator: 'metrics',
44-
metric: 'discovery_latency_ms',
45-
score: 500,
44+
metric: 'discovery_latency_s',
45+
score: 0.5,
4646
kind: 'metric',
4747
};
4848
const builderLatency: Feedback = {
4949
evaluator: 'metrics',
50-
metric: 'builder_latency_ms',
51-
score: 1500,
50+
metric: 'builder_latency_s',
51+
score: 1.5,
5252
kind: 'metric',
5353
};
5454
const responderLatency: Feedback = {
5555
evaluator: 'metrics',
56-
metric: 'responder_latency_ms',
57-
score: 200,
56+
metric: 'responder_latency_s',
57+
score: 0.2,
5858
kind: 'metric',
5959
};
6060
const nodeCount: Feedback = {
@@ -64,9 +64,9 @@ describe('langsmithMetricKey()', () => {
6464
kind: 'metric',
6565
};
6666

67-
expect(langsmithMetricKey(discoveryLatency)).toBe('metrics.discovery_latency_ms');
68-
expect(langsmithMetricKey(builderLatency)).toBe('metrics.builder_latency_ms');
69-
expect(langsmithMetricKey(responderLatency)).toBe('metrics.responder_latency_ms');
67+
expect(langsmithMetricKey(discoveryLatency)).toBe('metrics.discovery_latency_s');
68+
expect(langsmithMetricKey(builderLatency)).toBe('metrics.builder_latency_s');
69+
expect(langsmithMetricKey(responderLatency)).toBe('metrics.responder_latency_s');
7070
expect(langsmithMetricKey(nodeCount)).toBe('metrics.node_count');
7171
});
7272

@@ -79,11 +79,74 @@ describe('langsmithMetricKey()', () => {
7979
{ evaluator: 'pairwise', metric: 'pairwise_primary', score: 1, kind: 'score' },
8080
{ evaluator: 'pairwise', metric: 'pairwise_total_violations', score: 1, kind: 'detail' },
8181
{ evaluator: 'pairwise', metric: 'judge1', score: 0, kind: 'detail' },
82-
{ evaluator: 'metrics', metric: 'discovery_latency_ms', score: 500, kind: 'metric' },
82+
{ evaluator: 'metrics', metric: 'discovery_latency_s', score: 0.5, kind: 'metric' },
8383
{ evaluator: 'metrics', metric: 'node_count', score: 5, kind: 'metric' },
8484
];
8585

8686
const keys = feedback.map(langsmithMetricKey);
8787
expect(new Set(keys).size).toBe(keys.length);
8888
});
8989
});
90+
91+
describe('toLangsmithEvaluationResult()', () => {
92+
it('should clamp scores exceeding LangSmith max limit (safety net)', () => {
93+
const fb: Feedback = {
94+
evaluator: 'metrics',
95+
metric: 'some_metric',
96+
score: 150000, // Exceeds 99999.9999
97+
kind: 'metric',
98+
};
99+
100+
const result = toLangsmithEvaluationResult(fb);
101+
expect(result.score).toBe(99999.9999);
102+
});
103+
104+
it('should clamp scores below LangSmith min limit (safety net)', () => {
105+
const fb: Feedback = {
106+
evaluator: 'metrics',
107+
metric: 'some_metric',
108+
score: -200000,
109+
kind: 'metric',
110+
};
111+
112+
const result = toLangsmithEvaluationResult(fb);
113+
expect(result.score).toBe(-99999.9999);
114+
});
115+
116+
it('should preserve scores within valid range', () => {
117+
const fb: Feedback = {
118+
evaluator: 'metrics',
119+
metric: 'discovery_latency_s',
120+
score: 161.288, // 161 seconds, well within limits
121+
kind: 'metric',
122+
};
123+
124+
const result = toLangsmithEvaluationResult(fb);
125+
expect(result.score).toBe(161.288);
126+
});
127+
128+
it('should include comment when present', () => {
129+
const fb: Feedback = {
130+
evaluator: 'llm-judge',
131+
metric: 'overallScore',
132+
score: 0.85,
133+
kind: 'score',
134+
comment: 'Good workflow',
135+
};
136+
137+
const result = toLangsmithEvaluationResult(fb);
138+
expect(result.comment).toBe('Good workflow');
139+
});
140+
141+
it('should not include comment when absent', () => {
142+
const fb: Feedback = {
143+
evaluator: 'llm-judge',
144+
metric: 'overallScore',
145+
score: 0.85,
146+
kind: 'score',
147+
};
148+
149+
const result = toLangsmithEvaluationResult(fb);
150+
expect(result.comment).toBeUndefined();
151+
});
152+
});

packages/@n8n/ai-workflow-builder.ee/evaluations/harness/evaluation-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function getChatPayload(options: GetChatPayloadOptions): ChatPayload {
8686
* Matches the CoordinationLogEntry type from src/types/coordination.ts
8787
*/
8888
interface CoordinationLogEntry {
89-
phase: 'discovery' | 'builder' | 'state_management' | 'responder';
89+
phase: 'discovery' | 'builder' | 'state_management' | 'responder' | 'planner';
9090
status: 'completed' | 'in_progress' | 'error';
9191
timestamp: number;
9292
}

packages/@n8n/ai-workflow-builder.ee/evaluations/harness/feedback.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function isPairwiseV1Metric(metric: string): boolean {
2121
* - Programmatic: keep evaluator prefix (e.g. `programmatic.trigger`)
2222
* - LLM-judge: keep metrics unprefixed (e.g. `overallScore`, `connections`, `maintainability.nodeNamingQuality`)
2323
* - Pairwise: keep v1 metrics unprefixed (e.g. `pairwise_primary`), but namespace non-v1 details.
24-
* - Metrics: keep evaluator prefix (e.g. `metrics.discovery_latency_ms`, `metrics.node_count`)
24+
* - Metrics: keep evaluator prefix (e.g. `metrics.discovery_latency_s`, `metrics.node_count`)
2525
*/
2626
export function langsmithMetricKey(feedback: Feedback): string {
2727
if (feedback.evaluator === 'pairwise') {
@@ -44,10 +44,24 @@ export function langsmithMetricKey(feedback: Feedback): string {
4444
return feedbackKey(feedback);
4545
}
4646

47+
/**
48+
* LangSmith score limits.
49+
*/
50+
const LANGSMITH_SCORE_MIN = -99999.9999;
51+
const LANGSMITH_SCORE_MAX = 99999.9999;
52+
53+
/**
54+
* Clamp a score to LangSmith's valid range.
55+
* LangSmith rejects scores outside [-99999.9999, 99999.9999].
56+
*/
57+
function clampScoreForLangsmith(score: number): number {
58+
return Math.max(LANGSMITH_SCORE_MIN, Math.min(LANGSMITH_SCORE_MAX, score));
59+
}
60+
4761
export function toLangsmithEvaluationResult(feedback: Feedback): LangsmithEvaluationResultLike {
4862
return {
4963
key: langsmithMetricKey(feedback),
50-
score: feedback.score,
64+
score: clampScoreForLangsmith(feedback.score),
5165
...(feedback.comment ? { comment: feedback.comment } : {}),
5266
};
5367
}

packages/@n8n/ai-workflow-builder.ee/evaluations/harness/runner.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,21 @@ function hasErrorFeedback(feedback: Feedback[]): boolean {
117117
return feedback.some((f) => f.metric === 'error');
118118
}
119119

120+
/**
121+
* Convert milliseconds to seconds for LangSmith metrics.
122+
* LangSmith scores must be within [-99999.9999, 99999.9999], so we store
123+
* latencies in seconds (supporting up to ~27 hours) instead of milliseconds.
124+
*/
125+
function msToSeconds(ms: number): number {
126+
return ms / 1000;
127+
}
128+
120129
/**
121130
* Create feedback items for subgraph metrics.
122131
* These are reported to LangSmith as 'metrics' evaluator feedback.
132+
*
133+
* Note: Latencies are converted from milliseconds to seconds to stay within
134+
* LangSmith's score limits while preserving precision for long-running operations.
123135
*/
124136
function createMetricsFeedback(args: {
125137
discoveryDurationMs?: number;
@@ -132,26 +144,26 @@ function createMetricsFeedback(args: {
132144
if (args.discoveryDurationMs !== undefined) {
133145
feedback.push({
134146
evaluator: 'metrics',
135-
metric: 'discovery_latency_ms',
136-
score: args.discoveryDurationMs,
147+
metric: 'discovery_latency_s',
148+
score: msToSeconds(args.discoveryDurationMs),
137149
kind: 'metric',
138150
});
139151
}
140152

141153
if (args.builderDurationMs !== undefined) {
142154
feedback.push({
143155
evaluator: 'metrics',
144-
metric: 'builder_latency_ms',
145-
score: args.builderDurationMs,
156+
metric: 'builder_latency_s',
157+
score: msToSeconds(args.builderDurationMs),
146158
kind: 'metric',
147159
});
148160
}
149161

150162
if (args.responderDurationMs !== undefined) {
151163
feedback.push({
152164
evaluator: 'metrics',
153-
metric: 'responder_latency_ms',
154-
score: args.responderDurationMs,
165+
metric: 'responder_latency_s',
166+
score: msToSeconds(args.responderDurationMs),
155167
kind: 'metric',
156168
});
157169
}

packages/@n8n/ai-workflow-builder.ee/evaluations/support/environment.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface StageModels {
4040
builder?: ModelId;
4141
/** Model for parameter updater (within builder) */
4242
parameterUpdater?: ModelId;
43+
/** Model for planner stage (plan mode) */
44+
planner?: ModelId;
4345
/** Model for LLM judge evaluation */
4446
judge?: ModelId;
4547
}
@@ -55,6 +57,7 @@ export interface ResolvedStageLLMs {
5557
discovery: BaseChatModel;
5658
builder: BaseChatModel;
5759
parameterUpdater: BaseChatModel;
60+
planner: BaseChatModel;
5861
judge: BaseChatModel;
5962
}
6063

@@ -106,6 +109,7 @@ export async function resolveStageModels(stageModels: StageModels): Promise<Reso
106109
parameterUpdater: stageModels.parameterUpdater
107110
? await setupLLM(stageModels.parameterUpdater)
108111
: builderLLM,
112+
planner: stageModels.planner ? await setupLLM(stageModels.planner) : defaultLLM,
109113
judge: stageModels.judge ? await setupLLM(stageModels.judge) : defaultLLM,
110114
};
111115
}
@@ -221,6 +225,7 @@ export function createAgent(options: CreateAgentOptions): WorkflowBuilderAgent {
221225
discovery: llms.discovery,
222226
builder: llms.builder,
223227
parameterUpdater: llms.parameterUpdater,
228+
planner: llms.planner,
224229
},
225230
checkpointer: new MemorySaver(),
226231
tracer,

packages/@n8n/ai-workflow-builder.ee/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@langchain/core": "catalog:",
5757
"@langchain/langgraph": "1.0.2",
5858
"@langchain/openai": "catalog:",
59+
"langchain": "catalog:",
5960
"@n8n/backend-common": "workspace:*",
6061
"@n8n/config": "workspace:*",
6162
"@n8n/di": "workspace:*",

0 commit comments

Comments
 (0)