@wundr.io/langgraph-orchestrator 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +842 -0
- package/dist/checkpointing.d.ts +265 -0
- package/dist/checkpointing.d.ts.map +1 -0
- package/dist/checkpointing.js +577 -0
- package/dist/checkpointing.js.map +1 -0
- package/dist/edges/conditional-edge.d.ts +230 -0
- package/dist/edges/conditional-edge.d.ts.map +1 -0
- package/dist/edges/conditional-edge.js +439 -0
- package/dist/edges/conditional-edge.js.map +1 -0
- package/dist/edges/loop-edge.d.ts +290 -0
- package/dist/edges/loop-edge.d.ts.map +1 -0
- package/dist/edges/loop-edge.js +503 -0
- package/dist/edges/loop-edge.js.map +1 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +269 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/decision-node.d.ts +276 -0
- package/dist/nodes/decision-node.d.ts.map +1 -0
- package/dist/nodes/decision-node.js +403 -0
- package/dist/nodes/decision-node.js.map +1 -0
- package/dist/nodes/human-node.d.ts +272 -0
- package/dist/nodes/human-node.d.ts.map +1 -0
- package/dist/nodes/human-node.js +394 -0
- package/dist/nodes/human-node.js.map +1 -0
- package/dist/nodes/llm-node.d.ts +173 -0
- package/dist/nodes/llm-node.d.ts.map +1 -0
- package/dist/nodes/llm-node.js +325 -0
- package/dist/nodes/llm-node.js.map +1 -0
- package/dist/nodes/tool-node.d.ts +151 -0
- package/dist/nodes/tool-node.d.ts.map +1 -0
- package/dist/nodes/tool-node.js +373 -0
- package/dist/nodes/tool-node.js.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts +149 -0
- package/dist/prebuilt-graphs/plan-execute-refine.d.ts.map +1 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js +600 -0
- package/dist/prebuilt-graphs/plan-execute-refine.js.map +1 -0
- package/dist/state-graph.d.ts +158 -0
- package/dist/state-graph.d.ts.map +1 -0
- package/dist/state-graph.js +756 -0
- package/dist/state-graph.js.map +1 -0
- package/dist/types.d.ts +762 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +73 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/checkpointing.ts +702 -0
- package/src/edges/conditional-edge.ts +518 -0
- package/src/edges/loop-edge.ts +623 -0
- package/src/index.ts +416 -0
- package/src/nodes/decision-node.ts +538 -0
- package/src/nodes/human-node.ts +572 -0
- package/src/nodes/llm-node.ts +448 -0
- package/src/nodes/tool-node.ts +525 -0
- package/src/prebuilt-graphs/plan-execute-refine.ts +769 -0
- package/src/state-graph.ts +990 -0
- package/src/types.ts +729 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-Execute-Refine Graph - Ready-to-use workflow pattern
|
|
3
|
+
* @module @wundr.io/langgraph-orchestrator
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
import { createDecisionNode, createIfElseNode } from '../nodes/decision-node';
|
|
10
|
+
import { createHumanNode } from '../nodes/human-node';
|
|
11
|
+
import { createLLMNode } from '../nodes/llm-node';
|
|
12
|
+
import { createToolRegistry } from '../nodes/tool-node';
|
|
13
|
+
import { StateGraph } from '../state-graph';
|
|
14
|
+
|
|
15
|
+
import type { HumanInputHandler } from '../nodes/human-node';
|
|
16
|
+
import type {
|
|
17
|
+
AgentState,
|
|
18
|
+
Tool,
|
|
19
|
+
LLMProvider,
|
|
20
|
+
NodeResult,
|
|
21
|
+
NodeContext,
|
|
22
|
+
} from '../types';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extended state for plan-execute-refine workflow
|
|
26
|
+
*/
|
|
27
|
+
export interface PlanExecuteState extends AgentState {
|
|
28
|
+
/** The current plan */
|
|
29
|
+
readonly data: AgentState['data'] & {
|
|
30
|
+
/** Original task/goal */
|
|
31
|
+
task?: string;
|
|
32
|
+
/** Generated plan steps */
|
|
33
|
+
plan?: PlanStep[];
|
|
34
|
+
/** Current step being executed */
|
|
35
|
+
currentStepIndex?: number;
|
|
36
|
+
/** Execution results for each step */
|
|
37
|
+
stepResults?: StepResult[];
|
|
38
|
+
/** Overall execution status */
|
|
39
|
+
executionStatus?:
|
|
40
|
+
| 'planning'
|
|
41
|
+
| 'executing'
|
|
42
|
+
| 'refining'
|
|
43
|
+
| 'complete'
|
|
44
|
+
| 'failed';
|
|
45
|
+
/** Number of refinement iterations */
|
|
46
|
+
refinementCount?: number;
|
|
47
|
+
/** Maximum refinement iterations */
|
|
48
|
+
maxRefinements?: number;
|
|
49
|
+
/** Final result */
|
|
50
|
+
finalResult?: unknown;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A step in the plan
|
|
56
|
+
*/
|
|
57
|
+
export interface PlanStep {
|
|
58
|
+
/** Step identifier */
|
|
59
|
+
id: string;
|
|
60
|
+
/** Step description */
|
|
61
|
+
description: string;
|
|
62
|
+
/** Tool to use (if any) */
|
|
63
|
+
tool?: string;
|
|
64
|
+
/** Tool arguments */
|
|
65
|
+
toolArgs?: Record<string, unknown>;
|
|
66
|
+
/** Dependencies on other steps */
|
|
67
|
+
dependsOn?: string[];
|
|
68
|
+
/** Expected output description */
|
|
69
|
+
expectedOutput?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Result of executing a step
|
|
74
|
+
*/
|
|
75
|
+
export interface StepResult {
|
|
76
|
+
/** Step ID */
|
|
77
|
+
stepId: string;
|
|
78
|
+
/** Whether step succeeded */
|
|
79
|
+
success: boolean;
|
|
80
|
+
/** Result data */
|
|
81
|
+
result?: unknown;
|
|
82
|
+
/** Error message if failed */
|
|
83
|
+
error?: string;
|
|
84
|
+
/** Execution timestamp */
|
|
85
|
+
timestamp: Date;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Configuration for plan-execute-refine graph
|
|
90
|
+
*/
|
|
91
|
+
export interface PlanExecuteRefineConfig {
|
|
92
|
+
/** LLM provider for planning and refinement */
|
|
93
|
+
llmProvider: LLMProvider;
|
|
94
|
+
/** Available tools */
|
|
95
|
+
tools?: Tool[];
|
|
96
|
+
/** Human input handler for approval steps */
|
|
97
|
+
humanHandler?: HumanInputHandler;
|
|
98
|
+
/** Maximum refinement iterations */
|
|
99
|
+
maxRefinements?: number;
|
|
100
|
+
/** Whether to require human approval before execution */
|
|
101
|
+
requireApproval?: boolean;
|
|
102
|
+
/** Custom planner prompt */
|
|
103
|
+
plannerPrompt?: string;
|
|
104
|
+
/** Custom executor prompt */
|
|
105
|
+
executorPrompt?: string;
|
|
106
|
+
/** Custom refiner prompt */
|
|
107
|
+
refinerPrompt?: string;
|
|
108
|
+
/** Model to use */
|
|
109
|
+
model?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Schema for plan validation
|
|
114
|
+
*/
|
|
115
|
+
export const PlanSchema = z.array(
|
|
116
|
+
z.object({
|
|
117
|
+
id: z.string(),
|
|
118
|
+
description: z.string(),
|
|
119
|
+
tool: z.string().optional(),
|
|
120
|
+
toolArgs: z.record(z.unknown()).optional(),
|
|
121
|
+
dependsOn: z.array(z.string()).optional(),
|
|
122
|
+
expectedOutput: z.string().optional(),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create a plan-execute-refine workflow graph
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const graph = createPlanExecuteRefineGraph({
|
|
132
|
+
* llmProvider: myLLMProvider,
|
|
133
|
+
* tools: [searchTool, calculatorTool],
|
|
134
|
+
* maxRefinements: 3,
|
|
135
|
+
* requireApproval: true,
|
|
136
|
+
* humanHandler: myHumanHandler
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* const result = await graph.execute({
|
|
140
|
+
* initialState: {
|
|
141
|
+
* data: { task: 'Research and summarize the latest AI developments' }
|
|
142
|
+
* }
|
|
143
|
+
* });
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @param config - Graph configuration
|
|
147
|
+
* @returns Configured StateGraph
|
|
148
|
+
*/
|
|
149
|
+
export function createPlanExecuteRefineGraph(
|
|
150
|
+
config: PlanExecuteRefineConfig,
|
|
151
|
+
): StateGraph<PlanExecuteState> {
|
|
152
|
+
const graph = new StateGraph<PlanExecuteState>('plan-execute-refine');
|
|
153
|
+
|
|
154
|
+
// Set up services
|
|
155
|
+
const toolRegistry = createToolRegistry();
|
|
156
|
+
config.tools?.forEach(tool => toolRegistry.register(tool));
|
|
157
|
+
|
|
158
|
+
graph.setServices({
|
|
159
|
+
llmProvider: config.llmProvider,
|
|
160
|
+
toolRegistry,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// =========================================================================
|
|
164
|
+
// Node: Planner - Creates the initial plan
|
|
165
|
+
// =========================================================================
|
|
166
|
+
const plannerNode = createLLMNode<PlanExecuteState>({
|
|
167
|
+
id: 'planner',
|
|
168
|
+
name: 'Planner',
|
|
169
|
+
config: {
|
|
170
|
+
model: config.model,
|
|
171
|
+
systemPrompt: config.plannerPrompt ?? DEFAULT_PLANNER_PROMPT,
|
|
172
|
+
promptTemplate: state => {
|
|
173
|
+
const task = state.data.task ?? 'No task specified';
|
|
174
|
+
const tools =
|
|
175
|
+
config.tools?.map(t => `- ${t.name}: ${t.description}`).join('\n') ??
|
|
176
|
+
'No tools available';
|
|
177
|
+
return `Task: ${task}\n\nAvailable Tools:\n${tools}\n\nCreate a detailed plan to accomplish this task.`;
|
|
178
|
+
},
|
|
179
|
+
postProcess: (response, state) => {
|
|
180
|
+
// Parse plan from response
|
|
181
|
+
const plan = extractPlanFromResponse(response.message.content);
|
|
182
|
+
return {
|
|
183
|
+
data: {
|
|
184
|
+
...state.data,
|
|
185
|
+
plan,
|
|
186
|
+
currentStepIndex: 0,
|
|
187
|
+
stepResults: [],
|
|
188
|
+
executionStatus: 'planning' as const,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// =========================================================================
|
|
196
|
+
// Node: Plan Validator - Validates the generated plan
|
|
197
|
+
// =========================================================================
|
|
198
|
+
const validatorNode = {
|
|
199
|
+
id: 'validator',
|
|
200
|
+
name: 'Plan Validator',
|
|
201
|
+
type: 'transform' as const,
|
|
202
|
+
config: {},
|
|
203
|
+
execute: async (
|
|
204
|
+
state: PlanExecuteState,
|
|
205
|
+
context: NodeContext,
|
|
206
|
+
): Promise<NodeResult<PlanExecuteState>> => {
|
|
207
|
+
const plan = state.data.plan;
|
|
208
|
+
|
|
209
|
+
if (!plan || plan.length === 0) {
|
|
210
|
+
context.services.logger.warn('Empty or invalid plan generated');
|
|
211
|
+
return {
|
|
212
|
+
state: {
|
|
213
|
+
...state,
|
|
214
|
+
data: {
|
|
215
|
+
...state.data,
|
|
216
|
+
executionStatus: 'failed' as const,
|
|
217
|
+
},
|
|
218
|
+
error: {
|
|
219
|
+
code: 'INVALID_PLAN',
|
|
220
|
+
message: 'Failed to generate a valid plan',
|
|
221
|
+
recoverable: true,
|
|
222
|
+
},
|
|
223
|
+
} as PlanExecuteState,
|
|
224
|
+
next: 'refiner',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Validate plan structure
|
|
229
|
+
try {
|
|
230
|
+
PlanSchema.parse(plan);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
context.services.logger.error('Plan validation failed', { error });
|
|
233
|
+
return {
|
|
234
|
+
state: {
|
|
235
|
+
...state,
|
|
236
|
+
data: {
|
|
237
|
+
...state.data,
|
|
238
|
+
executionStatus: 'failed' as const,
|
|
239
|
+
},
|
|
240
|
+
} as PlanExecuteState,
|
|
241
|
+
next: 'refiner',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Validate tool references
|
|
246
|
+
for (const step of plan) {
|
|
247
|
+
if (step.tool && !toolRegistry.get(step.tool)) {
|
|
248
|
+
context.services.logger.warn(`Unknown tool referenced: ${step.tool}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
context.services.logger.info(`Plan validated with ${plan.length} steps`);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
state: {
|
|
256
|
+
...state,
|
|
257
|
+
data: {
|
|
258
|
+
...state.data,
|
|
259
|
+
executionStatus: 'executing' as const,
|
|
260
|
+
},
|
|
261
|
+
} as PlanExecuteState,
|
|
262
|
+
next: config.requireApproval ? 'approval' : 'executor',
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// =========================================================================
|
|
268
|
+
// Node: Approval - Human approval before execution (optional)
|
|
269
|
+
// =========================================================================
|
|
270
|
+
let approvalNode;
|
|
271
|
+
if (config.humanHandler) {
|
|
272
|
+
approvalNode = createHumanNode<PlanExecuteState>({
|
|
273
|
+
id: 'approval',
|
|
274
|
+
name: 'Plan Approval',
|
|
275
|
+
config: {
|
|
276
|
+
inputHandler: config.humanHandler,
|
|
277
|
+
prompt: state => {
|
|
278
|
+
const plan = (state.data['plan'] as PlanStep[] | undefined) ?? [];
|
|
279
|
+
const planText = plan
|
|
280
|
+
.map((s: PlanStep, i: number) => `${i + 1}. ${s.description}`)
|
|
281
|
+
.join('\n');
|
|
282
|
+
return `Please review the following plan:\n\n${planText}\n\nDo you approve this plan?`;
|
|
283
|
+
},
|
|
284
|
+
choices: [
|
|
285
|
+
{
|
|
286
|
+
value: 'approve',
|
|
287
|
+
label: 'Approve',
|
|
288
|
+
description: 'Proceed with execution',
|
|
289
|
+
},
|
|
290
|
+
{ value: 'reject', label: 'Reject', description: 'Request new plan' },
|
|
291
|
+
{
|
|
292
|
+
value: 'modify',
|
|
293
|
+
label: 'Modify',
|
|
294
|
+
description: 'Provide feedback for refinement',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
processResponse: (response, _state) => {
|
|
298
|
+
if (response.value === 'reject') {
|
|
299
|
+
return { planRejected: true };
|
|
300
|
+
}
|
|
301
|
+
if (response.value === 'modify') {
|
|
302
|
+
return { planFeedback: response.metadata?.feedback };
|
|
303
|
+
}
|
|
304
|
+
return { planApproved: true };
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// =========================================================================
|
|
311
|
+
// Node: Approval Router - Routes based on approval decision
|
|
312
|
+
// =========================================================================
|
|
313
|
+
const approvalRouterNode = createDecisionNode<PlanExecuteState>({
|
|
314
|
+
id: 'approval-router',
|
|
315
|
+
name: 'Approval Router',
|
|
316
|
+
config: {
|
|
317
|
+
branches: [
|
|
318
|
+
{
|
|
319
|
+
name: 'approved',
|
|
320
|
+
target: 'executor',
|
|
321
|
+
condition: {
|
|
322
|
+
type: 'equals',
|
|
323
|
+
field: 'data.planApproved',
|
|
324
|
+
value: true,
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'rejected',
|
|
329
|
+
target: 'planner',
|
|
330
|
+
condition: {
|
|
331
|
+
type: 'equals',
|
|
332
|
+
field: 'data.planRejected',
|
|
333
|
+
value: true,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
defaultBranch: 'refiner',
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// =========================================================================
|
|
342
|
+
// Node: Executor - Executes plan steps
|
|
343
|
+
// =========================================================================
|
|
344
|
+
const executorNode = {
|
|
345
|
+
id: 'executor',
|
|
346
|
+
name: 'Step Executor',
|
|
347
|
+
type: 'transform' as const,
|
|
348
|
+
config: {},
|
|
349
|
+
execute: async (
|
|
350
|
+
state: PlanExecuteState,
|
|
351
|
+
context: NodeContext,
|
|
352
|
+
): Promise<NodeResult<PlanExecuteState>> => {
|
|
353
|
+
const plan = state.data.plan ?? [];
|
|
354
|
+
const currentIndex = state.data.currentStepIndex ?? 0;
|
|
355
|
+
const stepResults = state.data.stepResults ?? [];
|
|
356
|
+
|
|
357
|
+
if (currentIndex >= plan.length) {
|
|
358
|
+
context.services.logger.info('All steps executed');
|
|
359
|
+
return {
|
|
360
|
+
state: {
|
|
361
|
+
...state,
|
|
362
|
+
data: {
|
|
363
|
+
...state.data,
|
|
364
|
+
executionStatus: 'refining' as const,
|
|
365
|
+
},
|
|
366
|
+
} as PlanExecuteState,
|
|
367
|
+
next: 'evaluator',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const step = plan[currentIndex];
|
|
372
|
+
if (!step) {
|
|
373
|
+
return {
|
|
374
|
+
state: {
|
|
375
|
+
...state,
|
|
376
|
+
data: {
|
|
377
|
+
...state.data,
|
|
378
|
+
currentStepIndex: currentIndex + 1,
|
|
379
|
+
},
|
|
380
|
+
} as PlanExecuteState,
|
|
381
|
+
next: 'executor', // Continue to next step
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
context.services.logger.info(
|
|
386
|
+
`Executing step ${currentIndex + 1}: ${step.description}`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
let result: StepResult;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
// Check dependencies
|
|
393
|
+
if (step.dependsOn?.length) {
|
|
394
|
+
for (const depId of step.dependsOn) {
|
|
395
|
+
const depResult = stepResults.find(r => r.stepId === depId);
|
|
396
|
+
if (!depResult?.success) {
|
|
397
|
+
throw new Error(`Dependency ${depId} not satisfied`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Execute tool if specified
|
|
403
|
+
let stepOutput: unknown;
|
|
404
|
+
if (step.tool) {
|
|
405
|
+
const tool = toolRegistry.get(step.tool);
|
|
406
|
+
if (!tool) {
|
|
407
|
+
throw new Error(`Tool ${step.tool} not found`);
|
|
408
|
+
}
|
|
409
|
+
stepOutput = await tool.execute(step.toolArgs ?? {});
|
|
410
|
+
} else {
|
|
411
|
+
// For non-tool steps, use LLM
|
|
412
|
+
const llmResponse = await config.llmProvider.generate({
|
|
413
|
+
messages: [
|
|
414
|
+
...state.messages,
|
|
415
|
+
{
|
|
416
|
+
id: uuidv4(),
|
|
417
|
+
role: 'user',
|
|
418
|
+
content: `Execute step: ${step.description}`,
|
|
419
|
+
timestamp: new Date(),
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
model: config.model,
|
|
423
|
+
});
|
|
424
|
+
stepOutput = llmResponse.message.content;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
result = {
|
|
428
|
+
stepId: step.id,
|
|
429
|
+
success: true,
|
|
430
|
+
result: stepOutput,
|
|
431
|
+
timestamp: new Date(),
|
|
432
|
+
};
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
435
|
+
context.services.logger.error(`Step ${step.id} failed`, {
|
|
436
|
+
error: err.message,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
result = {
|
|
440
|
+
stepId: step.id,
|
|
441
|
+
success: false,
|
|
442
|
+
error: err.message,
|
|
443
|
+
timestamp: new Date(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
state: {
|
|
449
|
+
...state,
|
|
450
|
+
data: {
|
|
451
|
+
...state.data,
|
|
452
|
+
currentStepIndex: currentIndex + 1,
|
|
453
|
+
stepResults: [...stepResults, result],
|
|
454
|
+
},
|
|
455
|
+
} as PlanExecuteState,
|
|
456
|
+
next: 'executor', // Continue to next step
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// =========================================================================
|
|
462
|
+
// Node: Evaluator - Evaluates execution results
|
|
463
|
+
// =========================================================================
|
|
464
|
+
const evaluatorNode = createLLMNode<PlanExecuteState>({
|
|
465
|
+
id: 'evaluator',
|
|
466
|
+
name: 'Result Evaluator',
|
|
467
|
+
config: {
|
|
468
|
+
model: config.model,
|
|
469
|
+
systemPrompt: 'You are an expert at evaluating task execution results.',
|
|
470
|
+
promptTemplate: state => {
|
|
471
|
+
const task =
|
|
472
|
+
(state.data['task'] as string | undefined) ?? 'Unknown task';
|
|
473
|
+
const results =
|
|
474
|
+
(state.data['stepResults'] as StepResult[] | undefined) ?? [];
|
|
475
|
+
const resultsText = results
|
|
476
|
+
.map(
|
|
477
|
+
(r: StepResult) =>
|
|
478
|
+
`Step ${r.stepId}: ${r.success ? 'Success' : 'Failed'}\n${r.success ? JSON.stringify(r.result) : r.error}`,
|
|
479
|
+
)
|
|
480
|
+
.join('\n\n');
|
|
481
|
+
|
|
482
|
+
return `Original Task: ${task}\n\nExecution Results:\n${resultsText}\n\nEvaluate if the task was completed successfully. If not, identify what needs to be refined.`;
|
|
483
|
+
},
|
|
484
|
+
postProcess: (response, state) => {
|
|
485
|
+
const content = response.message.content.toLowerCase();
|
|
486
|
+
const isComplete =
|
|
487
|
+
content.includes('complete') || content.includes('success');
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
data: {
|
|
491
|
+
...state.data,
|
|
492
|
+
executionStatus: isComplete
|
|
493
|
+
? ('complete' as const)
|
|
494
|
+
: ('refining' as const),
|
|
495
|
+
evaluationResult: response.message.content,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
router: (response, _state) => {
|
|
500
|
+
const content = response.message.content.toLowerCase();
|
|
501
|
+
if (content.includes('complete') || content.includes('success')) {
|
|
502
|
+
return 'complete';
|
|
503
|
+
}
|
|
504
|
+
return 'refiner';
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// =========================================================================
|
|
510
|
+
// Node: Refiner - Refines the plan based on execution results
|
|
511
|
+
// =========================================================================
|
|
512
|
+
const refinerNode = createLLMNode<PlanExecuteState>({
|
|
513
|
+
id: 'refiner',
|
|
514
|
+
name: 'Plan Refiner',
|
|
515
|
+
config: {
|
|
516
|
+
model: config.model,
|
|
517
|
+
systemPrompt: config.refinerPrompt ?? DEFAULT_REFINER_PROMPT,
|
|
518
|
+
promptTemplate: state => {
|
|
519
|
+
const task = state.data.task ?? 'Unknown task';
|
|
520
|
+
const plan = (state.data['plan'] as PlanStep[] | undefined) ?? [];
|
|
521
|
+
const results =
|
|
522
|
+
(state.data['stepResults'] as StepResult[] | undefined) ?? [];
|
|
523
|
+
const evaluation =
|
|
524
|
+
(state.data['evaluationResult'] as string | undefined) ?? '';
|
|
525
|
+
const feedback =
|
|
526
|
+
(state.data['planFeedback'] as string | undefined) ?? '';
|
|
527
|
+
|
|
528
|
+
return `Original Task: ${task}
|
|
529
|
+
|
|
530
|
+
Previous Plan:
|
|
531
|
+
${plan.map((s: PlanStep, i: number) => `${i + 1}. ${s.description}`).join('\n')}
|
|
532
|
+
|
|
533
|
+
Execution Results:
|
|
534
|
+
${results.map((r: StepResult) => `${r.stepId}: ${r.success ? 'Success' : 'Failed - ' + r.error}`).join('\n')}
|
|
535
|
+
|
|
536
|
+
Evaluation: ${evaluation}
|
|
537
|
+
${feedback ? `Human Feedback: ${feedback}` : ''}
|
|
538
|
+
|
|
539
|
+
Please refine the plan to address any issues and ensure task completion.`;
|
|
540
|
+
},
|
|
541
|
+
postProcess: (response, state) => {
|
|
542
|
+
const refinedPlan = extractPlanFromResponse(response.message.content);
|
|
543
|
+
const refinementCount =
|
|
544
|
+
((state.data['refinementCount'] as number | undefined) ?? 0) + 1;
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
data: {
|
|
548
|
+
...state.data,
|
|
549
|
+
plan: refinedPlan,
|
|
550
|
+
currentStepIndex: 0,
|
|
551
|
+
stepResults: [],
|
|
552
|
+
refinementCount,
|
|
553
|
+
executionStatus: 'planning' as const,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// =========================================================================
|
|
561
|
+
// Node: Refinement Check - Checks if we've exceeded max refinements
|
|
562
|
+
// =========================================================================
|
|
563
|
+
const refinementCheckNode = createIfElseNode<PlanExecuteState>({
|
|
564
|
+
id: 'refinement-check',
|
|
565
|
+
name: 'Refinement Check',
|
|
566
|
+
condition: {
|
|
567
|
+
type: 'custom',
|
|
568
|
+
evaluate: async (state: PlanExecuteState) => {
|
|
569
|
+
const count = state.data.refinementCount ?? 0;
|
|
570
|
+
const max = state.data.maxRefinements ?? config.maxRefinements ?? 3;
|
|
571
|
+
return count < max;
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
ifTrue: 'validator',
|
|
575
|
+
ifFalse: 'failed',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// =========================================================================
|
|
579
|
+
// Node: Complete - Final success node
|
|
580
|
+
// =========================================================================
|
|
581
|
+
const completeNode = {
|
|
582
|
+
id: 'complete',
|
|
583
|
+
name: 'Complete',
|
|
584
|
+
type: 'end' as const,
|
|
585
|
+
config: {},
|
|
586
|
+
execute: async (
|
|
587
|
+
state: PlanExecuteState,
|
|
588
|
+
context: NodeContext,
|
|
589
|
+
): Promise<NodeResult<PlanExecuteState>> => {
|
|
590
|
+
context.services.logger.info('Workflow completed successfully');
|
|
591
|
+
|
|
592
|
+
// Compile final result from step results
|
|
593
|
+
const finalResult = state.data.stepResults?.map(r => r.result);
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
state: {
|
|
597
|
+
...state,
|
|
598
|
+
data: {
|
|
599
|
+
...state.data,
|
|
600
|
+
executionStatus: 'complete' as const,
|
|
601
|
+
finalResult,
|
|
602
|
+
},
|
|
603
|
+
} as PlanExecuteState,
|
|
604
|
+
terminate: true,
|
|
605
|
+
};
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// =========================================================================
|
|
610
|
+
// Node: Failed - Final failure node
|
|
611
|
+
// =========================================================================
|
|
612
|
+
const failedNode = {
|
|
613
|
+
id: 'failed',
|
|
614
|
+
name: 'Failed',
|
|
615
|
+
type: 'end' as const,
|
|
616
|
+
config: {},
|
|
617
|
+
execute: async (
|
|
618
|
+
state: PlanExecuteState,
|
|
619
|
+
context: NodeContext,
|
|
620
|
+
): Promise<NodeResult<PlanExecuteState>> => {
|
|
621
|
+
context.services.logger.error('Workflow failed after max refinements');
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
state: {
|
|
625
|
+
...state,
|
|
626
|
+
data: {
|
|
627
|
+
...state.data,
|
|
628
|
+
executionStatus: 'failed' as const,
|
|
629
|
+
},
|
|
630
|
+
error: {
|
|
631
|
+
code: 'MAX_REFINEMENTS',
|
|
632
|
+
message: `Failed to complete task after ${state.data.refinementCount} refinement attempts`,
|
|
633
|
+
recoverable: false,
|
|
634
|
+
},
|
|
635
|
+
} as PlanExecuteState,
|
|
636
|
+
terminate: true,
|
|
637
|
+
};
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// =========================================================================
|
|
642
|
+
// Build the graph
|
|
643
|
+
// =========================================================================
|
|
644
|
+
graph
|
|
645
|
+
.addNode('planner', plannerNode)
|
|
646
|
+
.addNode('validator', validatorNode)
|
|
647
|
+
.addNode('executor', executorNode)
|
|
648
|
+
.addNode('evaluator', evaluatorNode)
|
|
649
|
+
.addNode('refiner', refinerNode)
|
|
650
|
+
.addNode('refinement-check', refinementCheckNode)
|
|
651
|
+
.addNode('complete', completeNode)
|
|
652
|
+
.addNode('failed', failedNode);
|
|
653
|
+
|
|
654
|
+
if (approvalNode) {
|
|
655
|
+
graph.addNode('approval', approvalNode);
|
|
656
|
+
graph.addNode('approval-router', approvalRouterNode);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Add edges
|
|
660
|
+
graph.addEdge('planner', 'validator').addEdge('refiner', 'refinement-check');
|
|
661
|
+
|
|
662
|
+
if (config.requireApproval && approvalNode) {
|
|
663
|
+
graph
|
|
664
|
+
.addEdge('validator', 'approval')
|
|
665
|
+
.addEdge('approval', 'approval-router');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Set entry point
|
|
669
|
+
graph.setEntryPoint('planner');
|
|
670
|
+
|
|
671
|
+
return graph;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Extract plan from LLM response
|
|
676
|
+
*/
|
|
677
|
+
function extractPlanFromResponse(content: string): PlanStep[] {
|
|
678
|
+
const steps: PlanStep[] = [];
|
|
679
|
+
|
|
680
|
+
// Try to parse JSON if present
|
|
681
|
+
const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
682
|
+
if (jsonMatch) {
|
|
683
|
+
try {
|
|
684
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
685
|
+
if (Array.isArray(parsed)) {
|
|
686
|
+
return parsed.map((item, index) => ({
|
|
687
|
+
id: item.id ?? `step-${index + 1}`,
|
|
688
|
+
description: item.description ?? item.step ?? String(item),
|
|
689
|
+
tool: item.tool,
|
|
690
|
+
toolArgs: item.toolArgs ?? item.args,
|
|
691
|
+
dependsOn: item.dependsOn ?? item.dependencies,
|
|
692
|
+
expectedOutput: item.expectedOutput ?? item.expected,
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
// Fall through to text parsing
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Parse numbered list format
|
|
701
|
+
const lines = content.split('\n');
|
|
702
|
+
let stepIndex = 0;
|
|
703
|
+
|
|
704
|
+
for (const line of lines) {
|
|
705
|
+
const match = line.match(/^\d+\.\s*(.+)$/);
|
|
706
|
+
if (match && match[1]) {
|
|
707
|
+
stepIndex++;
|
|
708
|
+
steps.push({
|
|
709
|
+
id: `step-${stepIndex}`,
|
|
710
|
+
description: match[1].trim(),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return steps;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Default planner system prompt
|
|
720
|
+
*/
|
|
721
|
+
const DEFAULT_PLANNER_PROMPT = `You are an expert planner. Given a task, create a detailed step-by-step plan to accomplish it.
|
|
722
|
+
|
|
723
|
+
Your plan should:
|
|
724
|
+
1. Break down the task into clear, actionable steps
|
|
725
|
+
2. Identify which tools to use for each step (if applicable)
|
|
726
|
+
3. Consider dependencies between steps
|
|
727
|
+
4. Be specific about expected outputs
|
|
728
|
+
|
|
729
|
+
Output your plan as a JSON array of steps, each with:
|
|
730
|
+
- id: unique identifier
|
|
731
|
+
- description: what this step does
|
|
732
|
+
- tool: (optional) tool name to use
|
|
733
|
+
- toolArgs: (optional) arguments for the tool
|
|
734
|
+
- dependsOn: (optional) array of step IDs this depends on
|
|
735
|
+
- expectedOutput: (optional) description of expected result`;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Default refiner system prompt
|
|
739
|
+
*/
|
|
740
|
+
const DEFAULT_REFINER_PROMPT = `You are an expert at analyzing execution results and refining plans.
|
|
741
|
+
|
|
742
|
+
Based on the execution results and any feedback, create an improved plan that:
|
|
743
|
+
1. Addresses any failed steps
|
|
744
|
+
2. Incorporates lessons learned
|
|
745
|
+
3. Optimizes the approach based on what worked
|
|
746
|
+
4. Includes any additional steps needed for success
|
|
747
|
+
|
|
748
|
+
Output your refined plan in the same JSON format as the original.`;
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Create a simple task executor graph (simplified version)
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
755
|
+
* const graph = createSimpleTaskGraph({
|
|
756
|
+
* llmProvider: myProvider,
|
|
757
|
+
* tools: [searchTool]
|
|
758
|
+
* });
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
export function createSimpleTaskGraph(
|
|
762
|
+
config: Omit<PlanExecuteRefineConfig, 'requireApproval' | 'humanHandler'>,
|
|
763
|
+
): StateGraph<PlanExecuteState> {
|
|
764
|
+
return createPlanExecuteRefineGraph({
|
|
765
|
+
...config,
|
|
766
|
+
requireApproval: false,
|
|
767
|
+
maxRefinements: config.maxRefinements ?? 2,
|
|
768
|
+
});
|
|
769
|
+
}
|