@illuma-ai/agents 1.1.28 → 1.3.1

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.map +1 -1
  2. package/dist/cjs/common/spawnPath.cjs +104 -0
  3. package/dist/cjs/common/spawnPath.cjs.map +1 -0
  4. package/dist/cjs/graphs/Graph.cjs +89 -45
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
  7. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
  9. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  10. package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
  11. package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
  12. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
  13. package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
  14. package/dist/cjs/llm/bedrock/index.cjs +4 -3
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +117 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/memory/citations.cjs +69 -0
  19. package/dist/cjs/memory/citations.cjs.map +1 -0
  20. package/dist/cjs/memory/compositeBackend.cjs +60 -0
  21. package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
  22. package/dist/cjs/memory/constants.cjs +232 -0
  23. package/dist/cjs/memory/constants.cjs.map +1 -0
  24. package/dist/cjs/memory/embeddings.cjs +151 -0
  25. package/dist/cjs/memory/embeddings.cjs.map +1 -0
  26. package/dist/cjs/memory/factory.cjs +95 -0
  27. package/dist/cjs/memory/factory.cjs.map +1 -0
  28. package/dist/cjs/memory/migrate.cjs +81 -0
  29. package/dist/cjs/memory/migrate.cjs.map +1 -0
  30. package/dist/cjs/memory/mmr.cjs +138 -0
  31. package/dist/cjs/memory/mmr.cjs.map +1 -0
  32. package/dist/cjs/memory/paths.cjs +217 -0
  33. package/dist/cjs/memory/paths.cjs.map +1 -0
  34. package/dist/cjs/memory/pgvectorStore.cjs +225 -0
  35. package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
  36. package/dist/cjs/memory/recallTracking.cjs +98 -0
  37. package/dist/cjs/memory/recallTracking.cjs.map +1 -0
  38. package/dist/cjs/memory/schema.sql +51 -0
  39. package/dist/cjs/memory/temporalDecay.cjs +118 -0
  40. package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
  41. package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
  42. package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
  43. package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
  44. package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
  45. package/dist/cjs/run.cjs +16 -3
  46. package/dist/cjs/run.cjs.map +1 -1
  47. package/dist/cjs/tools/AskUser.cjs +6 -1
  48. package/dist/cjs/tools/AskUser.cjs.map +1 -1
  49. package/dist/cjs/tools/BrowserTools.cjs +1 -1
  50. package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
  51. package/dist/cjs/tools/ToolNode.cjs +127 -10
  52. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  53. package/dist/cjs/tools/approval/constants.cjs +2 -2
  54. package/dist/cjs/tools/approval/constants.cjs.map +1 -1
  55. package/dist/cjs/tools/memory/index.cjs +58 -0
  56. package/dist/cjs/tools/memory/index.cjs.map +1 -0
  57. package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
  58. package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
  59. package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
  60. package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
  61. package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
  62. package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
  63. package/dist/cjs/tools/memory/shared.cjs +106 -0
  64. package/dist/cjs/tools/memory/shared.cjs.map +1 -0
  65. package/dist/cjs/types/graph.cjs.map +1 -1
  66. package/dist/cjs/utils/childAgentContext.cjs +242 -0
  67. package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
  68. package/dist/cjs/utils/errors.cjs +113 -0
  69. package/dist/cjs/utils/errors.cjs.map +1 -0
  70. package/dist/cjs/utils/events.cjs +36 -7
  71. package/dist/cjs/utils/events.cjs.map +1 -1
  72. package/dist/cjs/utils/finishReasons.cjs +44 -0
  73. package/dist/cjs/utils/finishReasons.cjs.map +1 -0
  74. package/dist/cjs/utils/llm.cjs.map +1 -1
  75. package/dist/cjs/utils/logging.cjs +34 -0
  76. package/dist/cjs/utils/logging.cjs.map +1 -0
  77. package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
  78. package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
  79. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  80. package/dist/esm/common/spawnPath.mjs +95 -0
  81. package/dist/esm/common/spawnPath.mjs.map +1 -0
  82. package/dist/esm/graphs/Graph.mjs +89 -45
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
  85. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
  86. package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
  87. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  88. package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
  89. package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
  90. package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
  91. package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
  92. package/dist/esm/llm/bedrock/index.mjs +4 -3
  93. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  94. package/dist/esm/main.mjs +21 -0
  95. package/dist/esm/main.mjs.map +1 -1
  96. package/dist/esm/memory/citations.mjs +64 -0
  97. package/dist/esm/memory/citations.mjs.map +1 -0
  98. package/dist/esm/memory/compositeBackend.mjs +58 -0
  99. package/dist/esm/memory/compositeBackend.mjs.map +1 -0
  100. package/dist/esm/memory/constants.mjs +198 -0
  101. package/dist/esm/memory/constants.mjs.map +1 -0
  102. package/dist/esm/memory/embeddings.mjs +148 -0
  103. package/dist/esm/memory/embeddings.mjs.map +1 -0
  104. package/dist/esm/memory/factory.mjs +93 -0
  105. package/dist/esm/memory/factory.mjs.map +1 -0
  106. package/dist/esm/memory/migrate.mjs +78 -0
  107. package/dist/esm/memory/migrate.mjs.map +1 -0
  108. package/dist/esm/memory/mmr.mjs +130 -0
  109. package/dist/esm/memory/mmr.mjs.map +1 -0
  110. package/dist/esm/memory/paths.mjs +207 -0
  111. package/dist/esm/memory/paths.mjs.map +1 -0
  112. package/dist/esm/memory/pgvectorStore.mjs +223 -0
  113. package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
  114. package/dist/esm/memory/recallTracking.mjs +94 -0
  115. package/dist/esm/memory/recallTracking.mjs.map +1 -0
  116. package/dist/esm/memory/schema.sql +51 -0
  117. package/dist/esm/memory/temporalDecay.mjs +110 -0
  118. package/dist/esm/memory/temporalDecay.mjs.map +1 -0
  119. package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
  120. package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
  121. package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
  122. package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
  123. package/dist/esm/run.mjs +16 -3
  124. package/dist/esm/run.mjs.map +1 -1
  125. package/dist/esm/tools/AskUser.mjs +6 -1
  126. package/dist/esm/tools/AskUser.mjs.map +1 -1
  127. package/dist/esm/tools/BrowserTools.mjs +1 -1
  128. package/dist/esm/tools/BrowserTools.mjs.map +1 -1
  129. package/dist/esm/tools/ToolNode.mjs +128 -11
  130. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  131. package/dist/esm/tools/approval/constants.mjs +2 -2
  132. package/dist/esm/tools/approval/constants.mjs.map +1 -1
  133. package/dist/esm/tools/memory/index.mjs +46 -0
  134. package/dist/esm/tools/memory/index.mjs.map +1 -0
  135. package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
  136. package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
  137. package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
  138. package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
  139. package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
  140. package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
  141. package/dist/esm/tools/memory/shared.mjs +98 -0
  142. package/dist/esm/tools/memory/shared.mjs.map +1 -0
  143. package/dist/esm/types/graph.mjs.map +1 -1
  144. package/dist/esm/utils/childAgentContext.mjs +237 -0
  145. package/dist/esm/utils/childAgentContext.mjs.map +1 -0
  146. package/dist/esm/utils/errors.mjs +109 -0
  147. package/dist/esm/utils/errors.mjs.map +1 -0
  148. package/dist/esm/utils/events.mjs +36 -8
  149. package/dist/esm/utils/events.mjs.map +1 -1
  150. package/dist/esm/utils/finishReasons.mjs +41 -0
  151. package/dist/esm/utils/finishReasons.mjs.map +1 -0
  152. package/dist/esm/utils/llm.mjs.map +1 -1
  153. package/dist/esm/utils/logging.mjs +31 -0
  154. package/dist/esm/utils/logging.mjs.map +1 -0
  155. package/dist/esm/utils/toolCallNormalization.mjs +247 -0
  156. package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
  157. package/dist/types/common/index.d.ts +1 -0
  158. package/dist/types/common/spawnPath.d.ts +59 -0
  159. package/dist/types/graphs/HandoffRegistry.d.ts +24 -7
  160. package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
  161. package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
  162. package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
  163. package/dist/types/index.d.ts +7 -0
  164. package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
  165. package/dist/types/memory/citations.d.ts +39 -0
  166. package/dist/types/memory/compositeBackend.d.ts +30 -0
  167. package/dist/types/memory/constants.d.ts +121 -0
  168. package/dist/types/memory/embeddings.d.ts +15 -0
  169. package/dist/types/memory/factory.d.ts +23 -0
  170. package/dist/types/memory/index.d.ts +21 -0
  171. package/dist/types/memory/migrate.d.ts +14 -0
  172. package/dist/types/memory/mmr.d.ts +50 -0
  173. package/dist/types/memory/paths.d.ts +107 -0
  174. package/dist/types/memory/pgvectorStore.d.ts +56 -0
  175. package/dist/types/memory/recallTracking.d.ts +30 -0
  176. package/dist/types/memory/temporalDecay.d.ts +53 -0
  177. package/dist/types/memory/types.d.ts +182 -0
  178. package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
  179. package/dist/types/run.d.ts +1 -0
  180. package/dist/types/tools/AskUser.d.ts +1 -1
  181. package/dist/types/tools/BrowserTools.d.ts +2 -2
  182. package/dist/types/tools/approval/constants.d.ts +2 -2
  183. package/dist/types/tools/memory/index.d.ts +39 -0
  184. package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
  185. package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
  186. package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
  187. package/dist/types/tools/memory/shared.d.ts +106 -0
  188. package/dist/types/types/graph.d.ts +10 -3
  189. package/dist/types/utils/childAgentContext.d.ts +99 -0
  190. package/dist/types/utils/errors.d.ts +37 -0
  191. package/dist/types/utils/events.d.ts +21 -0
  192. package/dist/types/utils/finishReasons.d.ts +32 -0
  193. package/dist/types/utils/index.d.ts +1 -0
  194. package/dist/types/utils/logging.d.ts +2 -0
  195. package/dist/types/utils/toolCallNormalization.d.ts +44 -0
  196. package/package.json +6 -4
  197. package/src/agents/AgentContext.ts +12 -4
  198. package/src/common/__tests__/enum.test.ts +4 -2
  199. package/src/common/__tests__/spawnPath.test.ts +110 -0
  200. package/src/common/index.ts +1 -0
  201. package/src/common/spawnPath.ts +101 -0
  202. package/src/graphs/Graph.ts +95 -61
  203. package/src/graphs/HandoffRegistry.ts +48 -17
  204. package/src/graphs/MultiAgentGraph.ts +588 -327
  205. package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
  206. package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
  207. package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
  208. package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
  209. package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
  210. package/src/graphs/contextManagement.e2e.test.ts +1 -1
  211. package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
  212. package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
  213. package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
  214. package/src/graphs/phases/flushLoop.ts +303 -0
  215. package/src/graphs/phases/memoryFlushPhase.ts +209 -0
  216. package/src/index.ts +30 -1
  217. package/src/llm/bedrock/index.ts +4 -5
  218. package/src/memory/__tests__/citations.test.ts +61 -0
  219. package/src/memory/__tests__/compositeBackend.test.ts +79 -0
  220. package/src/memory/__tests__/isolation.test.ts +206 -0
  221. package/src/memory/__tests__/mmr.test.ts +148 -0
  222. package/src/memory/__tests__/mockBackend.ts +161 -0
  223. package/src/memory/__tests__/paths.test.ts +168 -0
  224. package/src/memory/__tests__/recallTracking.test.ts +96 -0
  225. package/src/memory/__tests__/temporalDecay.test.ts +151 -0
  226. package/src/memory/citations.ts +80 -0
  227. package/src/memory/compositeBackend.ts +99 -0
  228. package/src/memory/constants.ts +229 -0
  229. package/src/memory/embeddings.ts +188 -0
  230. package/src/memory/factory.ts +111 -0
  231. package/src/memory/index.ts +46 -0
  232. package/src/memory/migrate.ts +116 -0
  233. package/src/memory/mmr.ts +161 -0
  234. package/src/memory/paths.ts +258 -0
  235. package/src/memory/pgvectorStore.ts +324 -0
  236. package/src/memory/recallTracking.ts +127 -0
  237. package/src/memory/schema.sql +51 -0
  238. package/src/memory/temporalDecay.ts +134 -0
  239. package/src/memory/types.ts +185 -0
  240. package/src/nodes/ApprovalGateNode.ts +4 -10
  241. package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
  242. package/src/prompts/memoryFlushPrompt.ts +78 -0
  243. package/src/run.ts +17 -6
  244. package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
  245. package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
  246. package/src/specs/agent-handoffs.test.ts +8 -2
  247. package/src/tools/AskUser.ts +7 -2
  248. package/src/tools/BrowserTools.ts +3 -5
  249. package/src/tools/ToolNode.ts +150 -13
  250. package/src/tools/__tests__/ToolApproval.test.ts +22 -9
  251. package/src/tools/approval/__tests__/constants.test.ts +1 -1
  252. package/src/tools/approval/constants.ts +2 -2
  253. package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
  254. package/src/tools/memory/index.ts +96 -0
  255. package/src/tools/memory/memoryAppendTool.ts +101 -0
  256. package/src/tools/memory/memoryGetTool.ts +53 -0
  257. package/src/tools/memory/memorySearchTool.ts +80 -0
  258. package/src/tools/memory/shared.ts +169 -0
  259. package/src/tools/search/search.test.ts +6 -1
  260. package/src/types/graph.ts +10 -3
  261. package/src/utils/__tests__/childAgentContext.test.ts +217 -0
  262. package/src/utils/__tests__/errors.test.ts +136 -0
  263. package/src/utils/__tests__/finishReasons.test.ts +55 -0
  264. package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
  265. package/src/utils/childAgentContext.ts +259 -0
  266. package/src/utils/errors.ts +115 -0
  267. package/src/utils/events.ts +37 -7
  268. package/src/utils/finishReasons.ts +40 -0
  269. package/src/utils/index.ts +1 -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,115 @@
1
+ /**
2
+ * Context-overflow error detection helpers.
3
+ *
4
+ * Provider error messages vary — Anthropic returns "prompt is too long",
5
+ * OpenAI returns "context_length_exceeded", Bedrock returns "Input is too
6
+ * long", Google returns a size-limit phrase. This module centralises the
7
+ * phrase list so the agent graph's emergency-prune retry can classify
8
+ * errors consistently instead of duplicating inline substring matches at
9
+ * each call site.
10
+ *
11
+ * The strict check (`isContextOverflowError`) matches only known phrases.
12
+ * The loose check (`isLikelyContextOverflowError`) also matches a heuristic
13
+ * regex for providers we haven't explicitly catalogued. Both filter out
14
+ * false positives (rate-limit, auth, quota, billing) that might otherwise
15
+ * trigger an unnecessary prune retry.
16
+ */
17
+
18
+ const CONTEXT_OVERFLOW_PHRASES = [
19
+ 'request_too_large',
20
+ 'context length exceeded',
21
+ 'maximum context length',
22
+ 'prompt is too long',
23
+ 'exceeds model context window',
24
+ 'exceeds the model',
25
+ 'too large for model',
26
+ 'context_length_exceeded',
27
+ 'max_tokens',
28
+ 'token limit',
29
+ 'input too long',
30
+ 'input is too long',
31
+ 'payload too large',
32
+ 'content_too_large',
33
+ ] as const;
34
+
35
+ const CONTEXT_OVERFLOW_HINT_RE =
36
+ /413|too large|too long|context.*exceed|exceed.*context|token.*limit|limit.*token|prompt.*size|size.*limit|maximum.*length|length.*maximum/i;
37
+
38
+ const FALSE_POSITIVE_RE =
39
+ /rate.?limit|too many requests|quota|billing|auth|permission|forbidden/i;
40
+
41
+ /**
42
+ * Extracts a human-readable error message from an unknown error value.
43
+ * Walks common shapes: strings, Error instances, `{ message }`,
44
+ * `{ error: string }`, `{ error: { message } }`. Falls back to
45
+ * JSON.stringify or String() so callers never have to null-check.
46
+ */
47
+ export function extractErrorMessage(error: unknown): string {
48
+ if (error == null) {
49
+ return '';
50
+ }
51
+ if (typeof error === 'string') {
52
+ return error;
53
+ }
54
+ if (error instanceof Error) {
55
+ return error.message;
56
+ }
57
+ if (typeof error === 'object') {
58
+ const record = error as Record<string, unknown>;
59
+ if (typeof record.message === 'string') {
60
+ return record.message;
61
+ }
62
+ if (typeof record.error === 'string') {
63
+ return record.error;
64
+ }
65
+ if (
66
+ typeof record.error === 'object' &&
67
+ record.error != null &&
68
+ typeof (record.error as Record<string, unknown>).message === 'string'
69
+ ) {
70
+ return (record.error as Record<string, unknown>).message as string;
71
+ }
72
+ }
73
+ try {
74
+ return JSON.stringify(error);
75
+ } catch {
76
+ return String(error);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Strict check: returns true only for known, unambiguous context-overflow
82
+ * phrases. Use when the recovery action is expensive (full prune + retry)
83
+ * and false positives are undesirable.
84
+ */
85
+ export function isContextOverflowError(errorMessage?: string): boolean {
86
+ if (!errorMessage) {
87
+ return false;
88
+ }
89
+ const lower = errorMessage.toLowerCase();
90
+ if (FALSE_POSITIVE_RE.test(lower)) {
91
+ return false;
92
+ }
93
+ return CONTEXT_OVERFLOW_PHRASES.some((phrase) => lower.includes(phrase));
94
+ }
95
+
96
+ /**
97
+ * Loose check: returns true for known phrases OR heuristic regex matches.
98
+ * Preferred by the graph's emergency-prune retry because the cost of a
99
+ * false positive is one extra retry with a smaller context, while the
100
+ * cost of a false negative is an opaque provider failure surfaced to
101
+ * the user.
102
+ */
103
+ export function isLikelyContextOverflowError(errorMessage?: string): boolean {
104
+ if (!errorMessage) {
105
+ return false;
106
+ }
107
+ if (isContextOverflowError(errorMessage)) {
108
+ return true;
109
+ }
110
+ const lower = errorMessage.toLowerCase();
111
+ if (FALSE_POSITIVE_RE.test(lower)) {
112
+ return false;
113
+ }
114
+ return CONTEXT_OVERFLOW_HINT_RE.test(lower);
115
+ }
@@ -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,10 +44,11 @@ export async function safeDispatchCustomEvent(
13
44
  config?: RunnableConfig
14
45
  ): Promise<void> {
15
46
  try {
16
- if (event === 'on_agent_transition') {
17
- console.log(`[safeDispatchCustomEvent] Dispatching: ${event}, payload=${JSON.stringify(payload)}`);
18
- }
19
- 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);
20
52
  } catch (e) {
21
53
  // Check if this is the known EventStreamCallbackHandler error
22
54
  if (
@@ -24,9 +56,7 @@ export async function safeDispatchCustomEvent(
24
56
  e.message.includes('handleCustomEvent: Run ID') &&
25
57
  e.message.includes('not found in run map')
26
58
  ) {
27
- // Suppress this specific error - it's expected during parallel execution
28
- // when EventStreamCallbackHandler loses track of run IDs
29
- // console.debug('Suppressed error dispatching custom event:', e);
59
+ // Suppress expected during parallel/async execution
30
60
  return;
31
61
  }
32
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
+ }
@@ -12,3 +12,4 @@ export * from './contextPressure';
12
12
  export * from './toolDiscoveryCache';
13
13
  export * from './pruneCalibration';
14
14
  export * from './fileManifest';
15
+ export * from './errors';
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
  }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Tool-call name normalization for malformed LLM tool_use outputs.
3
+ *
4
+ * LLMs — especially smaller / faster models — frequently emit tool_use
5
+ * blocks with names that don't exactly match the registered tool name:
6
+ *
7
+ * - Wrong delimiter: `outlook/operations` vs `outlook_operations`
8
+ * - Function-prefix style: `functions.outlook_operations`
9
+ * - Case drift: `Outlook_Operations`
10
+ * - Counter suffix: `outlook_operations_1`
11
+ * - Missing name, only id: `{name: "", id: "tool_outlook_operations_42"}`
12
+ *
13
+ * Before this normalization layer, any of the above caused the LangGraph
14
+ * ToolNode to throw "Tool X not found" and the whole turn to fail. With
15
+ * normalization, we resolve to the correct registered name case-insensitively
16
+ * and transparently fix the tool_call in place.
17
+ *
18
+ * Resolution order:
19
+ * 1. Exact match
20
+ * 2. Normalized delimiter + exact
21
+ * 3. Case-insensitive match
22
+ * 4. Structured candidates (strip prefixes, take suffix segments)
23
+ * 5. Infer from tool_call id (strip trailing digits, function/tool prefix)
24
+ *
25
+ * Any unresolvable tool_call is left as-is — the downstream ToolNode will
26
+ * fail with its usual error and the auto-recovery path kicks in.
27
+ */
28
+
29
+ function lowercaseOrEmpty(value: string): string {
30
+ return typeof value === 'string' ? value.toLowerCase() : '';
31
+ }
32
+
33
+ /** Collapse common delimiters (space, dot, slash, dash) to `_` and fold case. */
34
+ function normalizeDelimiters(name: string): string {
35
+ return name
36
+ .trim()
37
+ .replace(/[\s./-]+/g, '_')
38
+ .replace(/_{2,}/g, '_')
39
+ .replace(/^_+|_+$/g, '');
40
+ }
41
+
42
+ function resolveCaseInsensitive(
43
+ rawName: string,
44
+ allowed: Set<string>
45
+ ): string | null {
46
+ if (allowed.size === 0) return null;
47
+ const folded = lowercaseOrEmpty(rawName);
48
+ let match: string | null = null;
49
+ for (const name of allowed) {
50
+ if (lowercaseOrEmpty(name) !== folded) continue;
51
+ // Ambiguous → fail closed
52
+ if (match && match !== name) return null;
53
+ match = name;
54
+ }
55
+ return match;
56
+ }
57
+
58
+ function resolveExact(rawName: string, allowed: Set<string>): string | null {
59
+ if (allowed.size === 0) return null;
60
+ if (allowed.has(rawName)) return rawName;
61
+ const normalized = normalizeDelimiters(rawName);
62
+ if (allowed.has(normalized)) return normalized;
63
+ return (
64
+ resolveCaseInsensitive(rawName, allowed) ??
65
+ resolveCaseInsensitive(normalized, allowed)
66
+ );
67
+ }
68
+
69
+ function buildStructuredCandidates(rawName: string): string[] {
70
+ const trimmed = rawName.trim();
71
+ if (!trimmed) return [];
72
+ const seen = new Set<string>();
73
+ const out: string[] = [];
74
+ const add = (value: string): void => {
75
+ const c = value.trim();
76
+ if (!c || seen.has(c)) return;
77
+ seen.add(c);
78
+ out.push(c);
79
+ };
80
+ add(trimmed);
81
+ add(normalizeDelimiters(trimmed));
82
+ // Also try dot-normalized, then split
83
+ const dotForm = trimmed.replace(/\//g, '.');
84
+ add(dotForm);
85
+ add(normalizeDelimiters(dotForm));
86
+ const segments = dotForm
87
+ .split('.')
88
+ .map((s) => s.trim())
89
+ .filter(Boolean);
90
+ if (segments.length > 1) {
91
+ for (let i = 1; i < segments.length; i++) {
92
+ const suffix = segments.slice(i).join('.');
93
+ add(suffix);
94
+ add(normalizeDelimiters(suffix));
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ function resolveStructured(
101
+ rawName: string,
102
+ allowed: Set<string>
103
+ ): string | null {
104
+ if (allowed.size === 0) return null;
105
+ const candidates = buildStructuredCandidates(rawName);
106
+ for (const c of candidates) {
107
+ if (allowed.has(c)) return c;
108
+ }
109
+ for (const c of candidates) {
110
+ const ci = resolveCaseInsensitive(c, allowed);
111
+ if (ci) return ci;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ function inferFromToolCallId(
117
+ rawId: string | undefined,
118
+ allowed: Set<string>
119
+ ): string | null {
120
+ if (!rawId || allowed.size === 0) return null;
121
+ const id = rawId.trim();
122
+ if (!id) return null;
123
+
124
+ const tokens = new Set<string>();
125
+ const addTokens = (value: string): void => {
126
+ const t = value.trim();
127
+ if (!t) return;
128
+ tokens.add(t);
129
+ tokens.add(t.replace(/[:._/-]\d+$/, ''));
130
+ tokens.add(t.replace(/\d+$/, ''));
131
+ const dotForm = t.replace(/\//g, '.');
132
+ tokens.add(dotForm);
133
+ tokens.add(dotForm.replace(/[:._-]\d+$/, ''));
134
+ tokens.add(dotForm.replace(/\d+$/, ''));
135
+ for (const prefix of [/^functions?[._-]?/i, /^tools?[._-]?/i]) {
136
+ const stripped = dotForm.replace(prefix, '');
137
+ if (stripped !== dotForm) {
138
+ tokens.add(stripped);
139
+ tokens.add(stripped.replace(/[:._-]\d+$/, ''));
140
+ tokens.add(stripped.replace(/\d+$/, ''));
141
+ }
142
+ }
143
+ };
144
+
145
+ const preColon = id.split(':')[0] ?? id;
146
+ addTokens(id);
147
+ addTokens(preColon);
148
+
149
+ let singleMatch: string | null = null;
150
+ for (const token of tokens) {
151
+ const matched = resolveStructured(token, allowed);
152
+ if (!matched) continue;
153
+ // Ambiguous → fail closed
154
+ if (singleMatch && singleMatch !== matched) return null;
155
+ singleMatch = matched;
156
+ }
157
+ if (singleMatch) return singleMatch;
158
+
159
+ // Substring fallback: ids frequently look like `call_<toolname>_<n>` or
160
+ // `toolu_01...<toolname>...`. Scan for any allowed name embedded in the
161
+ // delimiter-normalized id (case-insensitive) and fail closed if more than
162
+ // one distinct allowed name matches.
163
+ const haystack = lowercaseOrEmpty(normalizeDelimiters(id));
164
+ let substrMatch: string | null = null;
165
+ for (const allowedName of allowed) {
166
+ const needle = lowercaseOrEmpty(allowedName);
167
+ if (!needle) continue;
168
+ if (!haystack.includes(needle)) continue;
169
+ if (substrMatch && substrMatch !== allowedName) return null;
170
+ substrMatch = allowedName;
171
+ }
172
+ return substrMatch;
173
+ }
174
+
175
+ /**
176
+ * Produce the best allowed tool name for a raw LLM tool_use name.
177
+ * Returns the original name unchanged if no resolution is possible —
178
+ * the downstream executor will then fail with its normal error path.
179
+ */
180
+ export function normalizeToolCallName(
181
+ rawName: string,
182
+ allowedToolNames: Set<string>,
183
+ rawToolCallId?: string
184
+ ): string {
185
+ const trimmed = rawName.trim() ?? '';
186
+ if (!trimmed) {
187
+ return (
188
+ inferFromToolCallId(rawToolCallId, allowedToolNames) ?? rawName ?? ''
189
+ );
190
+ }
191
+ if (allowedToolNames.size === 0) return trimmed;
192
+
193
+ const exact = resolveExact(trimmed, allowedToolNames);
194
+ if (exact) return exact;
195
+
196
+ const structured = resolveStructured(trimmed, allowedToolNames);
197
+ if (structured) return structured;
198
+
199
+ // Last-resort: try the raw id (if supplied). Substring matching lives
200
+ // inside inferFromToolCallId — only safe to invoke when the caller
201
+ // passed a real provider id, not when the raw name was used above.
202
+ const inferred = inferFromToolCallId(rawToolCallId, allowedToolNames);
203
+ if (inferred) return inferred;
204
+
205
+ return trimmed;
206
+ }
207
+
208
+ /**
209
+ * In-place normalization of all tool_calls on an AIMessage-like object.
210
+ *
211
+ * Applies `normalizeToolCallName` to each tool_call's `name` field using the
212
+ * provided allowed-tool set (derived from the agent's toolMap keys). Also
213
+ * rewrites the `name` field inside any content blocks of type `tool_use` so
214
+ * that downstream providers see the corrected name.
215
+ *
216
+ * Returns `true` if any name was rewritten.
217
+ */
218
+ export function normalizeMessageToolCalls(
219
+ message: unknown,
220
+ allowedToolNames: Set<string>
221
+ ): boolean {
222
+ if (!message || typeof message !== 'object') return false;
223
+ let changed = false;
224
+
225
+ // LangChain AIMessage.tool_calls (normalized form)
226
+ const msg = message as {
227
+ tool_calls?: Array<{ name?: string; id?: string }>;
228
+ content?: unknown;
229
+ };
230
+ if (Array.isArray(msg.tool_calls)) {
231
+ for (const tc of msg.tool_calls) {
232
+ if (!tc || typeof tc !== 'object') continue;
233
+ const rawName = typeof tc.name === 'string' ? tc.name : '';
234
+ const rawId = typeof tc.id === 'string' ? tc.id : undefined;
235
+ const normalized = normalizeToolCallName(
236
+ rawName,
237
+ allowedToolNames,
238
+ rawId
239
+ );
240
+ if (normalized && normalized !== rawName) {
241
+ tc.name = normalized;
242
+ changed = true;
243
+ }
244
+ }
245
+ }
246
+
247
+ // Anthropic-style content blocks with type: 'tool_use'
248
+ if (Array.isArray(msg.content)) {
249
+ for (const block of msg.content as Array<{
250
+ type?: unknown;
251
+ name?: unknown;
252
+ id?: unknown;
253
+ }>) {
254
+ if (!block || typeof block !== 'object') continue;
255
+ if (block.type !== 'tool_use') continue;
256
+ const rawName = typeof block.name === 'string' ? block.name : '';
257
+ const rawId = typeof block.id === 'string' ? block.id : undefined;
258
+ const normalized = normalizeToolCallName(
259
+ rawName,
260
+ allowedToolNames,
261
+ rawId
262
+ );
263
+ if (normalized && normalized !== rawName) {
264
+ block.name = normalized;
265
+ changed = true;
266
+ }
267
+ }
268
+ }
269
+
270
+ return changed;
271
+ }