@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,448 @@
1
+ /**
2
+ * LLM Node - LLM-based decision and generation node
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
+ LLMRequest,
16
+ LLMResponse,
17
+ Tool,
18
+ } from '../types';
19
+
20
+ /**
21
+ * Configuration for LLM node
22
+ */
23
+ export interface LLMNodeConfig {
24
+ /** Model to use (provider-specific) */
25
+ readonly model?: string;
26
+ /** System prompt for the LLM */
27
+ readonly systemPrompt?: string;
28
+ /** Temperature for generation */
29
+ readonly temperature?: number;
30
+ /** Maximum tokens to generate */
31
+ readonly maxTokens?: number;
32
+ /** Stop sequences */
33
+ readonly stop?: string[];
34
+ /** Available tools for the LLM */
35
+ readonly tools?: Tool[];
36
+ /** Whether to stream the response */
37
+ readonly stream?: boolean;
38
+ /** Custom prompt template */
39
+ readonly promptTemplate?: (state: AgentState) => string;
40
+ /** Post-processing function for response */
41
+ readonly postProcess?: (
42
+ response: LLMResponse,
43
+ state: AgentState
44
+ ) => Partial<AgentState>;
45
+ /** Routing function to determine next node based on response */
46
+ readonly router?: (
47
+ response: LLMResponse,
48
+ state: AgentState
49
+ ) => string | undefined;
50
+ }
51
+
52
+ /**
53
+ * Schema for LLM node configuration validation
54
+ */
55
+ export const LLMNodeConfigSchema = z.object({
56
+ model: z.string().optional(),
57
+ systemPrompt: z.string().optional(),
58
+ temperature: z.number().min(0).max(2).optional(),
59
+ maxTokens: z.number().min(1).optional(),
60
+ stop: z.array(z.string()).optional(),
61
+ stream: z.boolean().optional(),
62
+ });
63
+
64
+ /**
65
+ * Create an LLM node for the workflow graph
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const llmNode = createLLMNode({
70
+ * id: 'agent',
71
+ * name: 'Agent Node',
72
+ * config: {
73
+ * model: 'claude-3-sonnet-20240229',
74
+ * systemPrompt: 'You are a helpful assistant.',
75
+ * temperature: 0.7
76
+ * }
77
+ * });
78
+ *
79
+ * graph.addNode('agent', llmNode);
80
+ * ```
81
+ *
82
+ * @param options - Node creation options
83
+ * @returns NodeDefinition for use in StateGraph
84
+ */
85
+ export function createLLMNode<TState extends AgentState = AgentState>(options: {
86
+ id: string;
87
+ name: string;
88
+ config: LLMNodeConfig;
89
+ nodeConfig?: NodeDefinition<TState>['config'];
90
+ }): NodeDefinition<TState> {
91
+ const { id, name, config, nodeConfig = {} } = options;
92
+
93
+ return {
94
+ id,
95
+ name,
96
+ type: 'llm',
97
+ config: nodeConfig,
98
+ execute: async (
99
+ state: TState,
100
+ context: NodeContext,
101
+ ): Promise<NodeResult<TState>> => {
102
+ const llmProvider = context.services.llmProvider;
103
+
104
+ if (!llmProvider) {
105
+ throw new Error('LLM provider not configured in node services');
106
+ }
107
+
108
+ // Build messages for the request
109
+ const messages = buildMessages(state, config);
110
+
111
+ // Build the request
112
+ const request: LLMRequest = {
113
+ messages,
114
+ model: config.model,
115
+ temperature: config.temperature,
116
+ maxTokens: config.maxTokens,
117
+ stop: config.stop,
118
+ tools: config.tools,
119
+ };
120
+
121
+ context.services.logger.debug('Sending LLM request', {
122
+ model: config.model,
123
+ messageCount: messages.length,
124
+ hasTools: Boolean(config.tools?.length),
125
+ });
126
+
127
+ // Execute the request
128
+ const response = await llmProvider.generate(request);
129
+
130
+ context.services.logger.debug('Received LLM response', {
131
+ model: response.model,
132
+ finishReason: response.finishReason,
133
+ tokensUsed: response.usage.totalTokens,
134
+ });
135
+
136
+ // Build updated state
137
+ const newMessages: Message[] = [...state.messages, response.message];
138
+
139
+ let newData = { ...state.data };
140
+
141
+ // Apply post-processing if configured
142
+ if (config.postProcess) {
143
+ const processed = config.postProcess(response, state);
144
+ newData = { ...newData, ...processed.data };
145
+ }
146
+
147
+ // Update token count in metadata
148
+ const tokensUsed =
149
+ (state.metadata.tokensUsed ?? 0) + response.usage.totalTokens;
150
+
151
+ const newState: TState = {
152
+ ...state,
153
+ messages: newMessages,
154
+ data: newData,
155
+ metadata: {
156
+ ...state.metadata,
157
+ tokensUsed,
158
+ },
159
+ } as TState;
160
+
161
+ // Determine next node
162
+ let next: string | undefined;
163
+
164
+ // Check if there are tool calls - if so, route to tool node
165
+ if (
166
+ response.finishReason === 'tool_calls' &&
167
+ response.message.toolCalls?.length
168
+ ) {
169
+ // Store tool calls in state for tool node to process
170
+ newState.data['pendingToolCalls'] = response.message.toolCalls;
171
+ next = 'tools'; // Default name for tool node
172
+ } else if (config.router) {
173
+ next = config.router(response, state);
174
+ }
175
+
176
+ return {
177
+ state: newState,
178
+ next,
179
+ metadata: {
180
+ duration: 0, // Will be set by executor
181
+ tokensUsed: response.usage.totalTokens,
182
+ toolCalls: response.message.toolCalls,
183
+ },
184
+ };
185
+ },
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Build messages array for LLM request
191
+ */
192
+ function buildMessages(state: AgentState, config: LLMNodeConfig): Message[] {
193
+ const messages: Message[] = [];
194
+
195
+ // Add system prompt if configured
196
+ if (config.systemPrompt) {
197
+ messages.push({
198
+ id: uuidv4(),
199
+ role: 'system',
200
+ content: config.systemPrompt,
201
+ timestamp: new Date(),
202
+ });
203
+ }
204
+
205
+ // Add existing messages from state
206
+ messages.push(...state.messages);
207
+
208
+ // If there's a custom prompt template, add it as a user message
209
+ if (config.promptTemplate) {
210
+ const prompt = config.promptTemplate(state);
211
+ messages.push({
212
+ id: uuidv4(),
213
+ role: 'user',
214
+ content: prompt,
215
+ timestamp: new Date(),
216
+ });
217
+ }
218
+
219
+ return messages;
220
+ }
221
+
222
+ /**
223
+ * Create a router function for LLM decision-making
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const router = createLLMRouter({
228
+ * routes: {
229
+ * 'continue': 'process-node',
230
+ * 'finish': 'end-node',
231
+ * 'error': 'error-handler'
232
+ * },
233
+ * defaultRoute: 'continue',
234
+ * extractDecision: (response) => {
235
+ * const content = response.message.content;
236
+ * if (content.includes('DONE')) return 'finish';
237
+ * if (content.includes('ERROR')) return 'error';
238
+ * return 'continue';
239
+ * }
240
+ * });
241
+ * ```
242
+ *
243
+ * @param options - Router configuration
244
+ * @returns Router function for use in LLM node config
245
+ */
246
+ export function createLLMRouter<
247
+ TState extends AgentState = AgentState,
248
+ >(options: {
249
+ routes: Record<string, string>;
250
+ defaultRoute?: string;
251
+ extractDecision: (response: LLMResponse, state: TState) => string;
252
+ }): (response: LLMResponse, state: TState) => string | undefined {
253
+ return (response: LLMResponse, state: TState): string | undefined => {
254
+ const decision = options.extractDecision(response, state);
255
+ return options.routes[decision] ?? options.defaultRoute;
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Create a structured output LLM node that validates responses
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const responseSchema = z.object({
265
+ * action: z.enum(['search', 'answer', 'clarify']),
266
+ * content: z.string(),
267
+ * confidence: z.number()
268
+ * });
269
+ *
270
+ * const structuredNode = createStructuredLLMNode({
271
+ * id: 'structured-agent',
272
+ * name: 'Structured Agent',
273
+ * config: {
274
+ * model: 'claude-3-sonnet-20240229',
275
+ * systemPrompt: 'Respond in JSON format with action, content, and confidence fields.'
276
+ * },
277
+ * outputSchema: responseSchema,
278
+ * stateMapper: (parsed) => ({ agentDecision: parsed })
279
+ * });
280
+ * ```
281
+ *
282
+ * @param options - Structured node options
283
+ * @returns NodeDefinition for use in StateGraph
284
+ */
285
+ export function createStructuredLLMNode<
286
+ TState extends AgentState = AgentState,
287
+ TOutput = unknown,
288
+ >(options: {
289
+ id: string;
290
+ name: string;
291
+ config: LLMNodeConfig;
292
+ outputSchema: z.ZodSchema<TOutput>;
293
+ stateMapper: (parsed: TOutput) => Partial<TState['data']>;
294
+ nodeConfig?: NodeDefinition<TState>['config'];
295
+ }): NodeDefinition<TState> {
296
+ const {
297
+ id,
298
+ name,
299
+ config,
300
+ outputSchema,
301
+ stateMapper,
302
+ nodeConfig = {},
303
+ } = options;
304
+
305
+ const baseNode = createLLMNode<TState>({
306
+ id,
307
+ name,
308
+ config: {
309
+ ...config,
310
+ postProcess: (response, state) => {
311
+ // Try to parse JSON from response
312
+ const content = response.message.content;
313
+ let parsed: TOutput;
314
+
315
+ try {
316
+ // Try to extract JSON from the response
317
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
318
+ if (jsonMatch) {
319
+ const raw = JSON.parse(jsonMatch[0]);
320
+ parsed = outputSchema.parse(raw);
321
+ } else {
322
+ throw new Error('No JSON object found in response');
323
+ }
324
+ } catch (error) {
325
+ throw new Error(
326
+ `Failed to parse structured output: ${error instanceof Error ? error.message : String(error)}`,
327
+ );
328
+ }
329
+
330
+ // Map parsed output to state
331
+ const mappedData = stateMapper(parsed);
332
+ return { data: { ...state.data, ...mappedData } };
333
+ },
334
+ },
335
+ nodeConfig,
336
+ });
337
+
338
+ return {
339
+ ...baseNode,
340
+ outputSchema,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Create a conversational LLM node that maintains chat history
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * const chatNode = createConversationalLLMNode({
350
+ * id: 'chat',
351
+ * name: 'Chat Node',
352
+ * config: {
353
+ * model: 'claude-3-sonnet-20240229',
354
+ * systemPrompt: 'You are a helpful assistant.'
355
+ * },
356
+ * maxHistoryLength: 10
357
+ * });
358
+ * ```
359
+ *
360
+ * @param options - Conversational node options
361
+ * @returns NodeDefinition for use in StateGraph
362
+ */
363
+ export function createConversationalLLMNode<
364
+ TState extends AgentState = AgentState,
365
+ >(options: {
366
+ id: string;
367
+ name: string;
368
+ config: LLMNodeConfig;
369
+ maxHistoryLength?: number;
370
+ nodeConfig?: NodeDefinition<TState>['config'];
371
+ }): NodeDefinition<TState> {
372
+ const { id, name, config, maxHistoryLength = 50, nodeConfig = {} } = options;
373
+
374
+ return {
375
+ id,
376
+ name,
377
+ type: 'llm',
378
+ config: nodeConfig,
379
+ execute: async (
380
+ state: TState,
381
+ context: NodeContext,
382
+ ): Promise<NodeResult<TState>> => {
383
+ const llmProvider = context.services.llmProvider;
384
+
385
+ if (!llmProvider) {
386
+ throw new Error('LLM provider not configured in node services');
387
+ }
388
+
389
+ // Trim history if needed
390
+ let historyMessages = state.messages;
391
+ if (historyMessages.length > maxHistoryLength) {
392
+ // Keep system message if present, then most recent messages
393
+ const systemMessages = historyMessages.filter(m => m.role === 'system');
394
+ const nonSystemMessages = historyMessages.filter(
395
+ m => m.role !== 'system',
396
+ );
397
+ const trimmedNonSystem = nonSystemMessages.slice(-maxHistoryLength);
398
+ historyMessages = [...systemMessages, ...trimmedNonSystem];
399
+ }
400
+
401
+ // Build messages
402
+ const messages: Message[] = [];
403
+
404
+ if (config.systemPrompt) {
405
+ messages.push({
406
+ id: uuidv4(),
407
+ role: 'system',
408
+ content: config.systemPrompt,
409
+ timestamp: new Date(),
410
+ });
411
+ }
412
+
413
+ messages.push(...historyMessages.filter(m => m.role !== 'system'));
414
+
415
+ const request: LLMRequest = {
416
+ messages,
417
+ model: config.model,
418
+ temperature: config.temperature,
419
+ maxTokens: config.maxTokens,
420
+ stop: config.stop,
421
+ tools: config.tools,
422
+ };
423
+
424
+ const response = await llmProvider.generate(request);
425
+
426
+ const newMessages = [...state.messages, response.message];
427
+ const tokensUsed =
428
+ (state.metadata.tokensUsed ?? 0) + response.usage.totalTokens;
429
+
430
+ const newState: TState = {
431
+ ...state,
432
+ messages: newMessages,
433
+ metadata: {
434
+ ...state.metadata,
435
+ tokensUsed,
436
+ },
437
+ } as TState;
438
+
439
+ return {
440
+ state: newState,
441
+ metadata: {
442
+ duration: 0,
443
+ tokensUsed: response.usage.totalTokens,
444
+ },
445
+ };
446
+ },
447
+ };
448
+ }