@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,525 @@
1
+ /**
2
+ * Tool Node - Tool execution node for agent workflows
3
+ * @module @wundr.io/langgraph-orchestrator
4
+ */
5
+
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import { z } from 'zod';
8
+
9
+ import type {
10
+ AgentState,
11
+ NodeDefinition,
12
+ NodeContext,
13
+ NodeResult,
14
+ Message,
15
+ Tool,
16
+ ToolCall,
17
+ ToolResult,
18
+ ToolRegistry,
19
+ } from '../types';
20
+
21
+ /**
22
+ * Configuration for tool node
23
+ */
24
+ export interface ToolNodeConfig {
25
+ /** Available tools (if not using registry) */
26
+ readonly tools?: Tool[];
27
+ /** Whether to execute tools in parallel */
28
+ readonly parallel?: boolean;
29
+ /** Maximum concurrent tool executions */
30
+ readonly maxConcurrency?: number;
31
+ /** Timeout per tool execution in milliseconds */
32
+ readonly toolTimeout?: number;
33
+ /** Whether to continue on tool error */
34
+ readonly continueOnError?: boolean;
35
+ /** Custom error handler */
36
+ readonly onError?: (error: Error, toolCall: ToolCall) => ToolResult;
37
+ /** Post-processing for tool results */
38
+ readonly postProcess?: (
39
+ results: ToolResult[],
40
+ state: AgentState
41
+ ) => Partial<AgentState['data']>;
42
+ }
43
+
44
+ /**
45
+ * Schema for tool node configuration validation
46
+ */
47
+ export const ToolNodeConfigSchema = z.object({
48
+ parallel: z.boolean().optional(),
49
+ maxConcurrency: z.number().min(1).optional(),
50
+ toolTimeout: z.number().min(0).optional(),
51
+ continueOnError: z.boolean().optional(),
52
+ });
53
+
54
+ /**
55
+ * Create a tool execution node
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const toolNode = createToolNode({
60
+ * id: 'tools',
61
+ * name: 'Tool Executor',
62
+ * config: {
63
+ * parallel: true,
64
+ * maxConcurrency: 3,
65
+ * continueOnError: true
66
+ * }
67
+ * });
68
+ *
69
+ * graph.addNode('tools', toolNode);
70
+ * ```
71
+ *
72
+ * @param options - Node creation options
73
+ * @returns NodeDefinition for use in StateGraph
74
+ */
75
+ export function createToolNode<
76
+ TState extends AgentState = AgentState,
77
+ >(options: {
78
+ id: string;
79
+ name: string;
80
+ config?: ToolNodeConfig;
81
+ nodeConfig?: NodeDefinition<TState>['config'];
82
+ }): NodeDefinition<TState> {
83
+ const { id, name, config = {}, nodeConfig = {} } = options;
84
+
85
+ return {
86
+ id,
87
+ name,
88
+ type: 'tool',
89
+ config: nodeConfig,
90
+ execute: async (
91
+ state: TState,
92
+ context: NodeContext,
93
+ ): Promise<NodeResult<TState>> => {
94
+ // Get pending tool calls from state
95
+ const pendingToolCalls = state.data['pendingToolCalls'] as
96
+ | ToolCall[]
97
+ | undefined;
98
+
99
+ if (!pendingToolCalls || pendingToolCalls.length === 0) {
100
+ context.services.logger.warn(
101
+ 'Tool node executed with no pending tool calls',
102
+ );
103
+ return { state };
104
+ }
105
+
106
+ context.services.logger.debug('Executing tool calls', {
107
+ count: pendingToolCalls.length,
108
+ tools: pendingToolCalls.map(tc => tc.name),
109
+ });
110
+
111
+ // Get tools from config or registry
112
+ const tools =
113
+ config.tools ?? getToolsFromRegistry(context.services.toolRegistry);
114
+ const toolMap = new Map(tools.map(t => [t.name, t]));
115
+
116
+ // Execute tool calls
117
+ const results: ToolResult[] = await executeToolCalls(
118
+ pendingToolCalls,
119
+ toolMap,
120
+ config,
121
+ context,
122
+ );
123
+
124
+ // Build tool result messages
125
+ const toolMessages: Message[] = results.map(result => ({
126
+ id: uuidv4(),
127
+ role: 'tool' as const,
128
+ content: result.content,
129
+ toolResult: result,
130
+ timestamp: new Date(),
131
+ }));
132
+
133
+ // Build updated state
134
+ let newData = { ...state.data };
135
+ delete newData['pendingToolCalls']; // Clear pending calls
136
+
137
+ // Store results in state
138
+ newData['lastToolResults'] = results;
139
+
140
+ // Apply post-processing if configured
141
+ if (config.postProcess) {
142
+ const processed = config.postProcess(results, state);
143
+ newData = { ...newData, ...processed };
144
+ }
145
+
146
+ const newState: TState = {
147
+ ...state,
148
+ messages: [...state.messages, ...toolMessages],
149
+ data: newData,
150
+ } as TState;
151
+
152
+ // Check if all tools succeeded
153
+ const _allSucceeded = results.every(r => r.success);
154
+ const hasErrors = results.some(r => !r.success);
155
+
156
+ context.services.logger.debug('Tool execution complete', {
157
+ total: results.length,
158
+ succeeded: results.filter(r => r.success).length,
159
+ failed: results.filter(r => !r.success).length,
160
+ });
161
+
162
+ return {
163
+ state: newState,
164
+ // If there were errors and we're not continuing, route to error handler
165
+ next: hasErrors && !config.continueOnError ? 'error' : undefined,
166
+ metadata: {
167
+ duration: 0,
168
+ toolCalls: pendingToolCalls,
169
+ },
170
+ };
171
+ },
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Execute tool calls with concurrency control
177
+ */
178
+ async function executeToolCalls(
179
+ toolCalls: ToolCall[],
180
+ toolMap: Map<string, Tool>,
181
+ config: ToolNodeConfig,
182
+ context: NodeContext,
183
+ ): Promise<ToolResult[]> {
184
+ if (config.parallel) {
185
+ return executeParallel(toolCalls, toolMap, config, context);
186
+ }
187
+ return executeSequential(toolCalls, toolMap, config, context);
188
+ }
189
+
190
+ /**
191
+ * Execute tool calls sequentially
192
+ */
193
+ async function executeSequential(
194
+ toolCalls: ToolCall[],
195
+ toolMap: Map<string, Tool>,
196
+ config: ToolNodeConfig,
197
+ context: NodeContext,
198
+ ): Promise<ToolResult[]> {
199
+ const results: ToolResult[] = [];
200
+
201
+ for (const toolCall of toolCalls) {
202
+ const result = await executeSingleTool(toolCall, toolMap, config, context);
203
+ results.push(result);
204
+
205
+ // Stop if there's an error and we're not continuing on error
206
+ if (!result.success && !config.continueOnError) {
207
+ break;
208
+ }
209
+ }
210
+
211
+ return results;
212
+ }
213
+
214
+ /**
215
+ * Execute tool calls in parallel with concurrency limit
216
+ */
217
+ async function executeParallel(
218
+ toolCalls: ToolCall[],
219
+ toolMap: Map<string, Tool>,
220
+ config: ToolNodeConfig,
221
+ context: NodeContext,
222
+ ): Promise<ToolResult[]> {
223
+ const maxConcurrency = config.maxConcurrency ?? 5;
224
+ const results: ToolResult[] = [];
225
+ const executing: Promise<ToolResult>[] = [];
226
+
227
+ for (const toolCall of toolCalls) {
228
+ const promise = executeSingleTool(toolCall, toolMap, config, context);
229
+ executing.push(promise);
230
+
231
+ if (executing.length >= maxConcurrency) {
232
+ const result = await Promise.race(executing);
233
+ results.push(result);
234
+ const index = executing.indexOf(promise);
235
+ if (index > -1) {
236
+ executing.splice(index, 1);
237
+ }
238
+ }
239
+ }
240
+
241
+ // Wait for remaining executions
242
+ const remaining = await Promise.all(executing);
243
+ results.push(...remaining);
244
+
245
+ return results;
246
+ }
247
+
248
+ /**
249
+ * Execute a single tool call
250
+ */
251
+ async function executeSingleTool(
252
+ toolCall: ToolCall,
253
+ toolMap: Map<string, Tool>,
254
+ config: ToolNodeConfig,
255
+ context: NodeContext,
256
+ ): Promise<ToolResult> {
257
+ const tool = toolMap.get(toolCall.name);
258
+
259
+ if (!tool) {
260
+ const error = new Error(`Tool "${toolCall.name}" not found`);
261
+ if (config.onError) {
262
+ return config.onError(error, toolCall);
263
+ }
264
+ return {
265
+ toolCallId: toolCall.id,
266
+ content: `Error: Tool "${toolCall.name}" not found`,
267
+ success: false,
268
+ error: error.message,
269
+ };
270
+ }
271
+
272
+ try {
273
+ // Validate input if schema provided
274
+ if (tool.inputSchema) {
275
+ tool.inputSchema.parse(toolCall.arguments);
276
+ }
277
+
278
+ // Execute with timeout if configured
279
+ const result = config.toolTimeout
280
+ ? await executeWithTimeout(tool, toolCall.arguments, config.toolTimeout)
281
+ : await tool.execute(toolCall.arguments);
282
+
283
+ // Validate output if schema provided
284
+ if (tool.outputSchema) {
285
+ tool.outputSchema.parse(result);
286
+ }
287
+
288
+ context.services.logger.debug('Tool executed successfully', {
289
+ tool: toolCall.name,
290
+ callId: toolCall.id,
291
+ });
292
+
293
+ return {
294
+ toolCallId: toolCall.id,
295
+ content: typeof result === 'string' ? result : JSON.stringify(result),
296
+ success: true,
297
+ };
298
+ } catch (error) {
299
+ const err = error instanceof Error ? error : new Error(String(error));
300
+
301
+ context.services.logger.error('Tool execution failed', {
302
+ tool: toolCall.name,
303
+ callId: toolCall.id,
304
+ error: err.message,
305
+ });
306
+
307
+ if (config.onError) {
308
+ return config.onError(err, toolCall);
309
+ }
310
+
311
+ return {
312
+ toolCallId: toolCall.id,
313
+ content: `Error executing tool "${toolCall.name}": ${err.message}`,
314
+ success: false,
315
+ error: err.message,
316
+ };
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Execute tool with timeout
322
+ */
323
+ async function executeWithTimeout(
324
+ tool: Tool,
325
+ args: Record<string, unknown>,
326
+ timeout: number,
327
+ ): Promise<unknown> {
328
+ return Promise.race([
329
+ tool.execute(args),
330
+ new Promise((_, reject) =>
331
+ setTimeout(
332
+ () => reject(new Error(`Tool execution timed out after ${timeout}ms`)),
333
+ timeout,
334
+ ),
335
+ ),
336
+ ]);
337
+ }
338
+
339
+ /**
340
+ * Get tools from registry
341
+ */
342
+ function getToolsFromRegistry(registry?: ToolRegistry): Tool[] {
343
+ if (!registry) {
344
+ return [];
345
+ }
346
+ return registry.list();
347
+ }
348
+
349
+ /**
350
+ * Create a simple in-memory tool registry
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * const registry = createToolRegistry();
355
+ * registry.register({
356
+ * name: 'search',
357
+ * description: 'Search the web',
358
+ * inputSchema: z.object({ query: z.string() }),
359
+ * execute: async ({ query }) => searchWeb(query)
360
+ * });
361
+ *
362
+ * graph.setServices({ toolRegistry: registry });
363
+ * ```
364
+ *
365
+ * @returns ToolRegistry implementation
366
+ */
367
+ export function createToolRegistry(): ToolRegistry {
368
+ const tools = new Map<string, Tool>();
369
+
370
+ return {
371
+ get(name: string): Tool | undefined {
372
+ return tools.get(name);
373
+ },
374
+
375
+ list(): Tool[] {
376
+ return Array.from(tools.values());
377
+ },
378
+
379
+ register(tool: Tool): void {
380
+ if (tools.has(tool.name)) {
381
+ throw new Error(`Tool "${tool.name}" is already registered`);
382
+ }
383
+ tools.set(tool.name, tool);
384
+ },
385
+
386
+ unregister(name: string): void {
387
+ tools.delete(name);
388
+ },
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Create a tool definition with type-safe input/output
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const searchTool = createTool({
398
+ * name: 'search',
399
+ * description: 'Search the web for information',
400
+ * inputSchema: z.object({
401
+ * query: z.string(),
402
+ * maxResults: z.number().optional()
403
+ * }),
404
+ * outputSchema: z.array(z.object({
405
+ * title: z.string(),
406
+ * url: z.string(),
407
+ * snippet: z.string()
408
+ * })),
409
+ * execute: async ({ query, maxResults = 10 }) => {
410
+ * return await searchWeb(query, maxResults);
411
+ * }
412
+ * });
413
+ * ```
414
+ *
415
+ * @param options - Tool definition options
416
+ * @returns Tool definition
417
+ */
418
+ export function createTool<TInput, TOutput = unknown>(options: {
419
+ name: string;
420
+ description: string;
421
+ inputSchema: z.ZodSchema<TInput>;
422
+ outputSchema?: z.ZodSchema<TOutput>;
423
+ execute: (input: TInput) => Promise<TOutput>;
424
+ }): Tool {
425
+ return {
426
+ name: options.name,
427
+ description: options.description,
428
+ inputSchema: options.inputSchema,
429
+ outputSchema: options.outputSchema,
430
+ execute: async (input: unknown): Promise<unknown> => {
431
+ const parsed = options.inputSchema.parse(input);
432
+ return options.execute(parsed);
433
+ },
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Create a batch tool node that groups tool calls
439
+ *
440
+ * @example
441
+ * ```typescript
442
+ * const batchToolNode = createBatchToolNode({
443
+ * id: 'batch-tools',
444
+ * name: 'Batch Tool Executor',
445
+ * config: {
446
+ * batchSize: 5,
447
+ * batchTimeout: 1000
448
+ * }
449
+ * });
450
+ * ```
451
+ *
452
+ * @param options - Node creation options
453
+ * @returns NodeDefinition for use in StateGraph
454
+ */
455
+ export function createBatchToolNode<
456
+ TState extends AgentState = AgentState,
457
+ >(options: {
458
+ id: string;
459
+ name: string;
460
+ config?: ToolNodeConfig & {
461
+ batchSize?: number;
462
+ batchTimeout?: number;
463
+ };
464
+ nodeConfig?: NodeDefinition<TState>['config'];
465
+ }): NodeDefinition<TState> {
466
+ const { id, name, config = {}, nodeConfig = {} } = options;
467
+ const batchSize = config.batchSize ?? 10;
468
+
469
+ return {
470
+ id,
471
+ name,
472
+ type: 'tool',
473
+ config: nodeConfig,
474
+ execute: async (
475
+ state: TState,
476
+ context: NodeContext,
477
+ ): Promise<NodeResult<TState>> => {
478
+ const pendingToolCalls = state.data['pendingToolCalls'] as
479
+ | ToolCall[]
480
+ | undefined;
481
+
482
+ if (!pendingToolCalls || pendingToolCalls.length === 0) {
483
+ return { state };
484
+ }
485
+
486
+ const tools =
487
+ config.tools ?? getToolsFromRegistry(context.services.toolRegistry);
488
+ const toolMap = new Map(tools.map(t => [t.name, t]));
489
+
490
+ // Process in batches
491
+ const batches: ToolCall[][] = [];
492
+ for (let i = 0; i < pendingToolCalls.length; i += batchSize) {
493
+ batches.push(pendingToolCalls.slice(i, i + batchSize));
494
+ }
495
+
496
+ const allResults: ToolResult[] = [];
497
+
498
+ for (const batch of batches) {
499
+ const results = await executeParallel(batch, toolMap, config, context);
500
+ allResults.push(...results);
501
+ }
502
+
503
+ // Build tool result messages
504
+ const toolMessages: Message[] = allResults.map(result => ({
505
+ id: uuidv4(),
506
+ role: 'tool' as const,
507
+ content: result.content,
508
+ toolResult: result,
509
+ timestamp: new Date(),
510
+ }));
511
+
512
+ const newData = { ...state.data };
513
+ delete newData['pendingToolCalls'];
514
+ newData['lastToolResults'] = allResults;
515
+
516
+ const newState: TState = {
517
+ ...state,
518
+ messages: [...state.messages, ...toolMessages],
519
+ data: newData,
520
+ } as TState;
521
+
522
+ return { state: newState };
523
+ },
524
+ };
525
+ }