@agentforge/patterns 0.15.9 → 0.15.11

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 (3) hide show
  1. package/dist/index.cjs +183 -105
  2. package/dist/index.js +188 -105
  3. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -273,10 +273,12 @@ function formatScratchpad(scratchpad) {
273
273
  }).join("\n\n");
274
274
  }
275
275
 
276
- // src/react/nodes.ts
277
- var import_messages = require("@langchain/core/messages");
276
+ // src/react/nodes/reasoning.ts
278
277
  var import_core3 = require("@agentforge/core");
279
278
 
279
+ // src/react/nodes/shared.ts
280
+ var import_messages = require("@langchain/core/messages");
281
+
280
282
  // src/shared/deduplication.ts
281
283
  var import_core2 = require("@agentforge/core");
282
284
  function normalizeObject(obj) {
@@ -321,76 +323,114 @@ function buildDeduplicationMetrics(toolsExecuted, duplicatesSkipped, totalObserv
321
323
  };
322
324
  }
323
325
 
324
- // src/shared/error-handling.ts
325
- function isGraphInterrupt(error) {
326
- return error !== null && typeof error === "object" && "constructor" in error && error.constructor.name === "GraphInterrupt";
326
+ // src/react/nodes/shared.ts
327
+ var reasoningLogger = createPatternLogger("agentforge:patterns:react:reasoning");
328
+ var actionLogger = createPatternLogger("agentforge:patterns:react:action");
329
+ var observationLogger = createPatternLogger("agentforge:patterns:react:observation");
330
+ function normalizeConversationMessage(message) {
331
+ switch (message.role) {
332
+ case "user":
333
+ return new import_messages.HumanMessage(message.content);
334
+ case "assistant":
335
+ return new import_messages.AIMessage(message.content);
336
+ case "system":
337
+ return new import_messages.SystemMessage(message.content);
338
+ case "tool":
339
+ if (!message.tool_call_id) {
340
+ reasoningLogger.warn(
341
+ "Tool message missing tool_call_id; falling back to human message",
342
+ message.name ? { name: message.name } : void 0
343
+ );
344
+ return new import_messages.HumanMessage(message.content);
345
+ }
346
+ return new import_messages.ToolMessage({
347
+ content: message.content,
348
+ tool_call_id: message.tool_call_id,
349
+ name: message.name
350
+ });
351
+ default:
352
+ return new import_messages.HumanMessage(message.content);
353
+ }
327
354
  }
328
- function handleNodeError(error, context, verbose = false) {
329
- if (isGraphInterrupt(error)) {
330
- throw error;
355
+ function buildReasoningMessages(systemPrompt, stateMessages, scratchpad) {
356
+ const messages = [
357
+ new import_messages.SystemMessage(systemPrompt),
358
+ ...stateMessages.map(normalizeConversationMessage)
359
+ ];
360
+ if (scratchpad.length > 0) {
361
+ messages.push(new import_messages.SystemMessage(`Previous steps:
362
+ ${formatScratchpad(scratchpad)}`));
331
363
  }
332
- const errorMessage = error instanceof Error ? error.message : String(error);
333
- if (verbose) {
334
- console.error(`[${context}] Error:`, errorMessage);
335
- if (error instanceof Error && error.stack) {
336
- console.error(`[${context}] Stack:`, error.stack);
364
+ return messages;
365
+ }
366
+ function extractToolCalls(response) {
367
+ if (!response.tool_calls || response.tool_calls.length === 0) {
368
+ return [];
369
+ }
370
+ return response.tool_calls.map((toolCall) => ({
371
+ id: toolCall.id || `call_${Date.now()}_${Math.random()}`,
372
+ name: toolCall.name,
373
+ arguments: toolCall.args ?? {},
374
+ timestamp: Date.now()
375
+ }));
376
+ }
377
+ function formatObservationContent(observation) {
378
+ if (observation.error) {
379
+ return `Error: ${observation.error}`;
380
+ }
381
+ return stringifyObservationResult(observation.result, 2);
382
+ }
383
+ function formatActionSummary(actions) {
384
+ return actions.map((action) => `${action.name}(${stringifyActionArguments(action.arguments)})`).join(", ");
385
+ }
386
+ function formatObservationSummary(observations) {
387
+ return observations.map((observation) => {
388
+ if (observation.error) {
389
+ return `Error: ${observation.error}`;
337
390
  }
391
+ return stringifyObservationResult(observation.result);
392
+ }).join("; ");
393
+ }
394
+ function stringifyObservationResult(result, space) {
395
+ if (typeof result === "string") {
396
+ return result;
397
+ }
398
+ try {
399
+ const stringified = JSON.stringify(result, null, space);
400
+ return stringified ?? String(result);
401
+ } catch {
402
+ return String(result);
403
+ }
404
+ }
405
+ function stringifyActionArguments(arguments_) {
406
+ return stringifyObservationResult(arguments_);
407
+ }
408
+ function getLatestThought(thoughts) {
409
+ return thoughts[thoughts.length - 1]?.content ?? "";
410
+ }
411
+ function debugIfVerbose(logger4, verbose, message, data) {
412
+ if (verbose) {
413
+ logger4.debug(message, data);
338
414
  }
339
- return errorMessage;
340
415
  }
341
416
 
342
- // src/react/nodes.ts
343
- var reasoningLogger = createPatternLogger("agentforge:patterns:react:reasoning");
344
- var actionLogger = createPatternLogger("agentforge:patterns:react:action");
345
- var observationLogger = createPatternLogger("agentforge:patterns:react:observation");
417
+ // src/react/nodes/reasoning.ts
346
418
  function createReasoningNode(llm, tools, systemPrompt, maxIterations, verbose = false) {
347
419
  const langchainTools = (0, import_core3.toLangChainTools)(tools);
348
420
  const llmWithTools = llm.bindTools ? llm.bindTools(langchainTools) : llm;
349
421
  return async (state) => {
350
422
  const currentIteration = state.iteration || 0;
351
423
  const startTime = Date.now();
352
- reasoningLogger.debug("Reasoning iteration started", {
424
+ debugIfVerbose(reasoningLogger, verbose, "Reasoning iteration started", {
353
425
  iteration: currentIteration + 1,
354
426
  maxIterations,
355
- observationCount: state.observations?.length || 0,
356
- hasActions: !!state.actions?.length
427
+ observationCount: state.observations.length,
428
+ hasActions: state.actions.length > 0
357
429
  });
358
- const stateMessages = state.messages || [];
359
- const messages = [
360
- new import_messages.SystemMessage(systemPrompt),
361
- ...stateMessages.map((msg) => {
362
- if (msg.role === "user") return new import_messages.HumanMessage(msg.content);
363
- if (msg.role === "assistant") return new import_messages.AIMessage(msg.content);
364
- if (msg.role === "system") return new import_messages.SystemMessage(msg.content);
365
- if (msg.role === "tool") {
366
- return new import_messages.ToolMessage({
367
- content: msg.content,
368
- tool_call_id: msg.tool_call_id,
369
- name: msg.name
370
- });
371
- }
372
- return new import_messages.HumanMessage(msg.content);
373
- })
374
- ];
375
- const scratchpad = state.scratchpad || [];
376
- if (scratchpad.length > 0) {
377
- const scratchpadText = formatScratchpad(scratchpad);
378
- messages.push(new import_messages.SystemMessage(`Previous steps:
379
- ${scratchpadText}`));
380
- }
430
+ const messages = buildReasoningMessages(systemPrompt, state.messages, state.scratchpad);
381
431
  const response = await llmWithTools.invoke(messages);
382
432
  const thought = typeof response.content === "string" ? response.content : "";
383
- const toolCalls = [];
384
- if (response.tool_calls && response.tool_calls.length > 0) {
385
- for (const toolCall of response.tool_calls) {
386
- toolCalls.push({
387
- id: toolCall.id || `call_${Date.now()}_${Math.random()}`,
388
- name: toolCall.name,
389
- arguments: toolCall.args || {},
390
- timestamp: Date.now()
391
- });
392
- }
393
- }
433
+ const toolCalls = extractToolCalls(response);
394
434
  const shouldContinue = toolCalls.length > 0 && currentIteration + 1 < maxIterations;
395
435
  reasoningLogger.info("Reasoning complete", {
396
436
  iteration: currentIteration + 1,
@@ -405,21 +445,49 @@ ${scratchpadText}`));
405
445
  thoughts: thought ? [{ content: thought, timestamp: Date.now() }] : [],
406
446
  actions: toolCalls,
407
447
  iteration: 1,
408
- // Add 1 to iteration counter (uses additive reducer)
409
448
  shouldContinue,
410
449
  response: toolCalls.length === 0 ? thought : void 0
411
- // Final response if no tool calls
412
450
  };
413
451
  };
414
452
  }
453
+
454
+ // src/shared/error-handling.ts
455
+ function isGraphInterrupt(error) {
456
+ return error !== null && typeof error === "object" && "constructor" in error && error.constructor.name === "GraphInterrupt";
457
+ }
458
+ function handleNodeError(error, context, verbose = false) {
459
+ if (isGraphInterrupt(error)) {
460
+ throw error;
461
+ }
462
+ const errorMessage = error instanceof Error ? error.message : String(error);
463
+ if (verbose) {
464
+ console.error(`[${context}] Error:`, errorMessage);
465
+ if (error instanceof Error && error.stack) {
466
+ console.error(`[${context}] Stack:`, error.stack);
467
+ }
468
+ }
469
+ return errorMessage;
470
+ }
471
+
472
+ // src/react/nodes/action.ts
415
473
  function createActionNode(tools, verbose = false, enableDeduplication = true) {
416
474
  const toolMap = new Map(tools.map((tool) => [tool.metadata.name, tool]));
475
+ function buildCacheKey(toolName, args) {
476
+ try {
477
+ return generateToolCallCacheKey(toolName, args);
478
+ } catch {
479
+ debugIfVerbose(actionLogger, verbose, "Skipping deduplication for unserializable tool call", {
480
+ toolName
481
+ });
482
+ return void 0;
483
+ }
484
+ }
417
485
  return async (state) => {
418
- const actions = state.actions || [];
419
- const allObservations = state.observations || [];
486
+ const actions = state.actions;
487
+ const allObservations = state.observations;
420
488
  const iteration = state.iteration || 0;
421
489
  const startTime = Date.now();
422
- actionLogger.debug("Action node started", {
490
+ debugIfVerbose(actionLogger, verbose, "Action node started", {
423
491
  actionCount: actions.length,
424
492
  iteration,
425
493
  cacheEnabled: enableDeduplication
@@ -427,18 +495,27 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
427
495
  const recentActions = actions.slice(-10);
428
496
  const observations = [];
429
497
  const executionCache = /* @__PURE__ */ new Map();
498
+ const actionsById = new Map(actions.map((action) => [action.id, action]));
499
+ const observedToolCallIds = new Set(
500
+ allObservations.map((observation) => observation.toolCallId)
501
+ );
430
502
  let cacheSize = 0;
431
503
  if (enableDeduplication) {
432
504
  for (const observation of allObservations) {
433
- const correspondingAction = actions.find((a) => a.id === observation.toolCallId);
505
+ const correspondingAction = actionsById.get(observation.toolCallId);
434
506
  if (correspondingAction) {
435
- const cacheKey = generateToolCallCacheKey(correspondingAction.name, correspondingAction.arguments);
436
- executionCache.set(cacheKey, observation);
437
- cacheSize++;
507
+ const cacheKey = buildCacheKey(
508
+ correspondingAction.name,
509
+ correspondingAction.arguments
510
+ );
511
+ if (cacheKey) {
512
+ executionCache.set(cacheKey, observation);
513
+ cacheSize++;
514
+ }
438
515
  }
439
516
  }
440
517
  if (cacheSize > 0) {
441
- actionLogger.debug("Deduplication cache built", {
518
+ debugIfVerbose(actionLogger, verbose, "Deduplication cache built", {
442
519
  cacheSize,
443
520
  totalObservations: allObservations.length
444
521
  });
@@ -447,9 +524,8 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
447
524
  let duplicatesSkipped = 0;
448
525
  let toolsExecuted = 0;
449
526
  for (const action of recentActions) {
450
- const existingObservation = allObservations.find((obs) => obs.toolCallId === action.id);
451
- if (existingObservation) {
452
- actionLogger.debug("Skipping already-processed action", {
527
+ if (observedToolCallIds.has(action.id)) {
528
+ debugIfVerbose(actionLogger, verbose, "Skipping already-processed action", {
453
529
  toolName: action.name,
454
530
  toolCallId: action.id,
455
531
  iteration
@@ -457,13 +533,13 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
457
533
  continue;
458
534
  }
459
535
  if (enableDeduplication) {
460
- const cacheKey = generateToolCallCacheKey(action.name, action.arguments);
461
- const cachedResult = executionCache.get(cacheKey);
536
+ const cacheKey = buildCacheKey(action.name, action.arguments);
537
+ const cachedResult = cacheKey ? executionCache.get(cacheKey) : void 0;
462
538
  if (cachedResult) {
463
539
  duplicatesSkipped++;
464
540
  actionLogger.info("Duplicate tool call prevented", {
465
541
  toolName: action.name,
466
- arguments: action.arguments,
542
+ argumentKeys: Object.keys(action.arguments),
467
543
  iteration,
468
544
  cacheHit: true
469
545
  });
@@ -488,11 +564,11 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
488
564
  continue;
489
565
  }
490
566
  try {
491
- const startTime2 = Date.now();
567
+ const toolStartTime = Date.now();
492
568
  const result = await tool.invoke(action.arguments);
493
- const executionTime = Date.now() - startTime2;
569
+ const executionTime = Date.now() - toolStartTime;
494
570
  toolsExecuted++;
495
- actionLogger.debug("Tool executed successfully", {
571
+ debugIfVerbose(actionLogger, verbose, "Tool executed successfully", {
496
572
  toolName: action.name,
497
573
  executionTime,
498
574
  iteration
@@ -504,11 +580,13 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
504
580
  };
505
581
  observations.push(observation);
506
582
  if (enableDeduplication) {
507
- const cacheKey = generateToolCallCacheKey(action.name, action.arguments);
508
- executionCache.set(cacheKey, observation);
583
+ const cacheKey = buildCacheKey(action.name, action.arguments);
584
+ if (cacheKey) {
585
+ executionCache.set(cacheKey, observation);
586
+ }
509
587
  }
510
588
  } catch (error) {
511
- const errorMessage = handleNodeError(error, `action:${action.name}`, false);
589
+ const errorMessage = handleNodeError(error, `action:${action.name}`, verbose);
512
590
  actionLogger.error("Tool execution failed", {
513
591
  toolName: action.name,
514
592
  error: errorMessage,
@@ -523,7 +601,11 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
523
601
  }
524
602
  }
525
603
  if (duplicatesSkipped > 0 || toolsExecuted > 0) {
526
- const metrics = buildDeduplicationMetrics(toolsExecuted, duplicatesSkipped, observations.length);
604
+ const metrics = buildDeduplicationMetrics(
605
+ toolsExecuted,
606
+ duplicatesSkipped,
607
+ observations.length
608
+ );
527
609
  actionLogger.info("Action node complete", {
528
610
  iteration,
529
611
  ...metrics,
@@ -535,41 +617,37 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
535
617
  };
536
618
  };
537
619
  }
620
+
621
+ // src/react/nodes/observation.ts
538
622
  function createObservationNode(verbose = false, returnIntermediateSteps = false) {
539
623
  return async (state) => {
540
- const observations = state.observations || [];
541
- const thoughts = state.thoughts || [];
542
- const actions = state.actions || [];
624
+ const observations = state.observations;
625
+ const thoughts = state.thoughts;
626
+ const actions = state.actions;
543
627
  const iteration = state.iteration || 0;
544
- observationLogger.debug("Processing observations", {
628
+ debugIfVerbose(observationLogger, verbose, "Processing observations", {
545
629
  observationCount: observations.length,
546
630
  iteration
547
631
  });
548
632
  const recentObservations = observations.slice(-10);
549
633
  const latestActions = actions.slice(-10);
550
- const observationMessages = recentObservations.map((obs) => {
551
- const content = obs.error ? `Error: ${obs.error}` : typeof obs.result === "string" ? obs.result : JSON.stringify(obs.result, null, 2);
552
- return {
553
- role: "tool",
554
- content,
555
- name: latestActions.find((a) => a.id === obs.toolCallId)?.name,
556
- tool_call_id: obs.toolCallId
557
- // Include tool_call_id for proper ToolMessage construction
558
- };
559
- });
560
- const scratchpadEntries = returnIntermediateSteps ? [{
561
- step: state.iteration,
562
- thought: thoughts[thoughts.length - 1]?.content || "",
563
- action: latestActions.map((a) => `${a.name}(${JSON.stringify(a.arguments)})`).join(", "),
564
- observation: recentObservations.map((obs) => {
565
- if (obs.error) {
566
- return `Error: ${obs.error}`;
567
- }
568
- return typeof obs.result === "string" ? obs.result : JSON.stringify(obs.result);
569
- }).join("; "),
570
- timestamp: Date.now()
571
- }] : [];
572
- observationLogger.debug("Observation node complete", {
634
+ const actionNamesById = new Map(latestActions.map((action) => [action.id, action.name]));
635
+ const observationMessages = recentObservations.map((observation) => ({
636
+ role: "tool",
637
+ content: formatObservationContent(observation),
638
+ name: actionNamesById.get(observation.toolCallId),
639
+ tool_call_id: observation.toolCallId
640
+ }));
641
+ const scratchpadEntries = returnIntermediateSteps ? [
642
+ {
643
+ step: iteration,
644
+ thought: getLatestThought(thoughts),
645
+ action: formatActionSummary(latestActions),
646
+ observation: formatObservationSummary(recentObservations),
647
+ timestamp: Date.now()
648
+ }
649
+ ] : [];
650
+ debugIfVerbose(observationLogger, verbose, "Observation node complete", {
573
651
  iteration,
574
652
  scratchpadUpdated: returnIntermediateSteps,
575
653
  messageCount: observationMessages.length
package/dist/index.js CHANGED
@@ -170,10 +170,17 @@ function formatScratchpad(scratchpad) {
170
170
  }).join("\n\n");
171
171
  }
172
172
 
173
- // src/react/nodes.ts
174
- import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
173
+ // src/react/nodes/reasoning.ts
175
174
  import { toLangChainTools } from "@agentforge/core";
176
175
 
176
+ // src/react/nodes/shared.ts
177
+ import {
178
+ HumanMessage,
179
+ AIMessage,
180
+ SystemMessage,
181
+ ToolMessage
182
+ } from "@langchain/core/messages";
183
+
177
184
  // src/shared/deduplication.ts
178
185
  import { createLogger } from "@agentforge/core";
179
186
  function normalizeObject(obj) {
@@ -218,76 +225,114 @@ function buildDeduplicationMetrics(toolsExecuted, duplicatesSkipped, totalObserv
218
225
  };
219
226
  }
220
227
 
221
- // src/shared/error-handling.ts
222
- function isGraphInterrupt(error) {
223
- return error !== null && typeof error === "object" && "constructor" in error && error.constructor.name === "GraphInterrupt";
228
+ // src/react/nodes/shared.ts
229
+ var reasoningLogger = createPatternLogger("agentforge:patterns:react:reasoning");
230
+ var actionLogger = createPatternLogger("agentforge:patterns:react:action");
231
+ var observationLogger = createPatternLogger("agentforge:patterns:react:observation");
232
+ function normalizeConversationMessage(message) {
233
+ switch (message.role) {
234
+ case "user":
235
+ return new HumanMessage(message.content);
236
+ case "assistant":
237
+ return new AIMessage(message.content);
238
+ case "system":
239
+ return new SystemMessage(message.content);
240
+ case "tool":
241
+ if (!message.tool_call_id) {
242
+ reasoningLogger.warn(
243
+ "Tool message missing tool_call_id; falling back to human message",
244
+ message.name ? { name: message.name } : void 0
245
+ );
246
+ return new HumanMessage(message.content);
247
+ }
248
+ return new ToolMessage({
249
+ content: message.content,
250
+ tool_call_id: message.tool_call_id,
251
+ name: message.name
252
+ });
253
+ default:
254
+ return new HumanMessage(message.content);
255
+ }
224
256
  }
225
- function handleNodeError(error, context, verbose = false) {
226
- if (isGraphInterrupt(error)) {
227
- throw error;
257
+ function buildReasoningMessages(systemPrompt, stateMessages, scratchpad) {
258
+ const messages = [
259
+ new SystemMessage(systemPrompt),
260
+ ...stateMessages.map(normalizeConversationMessage)
261
+ ];
262
+ if (scratchpad.length > 0) {
263
+ messages.push(new SystemMessage(`Previous steps:
264
+ ${formatScratchpad(scratchpad)}`));
228
265
  }
229
- const errorMessage = error instanceof Error ? error.message : String(error);
230
- if (verbose) {
231
- console.error(`[${context}] Error:`, errorMessage);
232
- if (error instanceof Error && error.stack) {
233
- console.error(`[${context}] Stack:`, error.stack);
266
+ return messages;
267
+ }
268
+ function extractToolCalls(response) {
269
+ if (!response.tool_calls || response.tool_calls.length === 0) {
270
+ return [];
271
+ }
272
+ return response.tool_calls.map((toolCall) => ({
273
+ id: toolCall.id || `call_${Date.now()}_${Math.random()}`,
274
+ name: toolCall.name,
275
+ arguments: toolCall.args ?? {},
276
+ timestamp: Date.now()
277
+ }));
278
+ }
279
+ function formatObservationContent(observation) {
280
+ if (observation.error) {
281
+ return `Error: ${observation.error}`;
282
+ }
283
+ return stringifyObservationResult(observation.result, 2);
284
+ }
285
+ function formatActionSummary(actions) {
286
+ return actions.map((action) => `${action.name}(${stringifyActionArguments(action.arguments)})`).join(", ");
287
+ }
288
+ function formatObservationSummary(observations) {
289
+ return observations.map((observation) => {
290
+ if (observation.error) {
291
+ return `Error: ${observation.error}`;
234
292
  }
293
+ return stringifyObservationResult(observation.result);
294
+ }).join("; ");
295
+ }
296
+ function stringifyObservationResult(result, space) {
297
+ if (typeof result === "string") {
298
+ return result;
299
+ }
300
+ try {
301
+ const stringified = JSON.stringify(result, null, space);
302
+ return stringified ?? String(result);
303
+ } catch {
304
+ return String(result);
305
+ }
306
+ }
307
+ function stringifyActionArguments(arguments_) {
308
+ return stringifyObservationResult(arguments_);
309
+ }
310
+ function getLatestThought(thoughts) {
311
+ return thoughts[thoughts.length - 1]?.content ?? "";
312
+ }
313
+ function debugIfVerbose(logger4, verbose, message, data) {
314
+ if (verbose) {
315
+ logger4.debug(message, data);
235
316
  }
236
- return errorMessage;
237
317
  }
238
318
 
239
- // src/react/nodes.ts
240
- var reasoningLogger = createPatternLogger("agentforge:patterns:react:reasoning");
241
- var actionLogger = createPatternLogger("agentforge:patterns:react:action");
242
- var observationLogger = createPatternLogger("agentforge:patterns:react:observation");
319
+ // src/react/nodes/reasoning.ts
243
320
  function createReasoningNode(llm, tools, systemPrompt, maxIterations, verbose = false) {
244
321
  const langchainTools = toLangChainTools(tools);
245
322
  const llmWithTools = llm.bindTools ? llm.bindTools(langchainTools) : llm;
246
323
  return async (state) => {
247
324
  const currentIteration = state.iteration || 0;
248
325
  const startTime = Date.now();
249
- reasoningLogger.debug("Reasoning iteration started", {
326
+ debugIfVerbose(reasoningLogger, verbose, "Reasoning iteration started", {
250
327
  iteration: currentIteration + 1,
251
328
  maxIterations,
252
- observationCount: state.observations?.length || 0,
253
- hasActions: !!state.actions?.length
329
+ observationCount: state.observations.length,
330
+ hasActions: state.actions.length > 0
254
331
  });
255
- const stateMessages = state.messages || [];
256
- const messages = [
257
- new SystemMessage(systemPrompt),
258
- ...stateMessages.map((msg) => {
259
- if (msg.role === "user") return new HumanMessage(msg.content);
260
- if (msg.role === "assistant") return new AIMessage(msg.content);
261
- if (msg.role === "system") return new SystemMessage(msg.content);
262
- if (msg.role === "tool") {
263
- return new ToolMessage({
264
- content: msg.content,
265
- tool_call_id: msg.tool_call_id,
266
- name: msg.name
267
- });
268
- }
269
- return new HumanMessage(msg.content);
270
- })
271
- ];
272
- const scratchpad = state.scratchpad || [];
273
- if (scratchpad.length > 0) {
274
- const scratchpadText = formatScratchpad(scratchpad);
275
- messages.push(new SystemMessage(`Previous steps:
276
- ${scratchpadText}`));
277
- }
332
+ const messages = buildReasoningMessages(systemPrompt, state.messages, state.scratchpad);
278
333
  const response = await llmWithTools.invoke(messages);
279
334
  const thought = typeof response.content === "string" ? response.content : "";
280
- const toolCalls = [];
281
- if (response.tool_calls && response.tool_calls.length > 0) {
282
- for (const toolCall of response.tool_calls) {
283
- toolCalls.push({
284
- id: toolCall.id || `call_${Date.now()}_${Math.random()}`,
285
- name: toolCall.name,
286
- arguments: toolCall.args || {},
287
- timestamp: Date.now()
288
- });
289
- }
290
- }
335
+ const toolCalls = extractToolCalls(response);
291
336
  const shouldContinue = toolCalls.length > 0 && currentIteration + 1 < maxIterations;
292
337
  reasoningLogger.info("Reasoning complete", {
293
338
  iteration: currentIteration + 1,
@@ -302,21 +347,49 @@ ${scratchpadText}`));
302
347
  thoughts: thought ? [{ content: thought, timestamp: Date.now() }] : [],
303
348
  actions: toolCalls,
304
349
  iteration: 1,
305
- // Add 1 to iteration counter (uses additive reducer)
306
350
  shouldContinue,
307
351
  response: toolCalls.length === 0 ? thought : void 0
308
- // Final response if no tool calls
309
352
  };
310
353
  };
311
354
  }
355
+
356
+ // src/shared/error-handling.ts
357
+ function isGraphInterrupt(error) {
358
+ return error !== null && typeof error === "object" && "constructor" in error && error.constructor.name === "GraphInterrupt";
359
+ }
360
+ function handleNodeError(error, context, verbose = false) {
361
+ if (isGraphInterrupt(error)) {
362
+ throw error;
363
+ }
364
+ const errorMessage = error instanceof Error ? error.message : String(error);
365
+ if (verbose) {
366
+ console.error(`[${context}] Error:`, errorMessage);
367
+ if (error instanceof Error && error.stack) {
368
+ console.error(`[${context}] Stack:`, error.stack);
369
+ }
370
+ }
371
+ return errorMessage;
372
+ }
373
+
374
+ // src/react/nodes/action.ts
312
375
  function createActionNode(tools, verbose = false, enableDeduplication = true) {
313
376
  const toolMap = new Map(tools.map((tool) => [tool.metadata.name, tool]));
377
+ function buildCacheKey(toolName, args) {
378
+ try {
379
+ return generateToolCallCacheKey(toolName, args);
380
+ } catch {
381
+ debugIfVerbose(actionLogger, verbose, "Skipping deduplication for unserializable tool call", {
382
+ toolName
383
+ });
384
+ return void 0;
385
+ }
386
+ }
314
387
  return async (state) => {
315
- const actions = state.actions || [];
316
- const allObservations = state.observations || [];
388
+ const actions = state.actions;
389
+ const allObservations = state.observations;
317
390
  const iteration = state.iteration || 0;
318
391
  const startTime = Date.now();
319
- actionLogger.debug("Action node started", {
392
+ debugIfVerbose(actionLogger, verbose, "Action node started", {
320
393
  actionCount: actions.length,
321
394
  iteration,
322
395
  cacheEnabled: enableDeduplication
@@ -324,18 +397,27 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
324
397
  const recentActions = actions.slice(-10);
325
398
  const observations = [];
326
399
  const executionCache = /* @__PURE__ */ new Map();
400
+ const actionsById = new Map(actions.map((action) => [action.id, action]));
401
+ const observedToolCallIds = new Set(
402
+ allObservations.map((observation) => observation.toolCallId)
403
+ );
327
404
  let cacheSize = 0;
328
405
  if (enableDeduplication) {
329
406
  for (const observation of allObservations) {
330
- const correspondingAction = actions.find((a) => a.id === observation.toolCallId);
407
+ const correspondingAction = actionsById.get(observation.toolCallId);
331
408
  if (correspondingAction) {
332
- const cacheKey = generateToolCallCacheKey(correspondingAction.name, correspondingAction.arguments);
333
- executionCache.set(cacheKey, observation);
334
- cacheSize++;
409
+ const cacheKey = buildCacheKey(
410
+ correspondingAction.name,
411
+ correspondingAction.arguments
412
+ );
413
+ if (cacheKey) {
414
+ executionCache.set(cacheKey, observation);
415
+ cacheSize++;
416
+ }
335
417
  }
336
418
  }
337
419
  if (cacheSize > 0) {
338
- actionLogger.debug("Deduplication cache built", {
420
+ debugIfVerbose(actionLogger, verbose, "Deduplication cache built", {
339
421
  cacheSize,
340
422
  totalObservations: allObservations.length
341
423
  });
@@ -344,9 +426,8 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
344
426
  let duplicatesSkipped = 0;
345
427
  let toolsExecuted = 0;
346
428
  for (const action of recentActions) {
347
- const existingObservation = allObservations.find((obs) => obs.toolCallId === action.id);
348
- if (existingObservation) {
349
- actionLogger.debug("Skipping already-processed action", {
429
+ if (observedToolCallIds.has(action.id)) {
430
+ debugIfVerbose(actionLogger, verbose, "Skipping already-processed action", {
350
431
  toolName: action.name,
351
432
  toolCallId: action.id,
352
433
  iteration
@@ -354,13 +435,13 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
354
435
  continue;
355
436
  }
356
437
  if (enableDeduplication) {
357
- const cacheKey = generateToolCallCacheKey(action.name, action.arguments);
358
- const cachedResult = executionCache.get(cacheKey);
438
+ const cacheKey = buildCacheKey(action.name, action.arguments);
439
+ const cachedResult = cacheKey ? executionCache.get(cacheKey) : void 0;
359
440
  if (cachedResult) {
360
441
  duplicatesSkipped++;
361
442
  actionLogger.info("Duplicate tool call prevented", {
362
443
  toolName: action.name,
363
- arguments: action.arguments,
444
+ argumentKeys: Object.keys(action.arguments),
364
445
  iteration,
365
446
  cacheHit: true
366
447
  });
@@ -385,11 +466,11 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
385
466
  continue;
386
467
  }
387
468
  try {
388
- const startTime2 = Date.now();
469
+ const toolStartTime = Date.now();
389
470
  const result = await tool.invoke(action.arguments);
390
- const executionTime = Date.now() - startTime2;
471
+ const executionTime = Date.now() - toolStartTime;
391
472
  toolsExecuted++;
392
- actionLogger.debug("Tool executed successfully", {
473
+ debugIfVerbose(actionLogger, verbose, "Tool executed successfully", {
393
474
  toolName: action.name,
394
475
  executionTime,
395
476
  iteration
@@ -401,11 +482,13 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
401
482
  };
402
483
  observations.push(observation);
403
484
  if (enableDeduplication) {
404
- const cacheKey = generateToolCallCacheKey(action.name, action.arguments);
405
- executionCache.set(cacheKey, observation);
485
+ const cacheKey = buildCacheKey(action.name, action.arguments);
486
+ if (cacheKey) {
487
+ executionCache.set(cacheKey, observation);
488
+ }
406
489
  }
407
490
  } catch (error) {
408
- const errorMessage = handleNodeError(error, `action:${action.name}`, false);
491
+ const errorMessage = handleNodeError(error, `action:${action.name}`, verbose);
409
492
  actionLogger.error("Tool execution failed", {
410
493
  toolName: action.name,
411
494
  error: errorMessage,
@@ -420,7 +503,11 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
420
503
  }
421
504
  }
422
505
  if (duplicatesSkipped > 0 || toolsExecuted > 0) {
423
- const metrics = buildDeduplicationMetrics(toolsExecuted, duplicatesSkipped, observations.length);
506
+ const metrics = buildDeduplicationMetrics(
507
+ toolsExecuted,
508
+ duplicatesSkipped,
509
+ observations.length
510
+ );
424
511
  actionLogger.info("Action node complete", {
425
512
  iteration,
426
513
  ...metrics,
@@ -432,41 +519,37 @@ function createActionNode(tools, verbose = false, enableDeduplication = true) {
432
519
  };
433
520
  };
434
521
  }
522
+
523
+ // src/react/nodes/observation.ts
435
524
  function createObservationNode(verbose = false, returnIntermediateSteps = false) {
436
525
  return async (state) => {
437
- const observations = state.observations || [];
438
- const thoughts = state.thoughts || [];
439
- const actions = state.actions || [];
526
+ const observations = state.observations;
527
+ const thoughts = state.thoughts;
528
+ const actions = state.actions;
440
529
  const iteration = state.iteration || 0;
441
- observationLogger.debug("Processing observations", {
530
+ debugIfVerbose(observationLogger, verbose, "Processing observations", {
442
531
  observationCount: observations.length,
443
532
  iteration
444
533
  });
445
534
  const recentObservations = observations.slice(-10);
446
535
  const latestActions = actions.slice(-10);
447
- const observationMessages = recentObservations.map((obs) => {
448
- const content = obs.error ? `Error: ${obs.error}` : typeof obs.result === "string" ? obs.result : JSON.stringify(obs.result, null, 2);
449
- return {
450
- role: "tool",
451
- content,
452
- name: latestActions.find((a) => a.id === obs.toolCallId)?.name,
453
- tool_call_id: obs.toolCallId
454
- // Include tool_call_id for proper ToolMessage construction
455
- };
456
- });
457
- const scratchpadEntries = returnIntermediateSteps ? [{
458
- step: state.iteration,
459
- thought: thoughts[thoughts.length - 1]?.content || "",
460
- action: latestActions.map((a) => `${a.name}(${JSON.stringify(a.arguments)})`).join(", "),
461
- observation: recentObservations.map((obs) => {
462
- if (obs.error) {
463
- return `Error: ${obs.error}`;
464
- }
465
- return typeof obs.result === "string" ? obs.result : JSON.stringify(obs.result);
466
- }).join("; "),
467
- timestamp: Date.now()
468
- }] : [];
469
- observationLogger.debug("Observation node complete", {
536
+ const actionNamesById = new Map(latestActions.map((action) => [action.id, action.name]));
537
+ const observationMessages = recentObservations.map((observation) => ({
538
+ role: "tool",
539
+ content: formatObservationContent(observation),
540
+ name: actionNamesById.get(observation.toolCallId),
541
+ tool_call_id: observation.toolCallId
542
+ }));
543
+ const scratchpadEntries = returnIntermediateSteps ? [
544
+ {
545
+ step: iteration,
546
+ thought: getLatestThought(thoughts),
547
+ action: formatActionSummary(latestActions),
548
+ observation: formatObservationSummary(recentObservations),
549
+ timestamp: Date.now()
550
+ }
551
+ ] : [];
552
+ debugIfVerbose(observationLogger, verbose, "Observation node complete", {
470
553
  iteration,
471
554
  scratchpadUpdated: returnIntermediateSteps,
472
555
  messageCount: observationMessages.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge/patterns",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
4
4
  "description": "Production-ready agent workflow patterns for TypeScript including ReAct and Planner-Executor, built on LangGraph.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -41,13 +41,13 @@
41
41
  "url": "https://github.com/TVScoundrel/agentforge/issues"
42
42
  },
43
43
  "dependencies": {
44
- "@agentforge/core": "0.15.9",
44
+ "@agentforge/core": "0.15.11",
45
45
  "@langchain/core": "^1.1.17",
46
46
  "@langchain/langgraph": "^1.1.2",
47
47
  "zod": "^3.23.8"
48
48
  },
49
49
  "devDependencies": {
50
- "@agentforge/testing": "0.15.9",
50
+ "@agentforge/testing": "0.15.11",
51
51
  "@eslint/js": "^9.17.0",
52
52
  "@types/node": "^22.10.2",
53
53
  "eslint": "^9.17.0",