@illuma-ai/agents 1.1.21 → 1.1.22

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 (241) hide show
  1. package/dist/cjs/graphs/Graph.cjs +12 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +85 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/run.cjs +20 -9
  6. package/dist/cjs/run.cjs.map +1 -1
  7. package/dist/esm/graphs/Graph.mjs +12 -1
  8. package/dist/esm/graphs/Graph.mjs.map +1 -1
  9. package/dist/esm/graphs/MultiAgentGraph.mjs +85 -1
  10. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  11. package/dist/esm/run.mjs +20 -9
  12. package/dist/esm/run.mjs.map +1 -1
  13. package/dist/types/graphs/MultiAgentGraph.d.ts +17 -0
  14. package/package.json +1 -1
  15. package/src/graphs/Graph.ts +12 -1
  16. package/src/graphs/MultiAgentGraph.ts +105 -1
  17. package/src/graphs/__tests__/multi-agent-delegate.test.ts +191 -0
  18. package/src/run.ts +20 -11
  19. package/src/scripts/test-bedrock-handoff-autonomous.ts +231 -0
  20. package/src/agents/AgentContext.js +0 -782
  21. package/src/agents/AgentContext.test.js +0 -421
  22. package/src/agents/__tests__/AgentContext.test.js +0 -678
  23. package/src/agents/__tests__/resolveStructuredOutputMode.test.js +0 -117
  24. package/src/common/enum.js +0 -192
  25. package/src/common/index.js +0 -3
  26. package/src/events.js +0 -166
  27. package/src/graphs/Graph.js +0 -1857
  28. package/src/graphs/MultiAgentGraph.js +0 -1092
  29. package/src/graphs/__tests__/structured-output.integration.test.js +0 -624
  30. package/src/graphs/__tests__/structured-output.test.js +0 -144
  31. package/src/graphs/contextManagement.e2e.test.js +0 -718
  32. package/src/graphs/contextManagement.test.js +0 -485
  33. package/src/graphs/handoffValidation.test.js +0 -276
  34. package/src/graphs/index.js +0 -3
  35. package/src/index.js +0 -28
  36. package/src/instrumentation.js +0 -21
  37. package/src/llm/anthropic/index.js +0 -319
  38. package/src/llm/anthropic/types.js +0 -46
  39. package/src/llm/anthropic/utils/message_inputs.js +0 -627
  40. package/src/llm/anthropic/utils/message_outputs.js +0 -290
  41. package/src/llm/anthropic/utils/output_parsers.js +0 -89
  42. package/src/llm/anthropic/utils/tools.js +0 -25
  43. package/src/llm/bedrock/__tests__/bedrock-caching.test.js +0 -392
  44. package/src/llm/bedrock/index.js +0 -303
  45. package/src/llm/bedrock/types.js +0 -2
  46. package/src/llm/bedrock/utils/index.js +0 -6
  47. package/src/llm/bedrock/utils/message_inputs.js +0 -463
  48. package/src/llm/bedrock/utils/message_outputs.js +0 -269
  49. package/src/llm/fake.js +0 -92
  50. package/src/llm/google/index.js +0 -215
  51. package/src/llm/google/types.js +0 -12
  52. package/src/llm/google/utils/common.js +0 -670
  53. package/src/llm/google/utils/tools.js +0 -111
  54. package/src/llm/google/utils/zod_to_genai_parameters.js +0 -47
  55. package/src/llm/openai/index.js +0 -1033
  56. package/src/llm/openai/types.js +0 -2
  57. package/src/llm/openai/utils/index.js +0 -756
  58. package/src/llm/openai/utils/isReasoningModel.test.js +0 -79
  59. package/src/llm/openrouter/index.js +0 -261
  60. package/src/llm/openrouter/reasoning.test.js +0 -181
  61. package/src/llm/providers.js +0 -36
  62. package/src/llm/text.js +0 -65
  63. package/src/llm/vertexai/index.js +0 -402
  64. package/src/messages/__tests__/tools.test.js +0 -392
  65. package/src/messages/cache.js +0 -404
  66. package/src/messages/cache.test.js +0 -1167
  67. package/src/messages/content.js +0 -48
  68. package/src/messages/content.test.js +0 -314
  69. package/src/messages/core.js +0 -359
  70. package/src/messages/ensureThinkingBlock.test.js +0 -997
  71. package/src/messages/format.js +0 -973
  72. package/src/messages/formatAgentMessages.test.js +0 -2278
  73. package/src/messages/formatAgentMessages.tools.test.js +0 -362
  74. package/src/messages/formatMessage.test.js +0 -608
  75. package/src/messages/ids.js +0 -18
  76. package/src/messages/index.js +0 -9
  77. package/src/messages/labelContentByAgent.test.js +0 -725
  78. package/src/messages/prune.js +0 -438
  79. package/src/messages/reducer.js +0 -60
  80. package/src/messages/shiftIndexTokenCountMap.test.js +0 -63
  81. package/src/messages/summarize.js +0 -146
  82. package/src/messages/summarize.test.js +0 -332
  83. package/src/messages/tools.js +0 -90
  84. package/src/mockStream.js +0 -81
  85. package/src/prompts/collab.js +0 -7
  86. package/src/prompts/index.js +0 -3
  87. package/src/prompts/taskmanager.js +0 -58
  88. package/src/run.js +0 -427
  89. package/src/schemas/index.js +0 -3
  90. package/src/schemas/schema-preparation.test.js +0 -370
  91. package/src/schemas/validate.js +0 -314
  92. package/src/schemas/validate.test.js +0 -264
  93. package/src/scripts/abort.js +0 -127
  94. package/src/scripts/ant_web_search.js +0 -130
  95. package/src/scripts/ant_web_search_edge_case.js +0 -133
  96. package/src/scripts/ant_web_search_error_edge_case.js +0 -119
  97. package/src/scripts/args.js +0 -41
  98. package/src/scripts/bedrock-cache-debug.js +0 -186
  99. package/src/scripts/bedrock-content-aggregation-test.js +0 -195
  100. package/src/scripts/bedrock-merge-test.js +0 -80
  101. package/src/scripts/bedrock-parallel-tools-test.js +0 -150
  102. package/src/scripts/caching.js +0 -106
  103. package/src/scripts/cli.js +0 -152
  104. package/src/scripts/cli2.js +0 -119
  105. package/src/scripts/cli3.js +0 -163
  106. package/src/scripts/cli4.js +0 -165
  107. package/src/scripts/cli5.js +0 -165
  108. package/src/scripts/code_exec.js +0 -171
  109. package/src/scripts/code_exec_files.js +0 -180
  110. package/src/scripts/code_exec_multi_session.js +0 -185
  111. package/src/scripts/code_exec_ptc.js +0 -265
  112. package/src/scripts/code_exec_session.js +0 -217
  113. package/src/scripts/code_exec_simple.js +0 -120
  114. package/src/scripts/content.js +0 -111
  115. package/src/scripts/empty_input.js +0 -125
  116. package/src/scripts/handoff-test.js +0 -96
  117. package/src/scripts/image.js +0 -138
  118. package/src/scripts/memory.js +0 -83
  119. package/src/scripts/multi-agent-chain.js +0 -271
  120. package/src/scripts/multi-agent-conditional.js +0 -185
  121. package/src/scripts/multi-agent-document-review-chain.js +0 -171
  122. package/src/scripts/multi-agent-hybrid-flow.js +0 -264
  123. package/src/scripts/multi-agent-parallel-start.js +0 -214
  124. package/src/scripts/multi-agent-parallel.js +0 -346
  125. package/src/scripts/multi-agent-sequence.js +0 -184
  126. package/src/scripts/multi-agent-supervisor.js +0 -324
  127. package/src/scripts/multi-agent-test.js +0 -147
  128. package/src/scripts/parallel-asymmetric-tools-test.js +0 -202
  129. package/src/scripts/parallel-full-metadata-test.js +0 -176
  130. package/src/scripts/parallel-tools-test.js +0 -256
  131. package/src/scripts/programmatic_exec.js +0 -277
  132. package/src/scripts/programmatic_exec_agent.js +0 -168
  133. package/src/scripts/search.js +0 -118
  134. package/src/scripts/sequential-full-metadata-test.js +0 -143
  135. package/src/scripts/simple.js +0 -174
  136. package/src/scripts/single-agent-metadata-test.js +0 -152
  137. package/src/scripts/stream.js +0 -113
  138. package/src/scripts/test-custom-prompt-key.js +0 -132
  139. package/src/scripts/test-handoff-input.js +0 -143
  140. package/src/scripts/test-handoff-preamble.js +0 -227
  141. package/src/scripts/test-handoff-steering.js +0 -353
  142. package/src/scripts/test-multi-agent-list-handoff.js +0 -318
  143. package/src/scripts/test-parallel-agent-labeling.js +0 -253
  144. package/src/scripts/test-parallel-handoffs.js +0 -229
  145. package/src/scripts/test-thinking-handoff-bedrock.js +0 -132
  146. package/src/scripts/test-thinking-handoff.js +0 -132
  147. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +0 -140
  148. package/src/scripts/test-tool-before-handoff-role-order.js +0 -223
  149. package/src/scripts/test-tools-before-handoff.js +0 -187
  150. package/src/scripts/test_code_api.js +0 -263
  151. package/src/scripts/thinking-bedrock.js +0 -128
  152. package/src/scripts/thinking-vertexai.js +0 -130
  153. package/src/scripts/thinking.js +0 -134
  154. package/src/scripts/tool_search.js +0 -114
  155. package/src/scripts/tools.js +0 -125
  156. package/src/specs/agent-handoffs-bedrock.integration.test.js +0 -280
  157. package/src/specs/agent-handoffs.test.js +0 -924
  158. package/src/specs/anthropic.simple.test.js +0 -287
  159. package/src/specs/azure.simple.test.js +0 -381
  160. package/src/specs/cache.simple.test.js +0 -282
  161. package/src/specs/custom-event-await.test.js +0 -148
  162. package/src/specs/deepseek.simple.test.js +0 -189
  163. package/src/specs/emergency-prune.test.js +0 -308
  164. package/src/specs/moonshot.simple.test.js +0 -237
  165. package/src/specs/observability.integration.test.js +0 -1337
  166. package/src/specs/openai.simple.test.js +0 -233
  167. package/src/specs/openrouter.simple.test.js +0 -202
  168. package/src/specs/prune.test.js +0 -733
  169. package/src/specs/reasoning.test.js +0 -144
  170. package/src/specs/spec.utils.js +0 -4
  171. package/src/specs/thinking-handoff.test.js +0 -486
  172. package/src/specs/thinking-prune.test.js +0 -600
  173. package/src/specs/token-distribution-edge-case.test.js +0 -246
  174. package/src/specs/token-memoization.test.js +0 -32
  175. package/src/specs/tokens.test.js +0 -49
  176. package/src/specs/tool-error.test.js +0 -139
  177. package/src/splitStream.js +0 -204
  178. package/src/splitStream.test.js +0 -504
  179. package/src/stream.js +0 -650
  180. package/src/stream.test.js +0 -225
  181. package/src/test/mockTools.js +0 -340
  182. package/src/tools/BrowserTools.js +0 -245
  183. package/src/tools/Calculator.js +0 -38
  184. package/src/tools/Calculator.test.js +0 -225
  185. package/src/tools/CodeExecutor.js +0 -233
  186. package/src/tools/ProgrammaticToolCalling.js +0 -602
  187. package/src/tools/StreamingToolCallBuffer.js +0 -179
  188. package/src/tools/ToolNode.js +0 -930
  189. package/src/tools/ToolSearch.js +0 -904
  190. package/src/tools/__tests__/BrowserTools.test.js +0 -306
  191. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +0 -276
  192. package/src/tools/__tests__/ProgrammaticToolCalling.test.js +0 -807
  193. package/src/tools/__tests__/StreamingToolCallBuffer.test.js +0 -175
  194. package/src/tools/__tests__/ToolApproval.test.js +0 -675
  195. package/src/tools/__tests__/ToolNode.recovery.test.js +0 -200
  196. package/src/tools/__tests__/ToolNode.session.test.js +0 -319
  197. package/src/tools/__tests__/ToolSearch.integration.test.js +0 -125
  198. package/src/tools/__tests__/ToolSearch.test.js +0 -812
  199. package/src/tools/__tests__/handlers.test.js +0 -799
  200. package/src/tools/__tests__/truncation-recovery.integration.test.js +0 -362
  201. package/src/tools/handlers.js +0 -306
  202. package/src/tools/schema.js +0 -25
  203. package/src/tools/search/anthropic.js +0 -34
  204. package/src/tools/search/content.js +0 -116
  205. package/src/tools/search/content.test.js +0 -133
  206. package/src/tools/search/firecrawl.js +0 -173
  207. package/src/tools/search/format.js +0 -198
  208. package/src/tools/search/highlights.js +0 -241
  209. package/src/tools/search/index.js +0 -3
  210. package/src/tools/search/jina-reranker.test.js +0 -106
  211. package/src/tools/search/rerankers.js +0 -165
  212. package/src/tools/search/schema.js +0 -102
  213. package/src/tools/search/search.js +0 -561
  214. package/src/tools/search/serper-scraper.js +0 -126
  215. package/src/tools/search/test.js +0 -129
  216. package/src/tools/search/tool.js +0 -453
  217. package/src/tools/search/types.js +0 -2
  218. package/src/tools/search/utils.js +0 -59
  219. package/src/types/graph.js +0 -24
  220. package/src/types/graph.test.js +0 -192
  221. package/src/types/index.js +0 -7
  222. package/src/types/llm.js +0 -2
  223. package/src/types/messages.js +0 -2
  224. package/src/types/run.js +0 -2
  225. package/src/types/stream.js +0 -2
  226. package/src/types/tools.js +0 -2
  227. package/src/utils/contextAnalytics.js +0 -79
  228. package/src/utils/contextAnalytics.test.js +0 -166
  229. package/src/utils/events.js +0 -26
  230. package/src/utils/graph.js +0 -11
  231. package/src/utils/handlers.js +0 -65
  232. package/src/utils/index.js +0 -10
  233. package/src/utils/llm.js +0 -21
  234. package/src/utils/llmConfig.js +0 -205
  235. package/src/utils/logging.js +0 -37
  236. package/src/utils/misc.js +0 -51
  237. package/src/utils/run.js +0 -69
  238. package/src/utils/schema.js +0 -21
  239. package/src/utils/title.js +0 -119
  240. package/src/utils/tokens.js +0 -92
  241. package/src/utils/toonFormat.js +0 -379
@@ -1,362 +0,0 @@
1
- /**
2
- * Integration test: Full Truncation Recovery Pipeline
3
- *
4
- * Tests the end-to-end flow that would catch the Bedrock buffer bug:
5
- * Streaming chunks → handleToolCallChunks → StreamingToolCallBuffer → handleToolCalls → ToolNode.recoverTruncatedArgs → tool receives recovered args
6
- *
7
- * Uses real StreamingToolCallBuffer (not mocked) and real ToolNode with real tool definitions.
8
- * Only the graph dispatch layer is mocked (since we're not testing SSE dispatch).
9
- *
10
- * Verified against live Bedrock API output (see bedrock-live-poc.ts):
11
- * START chunk: { id, name, index, args: "" }
12
- * DELTA chunks: { args, index } — NO id, NO name
13
- */
14
- import { z } from 'zod';
15
- import { tool } from '@langchain/core/tools';
16
- import { AIMessage } from '@langchain/core/messages';
17
- import { describe, it, expect, beforeEach, jest } from '@jest/globals';
18
- import { StepTypes } from '@/common';
19
- import { StreamingToolCallBuffer } from '../StreamingToolCallBuffer';
20
- import { handleToolCallChunks } from '../handlers';
21
- import { ToolNode } from '../ToolNode';
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
- /** Creates a mock graph that delegates buffer operations to a REAL StreamingToolCallBuffer */
26
- function createIntegrationGraph(buffer) {
27
- let stepCounter = 0;
28
- return {
29
- getStepKey: jest.fn().mockReturnValue('step-key'),
30
- getStepIdByKey: jest.fn().mockReturnValue('prev-step-id'),
31
- getRunStep: jest.fn().mockReturnValue({
32
- type: StepTypes.MESSAGE_CREATION,
33
- id: 'prev-step-id',
34
- index: 0,
35
- stepDetails: { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'msg-1' } },
36
- usage: null,
37
- }),
38
- dispatchRunStep: jest.fn()
39
- .mockImplementation(async () => `step-${++stepCounter}`),
40
- dispatchRunStepDelta: jest.fn().mockResolvedValue(undefined),
41
- toolCallStepIds: new Map(),
42
- messageStepHasToolCalls: new Map(),
43
- messageIdsByStepKey: new Map(),
44
- prelimMessageIdsByStepKey: new Map(),
45
- streamingToolCallBuffer: buffer, // REAL buffer, not mocked
46
- };
47
- }
48
- /** Creates a tool that captures received args for assertion */
49
- function createCaptureTool(name, schema, capturedArgs) {
50
- return tool(async (input) => {
51
- capturedArgs.push({ ...input });
52
- return `OK`;
53
- }, { name, description: `Test tool: ${name}`, schema });
54
- }
55
- /** Simulates Bedrock streaming: START chunk with {id, name, index, args:""}, then DELTA chunks with {args, index} */
56
- function simulateBedrockChunks(toolCallId, toolName, index, argsJson, chunkSize = 30) {
57
- const chunks = [];
58
- // START chunk: id + name + index, args="" (verified from live Bedrock API)
59
- chunks.push({
60
- id: toolCallId,
61
- name: toolName,
62
- index,
63
- args: '',
64
- type: 'tool_call_chunk',
65
- });
66
- // DELTA chunks: args + index, NO id, NO name (verified from live Bedrock API)
67
- for (let i = 0; i < argsJson.length; i += chunkSize) {
68
- chunks.push({
69
- id: undefined,
70
- name: undefined,
71
- index,
72
- args: argsJson.substring(i, i + chunkSize),
73
- type: 'tool_call_chunk',
74
- });
75
- }
76
- return chunks;
77
- }
78
- /** Simulates OpenAI/Anthropic streaming: every chunk has {id, args} */
79
- function simulateOpenAIChunks(toolCallId, toolName, index, argsJson, chunkSize = 30) {
80
- const chunks = [];
81
- // First chunk: id + name + first args fragment
82
- const firstArgs = argsJson.substring(0, chunkSize);
83
- chunks.push({
84
- id: toolCallId,
85
- name: toolName,
86
- index,
87
- args: firstArgs,
88
- type: 'tool_call_chunk',
89
- });
90
- // Subsequent chunks: id + args (OpenAI sends id on every chunk)
91
- for (let i = chunkSize; i < argsJson.length; i += chunkSize) {
92
- chunks.push({
93
- id: toolCallId,
94
- name: undefined,
95
- index,
96
- args: argsJson.substring(i, i + chunkSize),
97
- type: 'tool_call_chunk',
98
- });
99
- }
100
- return chunks;
101
- }
102
- const contentToolSchema = z.object({
103
- action: z.string(),
104
- filename: z.string().optional(),
105
- content: z.string().optional(),
106
- });
107
- const codeExecutorSchema = z.object({
108
- code: z.string(),
109
- language: z.string().optional(),
110
- });
111
- // ---------------------------------------------------------------------------
112
- // Tests
113
- // ---------------------------------------------------------------------------
114
- describe('Truncation Recovery Pipeline (Integration)', () => {
115
- let buffer;
116
- beforeEach(() => {
117
- buffer = new StreamingToolCallBuffer();
118
- });
119
- it('Bedrock truncated write — full pipeline recovery', async () => {
120
- const capturedArgs = [];
121
- const graph = createIntegrationGraph(buffer);
122
- // Full args JSON that Bedrock would stream (including content)
123
- const fullArgsJson = '{"action":"write","filename":"app.tsx","content":"import React from \\"react\\";\\nexport default function App() {\\n return <div>Hello World</div>;\\n}"}';
124
- // Simulate Bedrock streaming: START + DELTA chunks
125
- const chunks = simulateBedrockChunks('tooluse_abc', 'content_tool', 0, fullArgsJson, 20);
126
- // Feed all chunks through handleToolCallChunks (real buffer accumulation)
127
- for (const chunk of chunks) {
128
- await handleToolCallChunks({
129
- graph: graph,
130
- stepKey: 'step-key',
131
- toolCallChunks: [chunk],
132
- metadata: { run_id: 'test' },
133
- });
134
- }
135
- // Verify buffer was populated
136
- expect(buffer.has('tooluse_abc')).toBe(true);
137
- expect(buffer.getRawArgs('tooluse_abc')).toBe(fullArgsJson);
138
- // Simulate truncation: parsePartialJson lost the content field
139
- const truncatedArgs = { action: 'write', filename: 'app.tsx' }; // content MISSING
140
- // Create ToolNode with real buffer
141
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
142
- const toolNode = new ToolNode({
143
- tools: [contentTool],
144
- streamingToolCallBuffer: buffer,
145
- });
146
- // Invoke ToolNode with truncated args
147
- const aiMsg = new AIMessage({
148
- content: '',
149
- tool_calls: [{ id: 'tooluse_abc', name: 'content_tool', args: truncatedArgs }],
150
- });
151
- await toolNode.invoke({ messages: [aiMsg] });
152
- // Assert: tool received RECOVERED content, not truncated
153
- expect(capturedArgs).toHaveLength(1);
154
- expect(capturedArgs[0].action).toBe('write');
155
- expect(capturedArgs[0].filename).toBe('app.tsx');
156
- expect(capturedArgs[0].content).toBe('import React from "react";\nexport default function App() {\n return <div>Hello World</div>;\n}');
157
- });
158
- it('OpenAI/Anthropic chunked write — full pipeline recovery', async () => {
159
- const capturedArgs = [];
160
- const graph = createIntegrationGraph(buffer);
161
- const fullArgsJson = '{"action":"write","filename":"test.tsx","content":"const x = 42;\\nconst y = 100;"}';
162
- const chunks = simulateOpenAIChunks('tc_openai', 'content_tool', 0, fullArgsJson, 15);
163
- for (const chunk of chunks) {
164
- await handleToolCallChunks({
165
- graph: graph,
166
- stepKey: 'step-key',
167
- toolCallChunks: [chunk],
168
- metadata: { run_id: 'test' },
169
- });
170
- }
171
- expect(buffer.has('tc_openai')).toBe(true);
172
- expect(buffer.getRawArgs('tc_openai')).toBe(fullArgsJson);
173
- // Simulate truncation: content missing
174
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
175
- const toolNode = new ToolNode({
176
- tools: [contentTool],
177
- streamingToolCallBuffer: buffer,
178
- });
179
- const aiMsg = new AIMessage({
180
- content: '',
181
- tool_calls: [{ id: 'tc_openai', name: 'content_tool', args: { action: 'write', filename: 'test.tsx' } }],
182
- });
183
- await toolNode.invoke({ messages: [aiMsg] });
184
- expect(capturedArgs[0].content).toBe('const x = 42;\nconst y = 100;');
185
- });
186
- it('multiple parallel tool calls — no cross-contamination', async () => {
187
- const capturedArgs = [];
188
- const graph = createIntegrationGraph(buffer);
189
- // Two parallel tool calls with different indexes
190
- const args1 = '{"action":"write","filename":"a.tsx","content":"file A content"}';
191
- const args2 = '{"action":"write","filename":"b.tsx","content":"file B content"}';
192
- const chunks1 = simulateBedrockChunks('tc_1', 'content_tool', 0, args1, 15);
193
- const chunks2 = simulateBedrockChunks('tc_2', 'content_tool', 1, args2, 15);
194
- // Interleave chunks (realistic concurrent streaming)
195
- const interleaved = [];
196
- const maxLen = Math.max(chunks1.length, chunks2.length);
197
- for (let i = 0; i < maxLen; i++) {
198
- if (i < chunks1.length)
199
- interleaved.push(chunks1[i]);
200
- if (i < chunks2.length)
201
- interleaved.push(chunks2[i]);
202
- }
203
- for (const chunk of interleaved) {
204
- await handleToolCallChunks({
205
- graph: graph,
206
- stepKey: 'step-key',
207
- toolCallChunks: [chunk],
208
- metadata: { run_id: 'test' },
209
- });
210
- }
211
- expect(buffer.getRawArgs('tc_1')).toBe(args1);
212
- expect(buffer.getRawArgs('tc_2')).toBe(args2);
213
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
214
- const toolNode = new ToolNode({
215
- tools: [contentTool],
216
- streamingToolCallBuffer: buffer,
217
- });
218
- // Only tc_1 is truncated (missing content), tc_2 has all fields
219
- const aiMsg = new AIMessage({
220
- content: '',
221
- tool_calls: [
222
- { id: 'tc_1', name: 'content_tool', args: { action: 'write', filename: 'a.tsx' } }, // truncated
223
- { id: 'tc_2', name: 'content_tool', args: { action: 'write', filename: 'b.tsx', content: 'file B content' } }, // complete
224
- ],
225
- });
226
- await toolNode.invoke({ messages: [aiMsg] });
227
- expect(capturedArgs).toHaveLength(2);
228
- // tc_1: content recovered from buffer
229
- expect(capturedArgs[0].content).toBe('file A content');
230
- // tc_2: content unchanged (already present in parsed args)
231
- expect(capturedArgs[1].content).toBe('file B content');
232
- });
233
- it('buffer cleanup after recovery — no stale data', async () => {
234
- const capturedArgs = [];
235
- const graph = createIntegrationGraph(buffer);
236
- const fullArgs = '{"action":"write","filename":"x.tsx","content":"test"}';
237
- const chunks = simulateBedrockChunks('tc_cleanup', 'content_tool', 0, fullArgs);
238
- for (const chunk of chunks) {
239
- await handleToolCallChunks({
240
- graph: graph,
241
- stepKey: 'step-key',
242
- toolCallChunks: [chunk],
243
- metadata: { run_id: 'test' },
244
- });
245
- }
246
- expect(buffer.has('tc_cleanup')).toBe(true);
247
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
248
- const toolNode = new ToolNode({
249
- tools: [contentTool],
250
- streamingToolCallBuffer: buffer,
251
- });
252
- const aiMsg = new AIMessage({
253
- content: '',
254
- tool_calls: [{ id: 'tc_cleanup', name: 'content_tool', args: { action: 'write' } }],
255
- });
256
- await toolNode.invoke({ messages: [aiMsg] });
257
- // Buffer should be cleared after processing
258
- expect(buffer.has('tc_cleanup')).toBe(false);
259
- expect(buffer.getRawArgs('tc_cleanup')).toBeUndefined();
260
- });
261
- it('no truncation — buffer populated but not used for overwrite', async () => {
262
- const capturedArgs = [];
263
- const graph = createIntegrationGraph(buffer);
264
- const fullArgs = '{"action":"read","filename":"existing.tsx"}';
265
- const chunks = simulateBedrockChunks('tc_complete', 'content_tool', 0, fullArgs);
266
- for (const chunk of chunks) {
267
- await handleToolCallChunks({
268
- graph: graph,
269
- stepKey: 'step-key',
270
- toolCallChunks: [chunk],
271
- metadata: { run_id: 'test' },
272
- });
273
- }
274
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
275
- const toolNode = new ToolNode({
276
- tools: [contentTool],
277
- streamingToolCallBuffer: buffer,
278
- });
279
- // Complete args — nothing missing
280
- const aiMsg = new AIMessage({
281
- content: '',
282
- tool_calls: [{ id: 'tc_complete', name: 'content_tool', args: { action: 'read', filename: 'existing.tsx' } }],
283
- });
284
- await toolNode.invoke({ messages: [aiMsg] });
285
- expect(capturedArgs[0].action).toBe('read');
286
- expect(capturedArgs[0].filename).toBe('existing.tsx');
287
- // No content field in args or buffer — should stay absent
288
- expect(capturedArgs[0]).not.toHaveProperty('content');
289
- });
290
- it('generic tool recovery — non-content_tool (code_executor)', async () => {
291
- const capturedArgs = [];
292
- const graph = createIntegrationGraph(buffer);
293
- const largeCode = 'function fibonacci(n) {\\n if (n <= 1) return n;\\n return fibonacci(n-1) + fibonacci(n-2);\\n}\\nconsole.log(fibonacci(10));';
294
- const fullArgs = `{"code":"${largeCode}","language":"javascript"}`;
295
- const chunks = simulateBedrockChunks('tc_code', 'execute_code', 0, fullArgs, 20);
296
- for (const chunk of chunks) {
297
- await handleToolCallChunks({
298
- graph: graph,
299
- stepKey: 'step-key',
300
- toolCallChunks: [chunk],
301
- metadata: { run_id: 'test' },
302
- });
303
- }
304
- const codeTool = createCaptureTool('execute_code', codeExecutorSchema, capturedArgs);
305
- const toolNode = new ToolNode({
306
- tools: [codeTool],
307
- streamingToolCallBuffer: buffer,
308
- });
309
- // Truncated: only has language, code is missing
310
- const aiMsg = new AIMessage({
311
- content: '',
312
- tool_calls: [{ id: 'tc_code', name: 'execute_code', args: { language: 'javascript' } }],
313
- });
314
- await toolNode.invoke({ messages: [aiMsg] });
315
- expect(capturedArgs[0].code).toContain('fibonacci');
316
- expect(capturedArgs[0].language).toBe('javascript');
317
- });
318
- it('Bedrock large content (5000+ chars) — recovery preserves full content', async () => {
319
- const capturedArgs = [];
320
- const graph = createIntegrationGraph(buffer);
321
- // Generate large content (simulating a real dashboard component)
322
- const lines = [];
323
- lines.push('import React from \\"react\\";');
324
- lines.push('import { useState, useEffect } from \\"react\\";');
325
- for (let i = 0; i < 100; i++) {
326
- lines.push(`// Component section ${i}`);
327
- lines.push(`const Section${i} = () => <div>Section ${i}</div>;`);
328
- }
329
- lines.push('export default function Dashboard() {');
330
- lines.push(' return <div>Dashboard</div>;');
331
- lines.push('}');
332
- const content = lines.join('\\n');
333
- const fullArgs = `{"action":"write","filename":"dashboard.tsx","content":"${content}"}`;
334
- const chunks = simulateBedrockChunks('tc_large', 'content_tool', 0, fullArgs, 50);
335
- for (const chunk of chunks) {
336
- await handleToolCallChunks({
337
- graph: graph,
338
- stepKey: 'step-key',
339
- toolCallChunks: [chunk],
340
- metadata: { run_id: 'test' },
341
- });
342
- }
343
- const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
344
- const toolNode = new ToolNode({
345
- tools: [contentTool],
346
- streamingToolCallBuffer: buffer,
347
- });
348
- // Completely empty args (truncation wiped everything)
349
- const aiMsg = new AIMessage({
350
- content: '',
351
- tool_calls: [{ id: 'tc_large', name: 'content_tool', args: {} }],
352
- });
353
- await toolNode.invoke({ messages: [aiMsg] });
354
- expect(capturedArgs[0].action).toBe('write');
355
- expect(capturedArgs[0].filename).toBe('dashboard.tsx');
356
- const recoveredContent = capturedArgs[0].content;
357
- expect(recoveredContent).toContain('import React from "react"');
358
- expect(recoveredContent).toContain('Section 99');
359
- expect(recoveredContent).toContain('export default function Dashboard()');
360
- });
361
- });
362
- //# sourceMappingURL=truncation-recovery.integration.test.js.map
@@ -1,306 +0,0 @@
1
- /* eslint-disable no-console */
2
- // src/tools/handlers.ts
3
- import { nanoid } from 'nanoid';
4
- import { ToolMessage } from '@langchain/core/messages';
5
- import { ToolCallTypes, GraphEvents, StepTypes, Providers, Constants, } from '@/common';
6
- import { coerceAnthropicSearchResults, isAnthropicWebSearchResult, } from '@/tools/search/anthropic';
7
- import { formatResultsForLLM } from '@/tools/search/format';
8
- import { getMessageId } from '@/messages';
9
- export async function handleToolCallChunks({ graph, stepKey, toolCallChunks, metadata, }) {
10
- let prevStepId;
11
- let prevRunStep;
12
- try {
13
- prevStepId = graph.getStepIdByKey(stepKey);
14
- prevRunStep = graph.getRunStep(prevStepId);
15
- }
16
- catch {
17
- /** Edge Case: If no previous step exists, create a new message creation step */
18
- const message_id = getMessageId(stepKey, graph, true) ?? '';
19
- prevStepId = await graph.dispatchRunStep(stepKey, {
20
- type: StepTypes.MESSAGE_CREATION,
21
- message_creation: {
22
- message_id,
23
- },
24
- }, metadata);
25
- prevRunStep = graph.getRunStep(prevStepId);
26
- }
27
- const _stepId = graph.getStepIdByKey(stepKey);
28
- /** Edge Case: Tool Call Run Step or `tool_call_ids` never dispatched */
29
- const tool_calls = prevStepId && prevRunStep && prevRunStep.type === StepTypes.MESSAGE_CREATION
30
- ? []
31
- : undefined;
32
- /**
33
- * Feed streaming tool call buffer — accumulate raw arg strings for truncation recovery.
34
- *
35
- * Provider chunk patterns:
36
- * - OpenAI/Anthropic: every chunk has {id, args} → direct append
37
- * - Bedrock: START chunk has {id, name, index}, DELTA chunks have {args, index} (no id)
38
- * → use index-to-id mapping to resolve the target buffer entry
39
- */
40
- for (const toolCallChunk of toolCallChunks) {
41
- const chunkIndex = toolCallChunk.index;
42
- // START chunk: has id (and usually name). Store index→id mapping for future DELTA chunks.
43
- if (toolCallChunk.id) {
44
- if (typeof chunkIndex === 'number') {
45
- graph.streamingToolCallBuffer.setIndexMapping(chunkIndex, toolCallChunk.id);
46
- }
47
- if (toolCallChunk.name) {
48
- graph.streamingToolCallBuffer.setToolName(toolCallChunk.id, toolCallChunk.name);
49
- }
50
- // Append args if present on the same chunk (OpenAI/Anthropic pattern)
51
- if (toolCallChunk.args) {
52
- graph.streamingToolCallBuffer.append(toolCallChunk.id, toolCallChunk.args);
53
- }
54
- }
55
- else if (toolCallChunk.args && typeof chunkIndex === 'number') {
56
- // DELTA chunk: no id, but has args + index. Resolve id via index mapping (Bedrock pattern).
57
- const resolvedId = graph.streamingToolCallBuffer.getIdByIndex(chunkIndex);
58
- if (resolvedId) {
59
- graph.streamingToolCallBuffer.append(resolvedId, toolCallChunk.args);
60
- }
61
- }
62
- }
63
- /** Edge Case: `id` and `name` fields cannot be empty strings */
64
- for (const toolCallChunk of toolCallChunks) {
65
- if (toolCallChunk.name === '') {
66
- toolCallChunk.name = undefined;
67
- }
68
- if (toolCallChunk.id === '') {
69
- toolCallChunk.id = undefined;
70
- }
71
- else if (tool_calls != null &&
72
- toolCallChunk.id != null &&
73
- toolCallChunk.name != null) {
74
- tool_calls.push({
75
- args: {},
76
- id: toolCallChunk.id,
77
- name: toolCallChunk.name,
78
- type: ToolCallTypes.TOOL_CALL,
79
- });
80
- }
81
- }
82
- let stepId = _stepId;
83
- const alreadyDispatched = prevRunStep?.type === StepTypes.MESSAGE_CREATION &&
84
- graph.messageStepHasToolCalls.has(prevStepId);
85
- if (prevRunStep?.type === StepTypes.TOOL_CALLS) {
86
- /**
87
- * If previous step is already a tool_calls step, use that step ID
88
- * This ensures tool call deltas are dispatched to the correct step
89
- */
90
- stepId = prevStepId;
91
- }
92
- else if (!alreadyDispatched &&
93
- prevRunStep?.type === StepTypes.MESSAGE_CREATION) {
94
- /**
95
- * Create tool_calls step as soon as we receive the first tool call chunk
96
- * This ensures deltas are always associated with the correct step
97
- *
98
- * NOTE: We do NOT dispatch an empty text block here because:
99
- * - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
100
- * - The tool_calls themselves are sufficient for the step
101
- * - Empty content with tool_call_ids gets stored in conversation history
102
- * and causes "messages must have non-empty content" errors on replay
103
- */
104
- graph.messageStepHasToolCalls.set(prevStepId, true);
105
- stepId = await graph.dispatchRunStep(stepKey, {
106
- type: StepTypes.TOOL_CALLS,
107
- tool_calls: tool_calls ?? [],
108
- }, metadata);
109
- }
110
- await graph.dispatchRunStepDelta(stepId, {
111
- type: StepTypes.TOOL_CALLS,
112
- tool_calls: toolCallChunks,
113
- });
114
- }
115
- export const handleToolCalls = async (toolCalls, metadata, graph) => {
116
- if (!graph || !metadata) {
117
- console.warn('Graph or metadata not found in `handleToolCalls`');
118
- return;
119
- }
120
- if (!toolCalls) {
121
- return;
122
- }
123
- if (toolCalls.length === 0) {
124
- return;
125
- }
126
- const stepKey = graph.getStepKey(metadata);
127
- /**
128
- * Track whether we've already reused an empty TOOL_CALLS step created by
129
- * handleToolCallChunks during streaming. Only reuse it once (for the first
130
- * tool call); subsequent parallel tool calls must create their own steps.
131
- */
132
- let reusedChunkStepId;
133
- for (const tool_call of toolCalls) {
134
- const toolCallId = tool_call.id ?? `toolu_${nanoid()}`;
135
- tool_call.id = toolCallId;
136
- // If this tool call ID was already tracked via handleToolCallChunks,
137
- // the step exists but may lack the name (Bedrock sends name only at model end).
138
- // Dispatch a delta with the complete data so the client can fill in the name.
139
- if (toolCallId && graph.toolCallStepIds.has(toolCallId)) {
140
- const existingStepId = graph.toolCallStepIds.get(toolCallId);
141
- if (existingStepId != null && existingStepId !== '') {
142
- const argsStr = typeof tool_call.args === 'string'
143
- ? tool_call.args
144
- : JSON.stringify(tool_call.args);
145
- await graph.dispatchRunStepDelta(existingStepId, {
146
- type: StepTypes.TOOL_CALLS,
147
- tool_calls: [{ name: tool_call.name, args: argsStr, id: toolCallId }],
148
- });
149
- }
150
- continue;
151
- }
152
- let prevStepId = '';
153
- let prevRunStep;
154
- try {
155
- prevStepId = graph.getStepIdByKey(stepKey);
156
- prevRunStep = graph.getRunStep(prevStepId);
157
- }
158
- catch {
159
- // no previous step
160
- }
161
- // If the previous step is TOOL_CALLS (created by handleToolCallChunks),
162
- // either reuse it (if empty) or dispatch a new TOOL_CALLS step directly —
163
- // skip the intermediate MESSAGE_CREATION to avoid orphaned gaps.
164
- if (prevRunStep?.type === StepTypes.TOOL_CALLS) {
165
- const details = prevRunStep.stepDetails;
166
- const isEmpty = !details.tool_calls || details.tool_calls.length === 0;
167
- if (isEmpty && prevStepId !== reusedChunkStepId) {
168
- graph.toolCallStepIds.set(toolCallId, prevStepId);
169
- reusedChunkStepId = prevStepId;
170
- continue;
171
- }
172
- await graph.dispatchRunStep(stepKey, { type: StepTypes.TOOL_CALLS, tool_calls: [tool_call] }, metadata);
173
- continue;
174
- }
175
- /**
176
- * NOTE: We do NOT dispatch empty text blocks with tool_call_ids because:
177
- * - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
178
- * - They get stored in conversation history and cause errors on replay:
179
- * "messages must have non-empty content" (Anthropic)
180
- * "The content field in the Message object is empty" (Bedrock)
181
- * - The tool_calls themselves are sufficient
182
- */
183
- /* If the previous step exists and is a message creation */
184
- if (prevStepId && prevRunStep) {
185
- graph.messageStepHasToolCalls.set(prevStepId, true);
186
- /* If the previous step doesn't exist */
187
- }
188
- else if (!prevRunStep) {
189
- const messageId = getMessageId(stepKey, graph, true) ?? '';
190
- const stepId = await graph.dispatchRunStep(stepKey, {
191
- type: StepTypes.MESSAGE_CREATION,
192
- message_creation: {
193
- message_id: messageId,
194
- },
195
- }, metadata);
196
- graph.messageStepHasToolCalls.set(stepId, true);
197
- }
198
- await graph.dispatchRunStep(stepKey, {
199
- type: StepTypes.TOOL_CALLS,
200
- tool_calls: [tool_call],
201
- }, metadata);
202
- }
203
- };
204
- export const toolResultTypes = new Set([
205
- // 'tool_use',
206
- // 'server_tool_use',
207
- // 'input_json_delta',
208
- 'tool_result',
209
- 'web_search_result',
210
- 'web_search_tool_result',
211
- ]);
212
- /**
213
- * Handles the result of a server tool call; in other words, a provider's built-in tool.
214
- * As of 2025-07-06, only Anthropic handles server tool calls with this pattern.
215
- */
216
- export async function handleServerToolResult({ graph, content, metadata, agentContext, }) {
217
- let skipHandling = false;
218
- if (agentContext?.provider !== Providers.ANTHROPIC) {
219
- return skipHandling;
220
- }
221
- if (typeof content === 'string' ||
222
- content == null ||
223
- content.length === 0 ||
224
- (content.length === 1 &&
225
- content[0].tool_use_id == null)) {
226
- return skipHandling;
227
- }
228
- for (const contentPart of content) {
229
- const toolUseId = contentPart.tool_use_id;
230
- if (toolUseId == null || toolUseId === '') {
231
- continue;
232
- }
233
- const stepId = graph.toolCallStepIds.get(toolUseId);
234
- if (stepId == null || stepId === '') {
235
- console.warn(`Tool use ID ${toolUseId} not found in graph, cannot dispatch tool result.`);
236
- continue;
237
- }
238
- const runStep = graph.getRunStep(stepId);
239
- if (!runStep) {
240
- console.warn(`Run step for ${stepId} does not exist, cannot dispatch tool result.`);
241
- continue;
242
- }
243
- else if (runStep.type !== StepTypes.TOOL_CALLS) {
244
- console.warn(`Run step for ${stepId} is not a tool call step, cannot dispatch tool result.`);
245
- continue;
246
- }
247
- const toolCall = runStep.stepDetails.type === StepTypes.TOOL_CALLS
248
- ? runStep.stepDetails.tool_calls?.find((toolCall) => toolCall.id === toolUseId)
249
- : undefined;
250
- if (!toolCall) {
251
- continue;
252
- }
253
- if (contentPart.type === 'web_search_result' ||
254
- contentPart.type === 'web_search_tool_result') {
255
- await handleAnthropicSearchResults({
256
- contentPart: contentPart,
257
- toolCall,
258
- metadata,
259
- graph,
260
- });
261
- }
262
- if (!skipHandling) {
263
- skipHandling = true;
264
- }
265
- }
266
- return skipHandling;
267
- }
268
- async function handleAnthropicSearchResults({ contentPart, toolCall, metadata, graph, }) {
269
- if (!Array.isArray(contentPart.content)) {
270
- console.warn(`Expected content to be an array, got ${typeof contentPart.content}`);
271
- return;
272
- }
273
- if (!isAnthropicWebSearchResult(contentPart.content[0])) {
274
- console.warn(`Expected content to be an Anthropic web search result, got ${JSON.stringify(contentPart.content)}`);
275
- return;
276
- }
277
- const turn = graph.invokedToolIds?.size ?? 0;
278
- const searchResultData = coerceAnthropicSearchResults({
279
- turn,
280
- results: contentPart.content,
281
- });
282
- const name = toolCall.name;
283
- const input = toolCall.args ?? {};
284
- const artifact = {
285
- [Constants.WEB_SEARCH]: searchResultData,
286
- };
287
- const { output: formattedOutput } = formatResultsForLLM(turn, searchResultData);
288
- const output = new ToolMessage({
289
- name,
290
- artifact,
291
- content: formattedOutput,
292
- tool_call_id: toolCall.id,
293
- });
294
- const toolEndData = {
295
- input,
296
- output,
297
- };
298
- await graph.handlerRegistry
299
- ?.getHandler(GraphEvents.TOOL_END)
300
- ?.handle(GraphEvents.TOOL_END, toolEndData, metadata, graph);
301
- if (graph.invokedToolIds == null) {
302
- graph.invokedToolIds = new Set();
303
- }
304
- graph.invokedToolIds.add(toolCall.id);
305
- }
306
- //# sourceMappingURL=handlers.js.map