@illuma-ai/agents 1.1.25 → 1.3.0

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 (272) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +20 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/spawnPath.cjs +104 -0
  4. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  5. package/dist/cjs/graphs/Graph.cjs +87 -31
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
  8. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
  10. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  11. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  12. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  14. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  15. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  16. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  17. package/dist/cjs/main.cjs +115 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/memory/citations.cjs +69 -0
  20. package/dist/cjs/memory/citations.cjs.map +1 -0
  21. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  22. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  23. package/dist/cjs/memory/constants.cjs +232 -0
  24. package/dist/cjs/memory/constants.cjs.map +1 -0
  25. package/dist/cjs/memory/embeddings.cjs +151 -0
  26. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  27. package/dist/cjs/memory/factory.cjs +95 -0
  28. package/dist/cjs/memory/factory.cjs.map +1 -0
  29. package/dist/cjs/memory/migrate.cjs +81 -0
  30. package/dist/cjs/memory/migrate.cjs.map +1 -0
  31. package/dist/cjs/memory/mmr.cjs +138 -0
  32. package/dist/cjs/memory/mmr.cjs.map +1 -0
  33. package/dist/cjs/memory/paths.cjs +217 -0
  34. package/dist/cjs/memory/paths.cjs.map +1 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  36. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  37. package/dist/cjs/memory/recallTracking.cjs +98 -0
  38. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  39. package/dist/cjs/memory/schema.sql +51 -0
  40. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  41. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  43. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  45. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  46. package/dist/cjs/run.cjs +16 -3
  47. package/dist/cjs/run.cjs.map +1 -1
  48. package/dist/cjs/stream.cjs +4 -4
  49. package/dist/cjs/stream.cjs.map +1 -1
  50. package/dist/cjs/tools/AskUser.cjs +6 -1
  51. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  52. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  53. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  54. package/dist/cjs/tools/ToolNode.cjs +127 -10
  55. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  56. package/dist/cjs/tools/approval/constants.cjs +2 -2
  57. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  58. package/dist/cjs/tools/memory/index.cjs +58 -0
  59. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  60. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  61. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  62. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  63. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  64. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  65. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  66. package/dist/cjs/tools/memory/shared.cjs +106 -0
  67. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  68. package/dist/cjs/types/graph.cjs.map +1 -1
  69. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  70. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  71. package/dist/cjs/utils/events.cjs +36 -4
  72. package/dist/cjs/utils/events.cjs.map +1 -1
  73. package/dist/cjs/utils/finishReasons.cjs +44 -0
  74. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  75. package/dist/cjs/utils/llm.cjs.map +1 -1
  76. package/dist/cjs/utils/logging.cjs +34 -0
  77. package/dist/cjs/utils/logging.cjs.map +1 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  79. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  80. package/dist/esm/agents/AgentContext.mjs +20 -3
  81. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  82. package/dist/esm/common/spawnPath.mjs +95 -0
  83. package/dist/esm/common/spawnPath.mjs.map +1 -0
  84. package/dist/esm/graphs/Graph.mjs +87 -31
  85. package/dist/esm/graphs/Graph.mjs.map +1 -1
  86. package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
  87. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
  88. package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
  89. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  90. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  91. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  92. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  93. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  94. package/dist/esm/llm/bedrock/index.mjs +4 -3
  95. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  96. package/dist/esm/main.mjs +21 -0
  97. package/dist/esm/main.mjs.map +1 -1
  98. package/dist/esm/memory/citations.mjs +64 -0
  99. package/dist/esm/memory/citations.mjs.map +1 -0
  100. package/dist/esm/memory/compositeBackend.mjs +58 -0
  101. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  102. package/dist/esm/memory/constants.mjs +198 -0
  103. package/dist/esm/memory/constants.mjs.map +1 -0
  104. package/dist/esm/memory/embeddings.mjs +148 -0
  105. package/dist/esm/memory/embeddings.mjs.map +1 -0
  106. package/dist/esm/memory/factory.mjs +93 -0
  107. package/dist/esm/memory/factory.mjs.map +1 -0
  108. package/dist/esm/memory/migrate.mjs +78 -0
  109. package/dist/esm/memory/migrate.mjs.map +1 -0
  110. package/dist/esm/memory/mmr.mjs +130 -0
  111. package/dist/esm/memory/mmr.mjs.map +1 -0
  112. package/dist/esm/memory/paths.mjs +207 -0
  113. package/dist/esm/memory/paths.mjs.map +1 -0
  114. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  115. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  116. package/dist/esm/memory/recallTracking.mjs +94 -0
  117. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  118. package/dist/esm/memory/schema.sql +51 -0
  119. package/dist/esm/memory/temporalDecay.mjs +110 -0
  120. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  121. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  122. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  123. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  124. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  125. package/dist/esm/run.mjs +16 -3
  126. package/dist/esm/run.mjs.map +1 -1
  127. package/dist/esm/stream.mjs +4 -4
  128. package/dist/esm/stream.mjs.map +1 -1
  129. package/dist/esm/tools/AskUser.mjs +6 -1
  130. package/dist/esm/tools/AskUser.mjs.map +1 -1
  131. package/dist/esm/tools/BrowserTools.mjs +1 -1
  132. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  133. package/dist/esm/tools/ToolNode.mjs +128 -11
  134. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  135. package/dist/esm/tools/approval/constants.mjs +2 -2
  136. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  137. package/dist/esm/tools/memory/index.mjs +46 -0
  138. package/dist/esm/tools/memory/index.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  140. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  142. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  143. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  144. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  145. package/dist/esm/tools/memory/shared.mjs +98 -0
  146. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  147. package/dist/esm/types/graph.mjs.map +1 -1
  148. package/dist/esm/utils/childAgentContext.mjs +237 -0
  149. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  150. package/dist/esm/utils/events.mjs +36 -5
  151. package/dist/esm/utils/events.mjs.map +1 -1
  152. package/dist/esm/utils/finishReasons.mjs +41 -0
  153. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  154. package/dist/esm/utils/llm.mjs.map +1 -1
  155. package/dist/esm/utils/logging.mjs +31 -0
  156. package/dist/esm/utils/logging.mjs.map +1 -0
  157. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  158. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  159. package/dist/types/common/index.d.ts +1 -0
  160. package/dist/types/common/spawnPath.d.ts +59 -0
  161. package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
  162. package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
  163. package/dist/types/graphs/index.d.ts +1 -0
  164. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  165. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  166. package/dist/types/index.d.ts +7 -0
  167. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  168. package/dist/types/memory/citations.d.ts +39 -0
  169. package/dist/types/memory/compositeBackend.d.ts +30 -0
  170. package/dist/types/memory/constants.d.ts +121 -0
  171. package/dist/types/memory/embeddings.d.ts +15 -0
  172. package/dist/types/memory/factory.d.ts +23 -0
  173. package/dist/types/memory/index.d.ts +21 -0
  174. package/dist/types/memory/migrate.d.ts +14 -0
  175. package/dist/types/memory/mmr.d.ts +50 -0
  176. package/dist/types/memory/paths.d.ts +107 -0
  177. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  178. package/dist/types/memory/recallTracking.d.ts +30 -0
  179. package/dist/types/memory/temporalDecay.d.ts +53 -0
  180. package/dist/types/memory/types.d.ts +182 -0
  181. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  182. package/dist/types/run.d.ts +1 -0
  183. package/dist/types/tools/AskUser.d.ts +1 -1
  184. package/dist/types/tools/BrowserTools.d.ts +2 -2
  185. package/dist/types/tools/approval/constants.d.ts +2 -2
  186. package/dist/types/tools/memory/index.d.ts +39 -0
  187. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  188. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  189. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  190. package/dist/types/tools/memory/shared.d.ts +106 -0
  191. package/dist/types/types/graph.d.ts +16 -3
  192. package/dist/types/utils/childAgentContext.d.ts +99 -0
  193. package/dist/types/utils/events.d.ts +21 -0
  194. package/dist/types/utils/finishReasons.d.ts +32 -0
  195. package/dist/types/utils/logging.d.ts +2 -0
  196. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  197. package/package.json +6 -4
  198. package/src/agents/AgentContext.ts +26 -3
  199. package/src/common/__tests__/enum.test.ts +4 -2
  200. package/src/common/__tests__/spawnPath.test.ts +110 -0
  201. package/src/common/index.ts +1 -0
  202. package/src/common/spawnPath.ts +101 -0
  203. package/src/graphs/Graph.ts +94 -43
  204. package/src/graphs/HandoffRegistry.ts +199 -0
  205. package/src/graphs/MultiAgentGraph.ts +694 -226
  206. package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
  207. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  208. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  209. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  210. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  211. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  212. package/src/graphs/index.ts +1 -0
  213. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  214. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  215. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  216. package/src/graphs/phases/flushLoop.ts +303 -0
  217. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  218. package/src/index.ts +30 -1
  219. package/src/llm/bedrock/index.ts +4 -5
  220. package/src/memory/__tests__/citations.test.ts +61 -0
  221. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  222. package/src/memory/__tests__/isolation.test.ts +206 -0
  223. package/src/memory/__tests__/mmr.test.ts +148 -0
  224. package/src/memory/__tests__/mockBackend.ts +161 -0
  225. package/src/memory/__tests__/paths.test.ts +168 -0
  226. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  227. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  228. package/src/memory/citations.ts +80 -0
  229. package/src/memory/compositeBackend.ts +99 -0
  230. package/src/memory/constants.ts +229 -0
  231. package/src/memory/embeddings.ts +188 -0
  232. package/src/memory/factory.ts +111 -0
  233. package/src/memory/index.ts +46 -0
  234. package/src/memory/migrate.ts +116 -0
  235. package/src/memory/mmr.ts +161 -0
  236. package/src/memory/paths.ts +258 -0
  237. package/src/memory/pgvectorStore.ts +324 -0
  238. package/src/memory/recallTracking.ts +127 -0
  239. package/src/memory/schema.sql +51 -0
  240. package/src/memory/temporalDecay.ts +134 -0
  241. package/src/memory/types.ts +185 -0
  242. package/src/nodes/ApprovalGateNode.ts +4 -10
  243. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  244. package/src/prompts/memoryFlushPrompt.ts +78 -0
  245. package/src/run.ts +17 -6
  246. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  247. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  248. package/src/specs/agent-handoffs.test.ts +8 -2
  249. package/src/stream.ts +4 -6
  250. package/src/tools/AskUser.ts +7 -2
  251. package/src/tools/BrowserTools.ts +3 -5
  252. package/src/tools/ToolNode.ts +150 -13
  253. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  254. package/src/tools/approval/__tests__/constants.test.ts +4 -4
  255. package/src/tools/approval/constants.ts +2 -2
  256. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  257. package/src/tools/memory/index.ts +96 -0
  258. package/src/tools/memory/memoryAppendTool.ts +101 -0
  259. package/src/tools/memory/memoryGetTool.ts +53 -0
  260. package/src/tools/memory/memorySearchTool.ts +80 -0
  261. package/src/tools/memory/shared.ts +169 -0
  262. package/src/tools/search/search.test.ts +6 -1
  263. package/src/types/graph.ts +16 -3
  264. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  265. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  266. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  267. package/src/utils/childAgentContext.ts +259 -0
  268. package/src/utils/events.ts +37 -4
  269. package/src/utils/finishReasons.ts +40 -0
  270. package/src/utils/llm.ts +0 -1
  271. package/src/utils/logging.ts +45 -8
  272. package/src/utils/toolCallNormalization.ts +271 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Child-agent context preparation utilities.
3
+ *
4
+ * When a parent agent invokes a child agent — via handoff, sequence edge,
5
+ * or scoped subgraph — the child cannot just receive `state.messages`
6
+ * verbatim. The parent's conversation contains tool_use/tool_result blocks
7
+ * that are (a) often incompatible with the child's tool registry, and
8
+ * (b) actively harmful to the child's ability to reason cleanly about its
9
+ * own task (noise → schema confusion → malformed tool_use).
10
+ *
11
+ * This module provides the two canonical strategies, extracted from
12
+ * `MultiAgentGraph` so they can be unit-tested in isolation and reused by
13
+ * future sub-agent orchestrators:
14
+ *
15
+ * 1. `prepareHandoffMessages` — "cleaned parent history"
16
+ * Used when the child still needs the orchestrator's context (it's
17
+ * the handoff target). Drops orphaned tool_use, compacts paired
18
+ * tool_use/tool_result into text summaries, and guarantees the tail
19
+ * is a HumanMessage so Bedrock/VertexAI won't reject the conversation
20
+ * with "assistant message prefill" errors.
21
+ *
22
+ * 2. `prepareIsolatedChildMessages` — "fresh session"
23
+ * Used for downstream sequence-node children inside a scoped subgraph.
24
+ * The child sees only the original user request plus a synthetic
25
+ * HumanMessage summarizing the upstream agent's final text output and
26
+ * directing the child to act. Raw upstream tool_use/tool_result blocks
27
+ * are discarded.
28
+ *
29
+ * Both helpers are pure functions over message arrays — no I/O, no
30
+ * LangGraph coupling — so they can be exercised by unit tests with
31
+ * synthetic message fixtures.
32
+ */
33
+
34
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
35
+ import type { AIMessageChunk, BaseMessage } from '@langchain/core/messages';
36
+
37
+ /* -------------------------------------------------------------------------- */
38
+ /* Prompt template constants (kept outside the functions for reuse/tuning) */
39
+ /* -------------------------------------------------------------------------- */
40
+
41
+ /**
42
+ * Prefix injected in front of a trailing AIMessage when we flip it to a
43
+ * HumanMessage to satisfy provider "last message must be user" rules.
44
+ */
45
+ export const HANDOFF_TAIL_CONTEXT_PREFIX = '[Context from orchestrator]: ';
46
+
47
+ /**
48
+ * Directive task-framing wrapper for downstream scoped-subgraph children.
49
+ *
50
+ * Design notes — each line is load-bearing:
51
+ * - "Prior step output" names the upstream role without leaking the
52
+ * agent's internal id.
53
+ * - "You MUST now perform..." replaces ambiguity with obligation.
54
+ * - "system instructions" references the agent's stored system prompt
55
+ * as the source of task definition — so operators can tune behavior
56
+ * via data, not code.
57
+ * - The tool-first clause prevents small/fast models from stalling on a
58
+ * text-only acknowledgement when a tool action is expected.
59
+ */
60
+ export function buildIsolatedChildPrompt(upstreamText: string): string {
61
+ return (
62
+ '## Prior step output\n\n' +
63
+ upstreamText +
64
+ '\n\n---\n\n' +
65
+ '## Your task\n\n' +
66
+ 'The previous step in this workflow has completed. You MUST now ' +
67
+ 'perform your own task as defined in your system instructions, ' +
68
+ "using the prior step's output as input where relevant.\n\n" +
69
+ 'If your task requires calling a tool, call it directly — do not ' +
70
+ 'ask for clarification and do not produce a text-only response when ' +
71
+ 'a tool action is expected.'
72
+ );
73
+ }
74
+
75
+ /* -------------------------------------------------------------------------- */
76
+ /* Internal helpers */
77
+ /* -------------------------------------------------------------------------- */
78
+
79
+ /**
80
+ * Extract concatenated text content from an AI message's content field.
81
+ * Handles both the string shape (OpenAI/plain) and the array-of-blocks
82
+ * shape (Anthropic/Bedrock).
83
+ */
84
+ function extractAIText(msg: AIMessage | AIMessageChunk): string {
85
+ const content = msg.content;
86
+ if (typeof content === 'string') return content;
87
+ if (!Array.isArray(content)) return '';
88
+ return (content as Array<{ type?: string; text?: string }>)
89
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
90
+ .map((b) => b.text ?? '')
91
+ .join('\n');
92
+ }
93
+
94
+ /* -------------------------------------------------------------------------- */
95
+ /* Strategy 1: cleaned parent history (handoff target / root subgraph) */
96
+ /* -------------------------------------------------------------------------- */
97
+
98
+ /**
99
+ * Prepare messages for a handoff child agent.
100
+ *
101
+ * Handles two problems that break Bedrock/Anthropic conversations:
102
+ *
103
+ * 1. **Orphaned tool_use**: The parent's AI message contains a `tool_use`
104
+ * block for the handoff tool itself, with no matching `tool_result`.
105
+ * Providers (Bedrock/Anthropic) reject this.
106
+ *
107
+ * 2. **Paired tool_use/tool_result in history**: The child may not have
108
+ * the same tools as the parent. Bedrock requires `toolConfig` when any
109
+ * tool_use/tool_result blocks exist in the history. Compacting these
110
+ * into text summaries avoids the requirement and reduces context bloat.
111
+ *
112
+ * Also ensures the tail is a HumanMessage — some providers reject a
113
+ * conversation that ends with an assistant message.
114
+ *
115
+ * @param messages - Current state messages from the parent
116
+ * @returns A sanitized copy, safe to pass to any provider as the child's
117
+ * input regardless of which tools the child has registered.
118
+ */
119
+ export function prepareHandoffMessages(messages: BaseMessage[]): BaseMessage[] {
120
+ if (messages.length === 0) return messages;
121
+
122
+ /** Collect tool_result IDs so we know which tool_use blocks are paired */
123
+ const pairedToolCallIds = new Set<string>();
124
+ for (const msg of messages) {
125
+ if (msg.getType() === 'tool') {
126
+ const tm = msg as ToolMessage;
127
+ if (tm.tool_call_id) pairedToolCallIds.add(tm.tool_call_id);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Pass 1: Drop all ToolMessages (paired ones are compacted in pass 2),
133
+ * rewrite AI messages with tool_calls into plain-text summaries, leave
134
+ * other messages untouched.
135
+ */
136
+ const cleaned: BaseMessage[] = [];
137
+ for (const msg of messages) {
138
+ if (msg.getType() === 'tool') continue;
139
+
140
+ if (msg.getType() !== 'ai') {
141
+ cleaned.push(msg);
142
+ continue;
143
+ }
144
+
145
+ const aiMsg = msg as AIMessage | AIMessageChunk;
146
+ const toolCalls = aiMsg.tool_calls ?? [];
147
+ if (toolCalls.length === 0) {
148
+ cleaned.push(msg);
149
+ continue;
150
+ }
151
+
152
+ const textContent = extractAIText(aiMsg);
153
+
154
+ const toolSummaries: string[] = [];
155
+ for (const tc of toolCalls) {
156
+ if (tc.id != null && pairedToolCallIds.has(tc.id)) {
157
+ const toolResult = messages.find(
158
+ (m) =>
159
+ m.getType() === 'tool' && (m as ToolMessage).tool_call_id === tc.id
160
+ ) as ToolMessage | undefined;
161
+ const resultContent = toolResult
162
+ ? typeof toolResult.content === 'string'
163
+ ? toolResult.content.slice(0, 500)
164
+ : '[complex result]'
165
+ : '[no result]';
166
+ toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
167
+ }
168
+ // Orphaned tool_use blocks (no matching result) are silently dropped.
169
+ }
170
+
171
+ const parts = [textContent, ...toolSummaries].filter(Boolean);
172
+ if (parts.length > 0) {
173
+ cleaned.push(
174
+ new AIMessage({ content: parts.join('\n\n'), id: aiMsg.id })
175
+ );
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Ensure messages end with a HumanMessage. After stripping tool artifacts
181
+ * the tail may be an AIMessage, which Bedrock/VertexAI reject. Convert it
182
+ * to a HumanMessage preserving whatever text content was present, or drop
183
+ * it entirely if empty.
184
+ */
185
+ if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
186
+ const lastAI = cleaned[cleaned.length - 1];
187
+ const content = typeof lastAI.content === 'string' ? lastAI.content : '';
188
+ if (content.trim()) {
189
+ cleaned[cleaned.length - 1] = new HumanMessage(
190
+ `${HANDOFF_TAIL_CONTEXT_PREFIX}${content}`
191
+ );
192
+ } else {
193
+ cleaned.pop();
194
+ }
195
+ }
196
+
197
+ return cleaned;
198
+ }
199
+
200
+ /* -------------------------------------------------------------------------- */
201
+ /* Strategy 2: isolated fresh session (downstream scoped-subgraph child) */
202
+ /* -------------------------------------------------------------------------- */
203
+
204
+ /**
205
+ * Build an ISOLATED message context for a downstream scoped-subgraph node.
206
+ *
207
+ * Unlike `prepareHandoffMessages` (which cleans up tool_use artifacts but
208
+ * preserves most of the parent history), this helper produces a fresh
209
+ * minimal context containing only:
210
+ *
211
+ * 1. The original user request (first HumanMessage in the history)
212
+ * 2. A synthetic HumanMessage summarizing the upstream agent's final
213
+ * text output and directing the downstream agent to act on it
214
+ *
215
+ * Tool_use / tool_result blocks from the upstream agent are discarded —
216
+ * the downstream agent shouldn't reason about how the upstream agent did
217
+ * its work, only about the result.
218
+ *
219
+ * This "fresh subagent session" pattern is the primary defense against
220
+ * schema confusion / malformed tool_use JSON that occurs when downstream
221
+ * models see a noisy upstream conversation.
222
+ *
223
+ * Defensive fallback: if the messages array contains neither a user
224
+ * message nor a non-empty upstream AI message, return the input unchanged
225
+ * so the caller still has something to invoke on. This only matters for
226
+ * malformed state fixtures in tests.
227
+ */
228
+ export function prepareIsolatedChildMessages(
229
+ messages: BaseMessage[]
230
+ ): BaseMessage[] {
231
+ if (messages.length === 0) return messages;
232
+
233
+ /** First HumanMessage is the original user request */
234
+ const originalUser = messages.find((m) => m.getType() === 'human');
235
+
236
+ /** Most recent AIMessage with non-empty text content */
237
+ let upstreamText = '';
238
+ for (let i = messages.length - 1; i >= 0; i--) {
239
+ const msg = messages[i];
240
+ if (msg.getType() !== 'ai') continue;
241
+ const text = extractAIText(msg as AIMessage | AIMessageChunk);
242
+ if (text.trim()) {
243
+ upstreamText = text;
244
+ break;
245
+ }
246
+ }
247
+
248
+ const result: BaseMessage[] = [];
249
+ if (originalUser) result.push(originalUser);
250
+
251
+ if (upstreamText.trim()) {
252
+ result.push(new HumanMessage(buildIsolatedChildPrompt(upstreamText)));
253
+ } else if (result.length === 0) {
254
+ /** Defensive: nothing to isolate — fall back to raw messages */
255
+ return messages;
256
+ }
257
+
258
+ return result;
259
+ }
@@ -1,11 +1,42 @@
1
1
  /* eslint-disable no-console */
2
2
  // src/utils/events.ts
3
3
  import { dispatchCustomEvent } from '@langchain/core/callbacks/dispatch';
4
+ import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
4
5
  import type { RunnableConfig } from '@langchain/core/runnables';
5
6
 
7
+ /**
8
+ * Returns the RunnableConfig currently active in LangChain's AsyncLocalStorage,
9
+ * or undefined if none is installed. This is the per-async-branch config that
10
+ * LangGraph installs when entering a node — it carries the correct
11
+ * `metadata.spawnKey` for child subgraph invocations inside `Promise.all`
12
+ * parallel handoffs.
13
+ *
14
+ * Prefer this over any Graph-instance-cached config (e.g. `this.config`)
15
+ * when dispatching events from code that may run concurrently across multiple
16
+ * child subgraphs. An instance-level cache is shared state and races between
17
+ * siblings — the last child to enter wins, so events fire with the wrong
18
+ * child's metadata and the backend routes them to the wrong spawnKey.
19
+ */
20
+ export function getCurrentRunnableConfig(): RunnableConfig | undefined {
21
+ try {
22
+ return AsyncLocalStorageProviderSingleton.getInstance().getStore() as
23
+ | RunnableConfig
24
+ | undefined;
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }
29
+
6
30
  /**
7
31
  * Safely dispatches a custom event and properly awaits it to avoid
8
32
  * race conditions where events are dispatched after run cleanup.
33
+ *
34
+ * **Parallel-handoff correctness:** callers should prefer passing
35
+ * `undefined` (or the per-node runtime config). When `config` is omitted,
36
+ * LangChain's `ensureConfig` reads the current RunnableConfig from
37
+ * AsyncLocalStorage, which is correctly isolated per async branch under
38
+ * `Promise.all`. Passing a stale instance-cached config overrides that
39
+ * implicit config's metadata and cross-contaminates parallel children.
9
40
  */
10
41
  export async function safeDispatchCustomEvent(
11
42
  event: string,
@@ -13,7 +44,11 @@ export async function safeDispatchCustomEvent(
13
44
  config?: RunnableConfig
14
45
  ): Promise<void> {
15
46
  try {
16
- await dispatchCustomEvent(event, payload, config);
47
+ // If the caller did not pass a config, fall back to the current
48
+ // AsyncLocalStorage-resident runnable config so nested Promise.all
49
+ // branches each use their own metadata.
50
+ const effectiveConfig = config ?? getCurrentRunnableConfig();
51
+ await dispatchCustomEvent(event, payload, effectiveConfig);
17
52
  } catch (e) {
18
53
  // Check if this is the known EventStreamCallbackHandler error
19
54
  if (
@@ -21,9 +56,7 @@ export async function safeDispatchCustomEvent(
21
56
  e.message.includes('handleCustomEvent: Run ID') &&
22
57
  e.message.includes('not found in run map')
23
58
  ) {
24
- // Suppress this specific error - it's expected during parallel execution
25
- // when EventStreamCallbackHandler loses track of run IDs
26
- // console.debug('Suppressed error dispatching custom event:', e);
59
+ // Suppress expected during parallel/async execution
27
60
  return;
28
61
  }
29
62
  // Log other errors
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Finish-reason constants and helpers.
3
+ *
4
+ * LLM providers emit different keys (`finish_reason`, `stop_reason`,
5
+ * `stopReason`, `finishReason`) and different values for the same concept.
6
+ * This module is the single source of truth for detecting *truncation* —
7
+ * i.e., the model hit its output budget and the response is incomplete.
8
+ *
9
+ * Used by:
10
+ * - `Graph.ts` — sticky `lastFinishReason` across inner subgraph invokes,
11
+ * so host's continuation retry can detect truncation that happens
12
+ * inside a scoped-subgraph child node.
13
+ * - Any future continuation / auto-retry logic added to agents.
14
+ *
15
+ * Kept as a small utils module (instead of inline in Graph.ts) so that
16
+ * additional callers — e.g., structured output recovery, sub-agent
17
+ * orchestrators — can reuse the exact same detection logic without drift.
18
+ */
19
+
20
+ /**
21
+ * Canonical set of finish-reason strings that mean "output was truncated".
22
+ *
23
+ * Covers:
24
+ * - `max_tokens` — Anthropic direct API, Bedrock
25
+ * - `length` — OpenAI/Azure
26
+ * - `MAX_TOKENS` — VertexAI/Google (uppercased enum)
27
+ */
28
+ export const TRUNCATION_FINISH_REASONS: ReadonlySet<string> = new Set([
29
+ 'max_tokens',
30
+ 'length',
31
+ 'MAX_TOKENS',
32
+ ]);
33
+
34
+ /**
35
+ * @returns true when the given finish/stop reason indicates the response
36
+ * was cut short by the output token budget.
37
+ */
38
+ export function isTruncationReason(reason: string | undefined | null): boolean {
39
+ return reason != null && TRUNCATION_FINISH_REASONS.has(reason);
40
+ }
package/src/utils/llm.ts CHANGED
@@ -24,4 +24,3 @@ export function isGoogleLike(provider?: string | Providers): boolean {
24
24
  provider
25
25
  );
26
26
  }
27
-
@@ -2,6 +2,33 @@
2
2
  import fs from 'fs';
3
3
  import util from 'util';
4
4
 
5
+ /**
6
+ * Multi-agent debug gate. Controls chatty graph/handoff/transfer/sequence
7
+ * trace logs emitted from `MultiAgentGraph` and `Graph`. Off by default so the
8
+ * package stays quiet when embedded in host apps. Flip to
9
+ * `DEBUG_MULTIAGENT=true` in the host env to re-enable for testing.
10
+ *
11
+ * Exported as functions so runtime toggling via env reload works; the check
12
+ * is cheap (string equality) and only runs when a log site fires.
13
+ */
14
+ function isMultiAgentDebugEnabled(): boolean {
15
+ return process.env.DEBUG_MULTIAGENT === 'true';
16
+ }
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ export const mlog = (...args: any[]): void => {
20
+ if (isMultiAgentDebugEnabled()) {
21
+ console.debug(...args);
22
+ }
23
+ };
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ export const mwarn = (...args: any[]): void => {
27
+ if (isMultiAgentDebugEnabled()) {
28
+ console.warn(...args);
29
+ }
30
+ };
31
+
5
32
  export function setupLogging(logFileName: string): void {
6
33
  const logFile = fs.createWriteStream(logFileName, { flags: 'a' });
7
34
 
@@ -10,39 +37,49 @@ export function setupLogging(logFileName: string): void {
10
37
  const originalStdoutWrite = process.stdout.write;
11
38
  const originalStderrWrite = process.stderr.write;
12
39
 
13
- console.log = function(...args): void {
40
+ console.log = function (...args): void {
14
41
  logFile.write(util.format.apply(null, args) + ' ');
15
42
  originalConsoleLog.apply(console, args);
16
43
  };
17
44
 
18
- console.error = function(...args): void {
45
+ console.error = function (...args): void {
19
46
  logFile.write(util.format.apply(null, args) + ' ');
20
47
  originalConsoleError.apply(console, args);
21
48
  };
22
49
 
23
- process.stdout.write = function(
50
+ process.stdout.write = function (
24
51
  buffer: Uint8Array | string,
25
52
  cb?: ((err?: Error) => void) | string,
26
- fd?: ((err?: Error) => void)
53
+ fd?: (err?: Error) => void
27
54
  ): boolean {
28
55
  if (typeof buffer === 'string') {
29
56
  logFile.write(buffer);
30
57
  } else {
31
58
  logFile.write(buffer.toString());
32
59
  }
33
- return originalStdoutWrite.call(process.stdout, buffer, cb as BufferEncoding | undefined, fd);
60
+ return originalStdoutWrite.call(
61
+ process.stdout,
62
+ buffer,
63
+ cb as BufferEncoding | undefined,
64
+ fd
65
+ );
34
66
  };
35
67
 
36
- process.stderr.write = function(
68
+ process.stderr.write = function (
37
69
  buffer: Uint8Array | string,
38
70
  cb?: ((err?: Error) => void) | string,
39
- fd?: ((err?: Error) => void)
71
+ fd?: (err?: Error) => void
40
72
  ): boolean {
41
73
  if (typeof buffer === 'string') {
42
74
  logFile.write(buffer);
43
75
  } else {
44
76
  logFile.write(buffer.toString());
45
77
  }
46
- return originalStderrWrite.call(process.stderr, buffer, cb as BufferEncoding | undefined, fd);
78
+ return originalStderrWrite.call(
79
+ process.stderr,
80
+ buffer,
81
+ cb as BufferEncoding | undefined,
82
+ fd
83
+ );
47
84
  };
48
85
  }