@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,136 @@
1
+ /**
2
+ * Unit tests for context-overflow error classification.
3
+ *
4
+ * The graph's emergency-prune retry relies on these helpers to decide
5
+ * whether a provider failure warrants a truncated retry. False positives
6
+ * cost one extra retry; false negatives surface an opaque failure to the
7
+ * user. Both are cheaper than the previous inline substring matching,
8
+ * which missed phrases like "request_too_large" (Anthropic 429-adjacent)
9
+ * and could falsely trigger on rate-limit errors mentioning "too many".
10
+ */
11
+ import {
12
+ extractErrorMessage,
13
+ isContextOverflowError,
14
+ isLikelyContextOverflowError,
15
+ } from '../errors';
16
+
17
+ describe('extractErrorMessage', () => {
18
+ it('returns empty string for null/undefined', () => {
19
+ expect(extractErrorMessage(null)).toBe('');
20
+ expect(extractErrorMessage(undefined)).toBe('');
21
+ });
22
+
23
+ it('returns the string directly', () => {
24
+ expect(extractErrorMessage('something broke')).toBe('something broke');
25
+ });
26
+
27
+ it('reads Error.message', () => {
28
+ expect(extractErrorMessage(new Error('boom'))).toBe('boom');
29
+ });
30
+
31
+ it('reads plain-object message/error fields', () => {
32
+ expect(extractErrorMessage({ message: 'm' })).toBe('m');
33
+ expect(extractErrorMessage({ error: 'e' })).toBe('e');
34
+ expect(extractErrorMessage({ error: { message: 'nested' } })).toBe(
35
+ 'nested'
36
+ );
37
+ });
38
+
39
+ it('falls back to JSON stringify for unknown shapes', () => {
40
+ expect(extractErrorMessage({ status: 500 })).toBe('{"status":500}');
41
+ });
42
+ });
43
+
44
+ describe('isContextOverflowError (strict)', () => {
45
+ it('returns false for empty input', () => {
46
+ expect(isContextOverflowError()).toBe(false);
47
+ expect(isContextOverflowError('')).toBe(false);
48
+ });
49
+
50
+ it('matches Anthropic prompt-too-long', () => {
51
+ expect(
52
+ isContextOverflowError('prompt is too long: 250000 tokens > 200000')
53
+ ).toBe(true);
54
+ });
55
+
56
+ it('matches OpenAI context_length_exceeded', () => {
57
+ expect(
58
+ isContextOverflowError(
59
+ "This model's maximum context length is 128000 tokens. context_length_exceeded"
60
+ )
61
+ ).toBe(true);
62
+ });
63
+
64
+ it('matches Bedrock input-too-long', () => {
65
+ expect(
66
+ isContextOverflowError(
67
+ 'ValidationException: Input is too long for requested model.'
68
+ )
69
+ ).toBe(true);
70
+ });
71
+
72
+ it('matches request_too_large', () => {
73
+ expect(
74
+ isContextOverflowError('Error code 413: request_too_large')
75
+ ).toBe(true);
76
+ });
77
+
78
+ it('is case-insensitive', () => {
79
+ expect(isContextOverflowError('PROMPT IS TOO LONG')).toBe(true);
80
+ });
81
+
82
+ it('rejects rate-limit errors even if they mention "too many"', () => {
83
+ expect(
84
+ isContextOverflowError('429 rate_limit_exceeded: too many requests')
85
+ ).toBe(false);
86
+ });
87
+
88
+ it('rejects auth / billing errors', () => {
89
+ expect(isContextOverflowError('insufficient quota on billing plan')).toBe(
90
+ false
91
+ );
92
+ expect(isContextOverflowError('forbidden: missing permission')).toBe(false);
93
+ });
94
+
95
+ it('does not match loose phrases like bare "too long"', () => {
96
+ // Strict check should NOT fire on just "too long" — that's for the
97
+ // loose variant. Keeps the retry budget tight.
98
+ expect(isContextOverflowError('the response was too long')).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe('isLikelyContextOverflowError (loose)', () => {
103
+ it('matches everything the strict check matches', () => {
104
+ expect(isLikelyContextOverflowError('prompt is too long')).toBe(true);
105
+ expect(isLikelyContextOverflowError('context_length_exceeded')).toBe(true);
106
+ });
107
+
108
+ it('matches heuristic regex: bare "too long"', () => {
109
+ expect(isLikelyContextOverflowError('response was too long')).toBe(true);
110
+ });
111
+
112
+ it('matches heuristic regex: 413 status code', () => {
113
+ expect(isLikelyContextOverflowError('HTTP 413 payload')).toBe(true);
114
+ });
115
+
116
+ it('matches "context ... exceed" in either order', () => {
117
+ expect(isLikelyContextOverflowError('your context exceeds limits')).toBe(
118
+ true
119
+ );
120
+ expect(isLikelyContextOverflowError('exceeds context window')).toBe(true);
121
+ });
122
+
123
+ it('still rejects rate-limit / auth even on loose match', () => {
124
+ expect(
125
+ isLikelyContextOverflowError('rate limit: too many requests queued')
126
+ ).toBe(false);
127
+ expect(
128
+ isLikelyContextOverflowError('authorization exceeds allowed quota')
129
+ ).toBe(false);
130
+ });
131
+
132
+ it('returns false for unrelated errors', () => {
133
+ expect(isLikelyContextOverflowError('ECONNREFUSED')).toBe(false);
134
+ expect(isLikelyContextOverflowError('unexpected token in JSON')).toBe(false);
135
+ });
136
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Unit tests for the finish-reason detection helpers.
3
+ *
4
+ * Guards the single source of truth that truncation-aware callers
5
+ * (Graph.ts sticky finish reason, host continuation retry) rely on.
6
+ */
7
+ import {
8
+ TRUNCATION_FINISH_REASONS,
9
+ isTruncationReason,
10
+ } from '../finishReasons';
11
+
12
+ describe('TRUNCATION_FINISH_REASONS', () => {
13
+ it('includes every provider-specific truncation value we support', () => {
14
+ expect(TRUNCATION_FINISH_REASONS.has('max_tokens')).toBe(true);
15
+ expect(TRUNCATION_FINISH_REASONS.has('length')).toBe(true);
16
+ expect(TRUNCATION_FINISH_REASONS.has('MAX_TOKENS')).toBe(true);
17
+ });
18
+
19
+ it('is case-sensitive — lowercase vertex value is not accepted', () => {
20
+ // VertexAI uses the uppercase enum name; lowercase would be a wire bug
21
+ // we do not want to silently treat as truncation.
22
+ expect(TRUNCATION_FINISH_REASONS.has('max_Tokens')).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe('isTruncationReason', () => {
27
+ it('returns true for Anthropic/Bedrock max_tokens', () => {
28
+ expect(isTruncationReason('max_tokens')).toBe(true);
29
+ });
30
+
31
+ it('returns true for OpenAI length', () => {
32
+ expect(isTruncationReason('length')).toBe(true);
33
+ });
34
+
35
+ it('returns true for VertexAI MAX_TOKENS', () => {
36
+ expect(isTruncationReason('MAX_TOKENS')).toBe(true);
37
+ });
38
+
39
+ it('returns false for the stop/end_turn happy path', () => {
40
+ expect(isTruncationReason('stop')).toBe(false);
41
+ expect(isTruncationReason('end_turn')).toBe(false);
42
+ expect(isTruncationReason('STOP')).toBe(false);
43
+ });
44
+
45
+ it('returns false for tool_use / tool_calls', () => {
46
+ expect(isTruncationReason('tool_use')).toBe(false);
47
+ expect(isTruncationReason('tool_calls')).toBe(false);
48
+ });
49
+
50
+ it('returns false for undefined, null, and empty string', () => {
51
+ expect(isTruncationReason(undefined)).toBe(false);
52
+ expect(isTruncationReason(null)).toBe(false);
53
+ expect(isTruncationReason('')).toBe(false);
54
+ });
55
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Unit tests for `toolCallNormalization`.
3
+ *
4
+ * Exercises every resolution branch of `normalizeToolCallName` and
5
+ * `normalizeMessageToolCalls` so future edits can't silently regress the
6
+ * fault-tolerance guarantees the downstream ToolNode relies on.
7
+ */
8
+ import {
9
+ normalizeToolCallName,
10
+ normalizeMessageToolCalls,
11
+ } from '../toolCallNormalization';
12
+
13
+ const allowed = new Set([
14
+ 'outlook_operations',
15
+ 'teams_operations',
16
+ 'sharepoint_operations',
17
+ 'person_lookup',
18
+ ]);
19
+
20
+ describe('normalizeToolCallName', () => {
21
+ describe('exact match fast path', () => {
22
+ it('returns the name unchanged when it matches exactly', () => {
23
+ expect(normalizeToolCallName('outlook_operations', allowed)).toBe(
24
+ 'outlook_operations'
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('delimiter normalization', () => {
30
+ it('maps slash delimiters to underscore', () => {
31
+ expect(normalizeToolCallName('outlook/operations', allowed)).toBe(
32
+ 'outlook_operations'
33
+ );
34
+ });
35
+
36
+ it('maps dot delimiters to underscore', () => {
37
+ expect(normalizeToolCallName('outlook.operations', allowed)).toBe(
38
+ 'outlook_operations'
39
+ );
40
+ });
41
+
42
+ it('maps dash delimiters to underscore', () => {
43
+ expect(normalizeToolCallName('outlook-operations', allowed)).toBe(
44
+ 'outlook_operations'
45
+ );
46
+ });
47
+
48
+ it('returns the trimmed input when delimiter-normalized form is not registered', () => {
49
+ // `outlook_operations_v2` is not in the allowed set, so the resolver
50
+ // leaves the name untouched rather than guessing which variant the
51
+ // model meant.
52
+ expect(normalizeToolCallName('outlook.operations-v2', allowed)).toBe(
53
+ 'outlook.operations-v2'
54
+ );
55
+ });
56
+ });
57
+
58
+ describe('case folding', () => {
59
+ it('resolves camelCase to registered snake_case', () => {
60
+ expect(normalizeToolCallName('Outlook_Operations', allowed)).toBe(
61
+ 'outlook_operations'
62
+ );
63
+ });
64
+
65
+ it('resolves SCREAMING_CASE', () => {
66
+ expect(normalizeToolCallName('OUTLOOK_OPERATIONS', allowed)).toBe(
67
+ 'outlook_operations'
68
+ );
69
+ });
70
+ });
71
+
72
+ describe('structured candidates — prefix stripping', () => {
73
+ it('strips functions. prefix', () => {
74
+ expect(
75
+ normalizeToolCallName('functions.outlook_operations', allowed)
76
+ ).toBe('outlook_operations');
77
+ });
78
+
79
+ it('strips tools. prefix', () => {
80
+ expect(normalizeToolCallName('tools.teams_operations', allowed)).toBe(
81
+ 'teams_operations'
82
+ );
83
+ });
84
+
85
+ it('takes suffix segment when dotted path has multiple segments', () => {
86
+ expect(
87
+ normalizeToolCallName('namespace.group.person_lookup', allowed)
88
+ ).toBe('person_lookup');
89
+ });
90
+ });
91
+
92
+ describe('infer from tool_call id when name is empty', () => {
93
+ it('recovers name from an id containing the tool name', () => {
94
+ expect(
95
+ normalizeToolCallName('', allowed, 'call_outlook_operations_42')
96
+ ).toBe('outlook_operations');
97
+ });
98
+
99
+ it('strips counter suffix from id', () => {
100
+ expect(normalizeToolCallName('', allowed, 'outlook_operations_1')).toBe(
101
+ 'outlook_operations'
102
+ );
103
+ });
104
+
105
+ it('returns empty string when id is also absent', () => {
106
+ expect(normalizeToolCallName('', allowed, undefined)).toBe('');
107
+ });
108
+ });
109
+
110
+ describe('fail-safe behavior', () => {
111
+ it('returns the trimmed input when no match is possible', () => {
112
+ expect(normalizeToolCallName('totally_unknown_tool', allowed)).toBe(
113
+ 'totally_unknown_tool'
114
+ );
115
+ });
116
+
117
+ it('returns the trimmed input when allowed set is empty', () => {
118
+ expect(normalizeToolCallName('outlook_operations', new Set())).toBe(
119
+ 'outlook_operations'
120
+ );
121
+ });
122
+
123
+ it('fails closed when case-insensitive match is ambiguous', () => {
124
+ const ambiguous = new Set(['Tool', 'tool']);
125
+ // Two allowed names fold to the same lowercase — resolver must not
126
+ // guess. It returns the input unchanged (via structured fallthrough).
127
+ const out = normalizeToolCallName('TOOL', ambiguous);
128
+ expect(out).toBe('TOOL');
129
+ });
130
+ });
131
+ });
132
+
133
+ describe('normalizeMessageToolCalls', () => {
134
+ it('rewrites LangChain-style tool_calls in place', () => {
135
+ const msg = {
136
+ tool_calls: [
137
+ { name: 'functions.outlook_operations', id: 'call_1' },
138
+ { name: 'Teams_Operations', id: 'call_2' },
139
+ ],
140
+ };
141
+ const changed = normalizeMessageToolCalls(msg, allowed);
142
+ expect(changed).toBe(true);
143
+ expect(msg.tool_calls[0].name).toBe('outlook_operations');
144
+ expect(msg.tool_calls[1].name).toBe('teams_operations');
145
+ });
146
+
147
+ it('rewrites Anthropic-style tool_use content blocks in place', () => {
148
+ const msg = {
149
+ content: [
150
+ { type: 'text', text: 'ok' },
151
+ { type: 'tool_use', name: 'outlook/operations', id: 'toolu_1' },
152
+ ],
153
+ };
154
+ const changed = normalizeMessageToolCalls(msg, allowed);
155
+ expect(changed).toBe(true);
156
+ const toolBlock = msg.content[1] as { name?: string };
157
+ expect(toolBlock.name).toBe('outlook_operations');
158
+ });
159
+
160
+ it('returns false when nothing needed rewriting', () => {
161
+ const msg = {
162
+ tool_calls: [{ name: 'outlook_operations', id: 'call_1' }],
163
+ };
164
+ expect(normalizeMessageToolCalls(msg, allowed)).toBe(false);
165
+ });
166
+
167
+ it('is a no-op for non-message objects', () => {
168
+ expect(normalizeMessageToolCalls(null, allowed)).toBe(false);
169
+ expect(normalizeMessageToolCalls(undefined, allowed)).toBe(false);
170
+ expect(normalizeMessageToolCalls('string', allowed)).toBe(false);
171
+ });
172
+
173
+ it('handles empty name by inferring from id', () => {
174
+ const msg = {
175
+ tool_calls: [{ name: '', id: 'call_person_lookup_99' }],
176
+ };
177
+ const changed = normalizeMessageToolCalls(msg, allowed);
178
+ expect(changed).toBe(true);
179
+ expect(msg.tool_calls[0].name).toBe('person_lookup');
180
+ });
181
+ });
@@ -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
+ }