@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.
Files changed (57) hide show
  1. package/README.md +842 -0
  2. package/dist/checkpointing.d.ts +265 -0
  3. package/dist/checkpointing.d.ts.map +1 -0
  4. package/dist/checkpointing.js +577 -0
  5. package/dist/checkpointing.js.map +1 -0
  6. package/dist/edges/conditional-edge.d.ts +230 -0
  7. package/dist/edges/conditional-edge.d.ts.map +1 -0
  8. package/dist/edges/conditional-edge.js +439 -0
  9. package/dist/edges/conditional-edge.js.map +1 -0
  10. package/dist/edges/loop-edge.d.ts +290 -0
  11. package/dist/edges/loop-edge.d.ts.map +1 -0
  12. package/dist/edges/loop-edge.js +503 -0
  13. package/dist/edges/loop-edge.js.map +1 -0
  14. package/dist/index.d.ts +125 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +269 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/nodes/decision-node.d.ts +276 -0
  19. package/dist/nodes/decision-node.d.ts.map +1 -0
  20. package/dist/nodes/decision-node.js +403 -0
  21. package/dist/nodes/decision-node.js.map +1 -0
  22. package/dist/nodes/human-node.d.ts +272 -0
  23. package/dist/nodes/human-node.d.ts.map +1 -0
  24. package/dist/nodes/human-node.js +394 -0
  25. package/dist/nodes/human-node.js.map +1 -0
  26. package/dist/nodes/llm-node.d.ts +173 -0
  27. package/dist/nodes/llm-node.d.ts.map +1 -0
  28. package/dist/nodes/llm-node.js +325 -0
  29. package/dist/nodes/llm-node.js.map +1 -0
  30. package/dist/nodes/tool-node.d.ts +151 -0
  31. package/dist/nodes/tool-node.d.ts.map +1 -0
  32. package/dist/nodes/tool-node.js +373 -0
  33. package/dist/nodes/tool-node.js.map +1 -0
  34. package/dist/prebuilt-graphs/plan-execute-refine.d.ts +149 -0
  35. package/dist/prebuilt-graphs/plan-execute-refine.d.ts.map +1 -0
  36. package/dist/prebuilt-graphs/plan-execute-refine.js +600 -0
  37. package/dist/prebuilt-graphs/plan-execute-refine.js.map +1 -0
  38. package/dist/state-graph.d.ts +158 -0
  39. package/dist/state-graph.d.ts.map +1 -0
  40. package/dist/state-graph.js +756 -0
  41. package/dist/state-graph.js.map +1 -0
  42. package/dist/types.d.ts +762 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +73 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +57 -0
  47. package/src/checkpointing.ts +702 -0
  48. package/src/edges/conditional-edge.ts +518 -0
  49. package/src/edges/loop-edge.ts +623 -0
  50. package/src/index.ts +416 -0
  51. package/src/nodes/decision-node.ts +538 -0
  52. package/src/nodes/human-node.ts +572 -0
  53. package/src/nodes/llm-node.ts +448 -0
  54. package/src/nodes/tool-node.ts +525 -0
  55. package/src/prebuilt-graphs/plan-execute-refine.ts +769 -0
  56. package/src/state-graph.ts +990 -0
  57. 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
+ }