@illuma-ai/agents 1.1.20 → 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 (246) 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/llm/bedrock/index.cjs +14 -0
  6. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  7. package/dist/cjs/run.cjs +20 -9
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/esm/graphs/Graph.mjs +12 -1
  10. package/dist/esm/graphs/Graph.mjs.map +1 -1
  11. package/dist/esm/graphs/MultiAgentGraph.mjs +85 -1
  12. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  13. package/dist/esm/llm/bedrock/index.mjs +14 -0
  14. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  15. package/dist/esm/run.mjs +20 -9
  16. package/dist/esm/run.mjs.map +1 -1
  17. package/dist/types/graphs/MultiAgentGraph.d.ts +17 -0
  18. package/package.json +1 -1
  19. package/src/graphs/Graph.ts +12 -1
  20. package/src/graphs/MultiAgentGraph.ts +105 -1
  21. package/src/graphs/__tests__/multi-agent-delegate.test.ts +191 -0
  22. package/src/llm/bedrock/index.ts +17 -0
  23. package/src/run.ts +20 -11
  24. package/src/scripts/test-bedrock-handoff-autonomous.ts +231 -0
  25. package/src/agents/AgentContext.js +0 -782
  26. package/src/agents/AgentContext.test.js +0 -421
  27. package/src/agents/__tests__/AgentContext.test.js +0 -678
  28. package/src/agents/__tests__/resolveStructuredOutputMode.test.js +0 -117
  29. package/src/common/enum.js +0 -192
  30. package/src/common/index.js +0 -3
  31. package/src/events.js +0 -166
  32. package/src/graphs/Graph.js +0 -1857
  33. package/src/graphs/MultiAgentGraph.js +0 -1092
  34. package/src/graphs/__tests__/structured-output.integration.test.js +0 -624
  35. package/src/graphs/__tests__/structured-output.test.js +0 -144
  36. package/src/graphs/contextManagement.e2e.test.js +0 -718
  37. package/src/graphs/contextManagement.test.js +0 -485
  38. package/src/graphs/handoffValidation.test.js +0 -276
  39. package/src/graphs/index.js +0 -3
  40. package/src/index.js +0 -28
  41. package/src/instrumentation.js +0 -21
  42. package/src/llm/anthropic/index.js +0 -319
  43. package/src/llm/anthropic/types.js +0 -46
  44. package/src/llm/anthropic/utils/message_inputs.js +0 -627
  45. package/src/llm/anthropic/utils/message_outputs.js +0 -290
  46. package/src/llm/anthropic/utils/output_parsers.js +0 -89
  47. package/src/llm/anthropic/utils/tools.js +0 -25
  48. package/src/llm/bedrock/__tests__/bedrock-caching.test.js +0 -392
  49. package/src/llm/bedrock/index.js +0 -303
  50. package/src/llm/bedrock/types.js +0 -2
  51. package/src/llm/bedrock/utils/index.js +0 -6
  52. package/src/llm/bedrock/utils/message_inputs.js +0 -463
  53. package/src/llm/bedrock/utils/message_outputs.js +0 -269
  54. package/src/llm/fake.js +0 -92
  55. package/src/llm/google/index.js +0 -215
  56. package/src/llm/google/types.js +0 -12
  57. package/src/llm/google/utils/common.js +0 -670
  58. package/src/llm/google/utils/tools.js +0 -111
  59. package/src/llm/google/utils/zod_to_genai_parameters.js +0 -47
  60. package/src/llm/openai/index.js +0 -1033
  61. package/src/llm/openai/types.js +0 -2
  62. package/src/llm/openai/utils/index.js +0 -756
  63. package/src/llm/openai/utils/isReasoningModel.test.js +0 -79
  64. package/src/llm/openrouter/index.js +0 -261
  65. package/src/llm/openrouter/reasoning.test.js +0 -181
  66. package/src/llm/providers.js +0 -36
  67. package/src/llm/text.js +0 -65
  68. package/src/llm/vertexai/index.js +0 -402
  69. package/src/messages/__tests__/tools.test.js +0 -392
  70. package/src/messages/cache.js +0 -404
  71. package/src/messages/cache.test.js +0 -1167
  72. package/src/messages/content.js +0 -48
  73. package/src/messages/content.test.js +0 -314
  74. package/src/messages/core.js +0 -359
  75. package/src/messages/ensureThinkingBlock.test.js +0 -997
  76. package/src/messages/format.js +0 -973
  77. package/src/messages/formatAgentMessages.test.js +0 -2278
  78. package/src/messages/formatAgentMessages.tools.test.js +0 -362
  79. package/src/messages/formatMessage.test.js +0 -608
  80. package/src/messages/ids.js +0 -18
  81. package/src/messages/index.js +0 -9
  82. package/src/messages/labelContentByAgent.test.js +0 -725
  83. package/src/messages/prune.js +0 -438
  84. package/src/messages/reducer.js +0 -60
  85. package/src/messages/shiftIndexTokenCountMap.test.js +0 -63
  86. package/src/messages/summarize.js +0 -146
  87. package/src/messages/summarize.test.js +0 -332
  88. package/src/messages/tools.js +0 -90
  89. package/src/mockStream.js +0 -81
  90. package/src/prompts/collab.js +0 -7
  91. package/src/prompts/index.js +0 -3
  92. package/src/prompts/taskmanager.js +0 -58
  93. package/src/run.js +0 -427
  94. package/src/schemas/index.js +0 -3
  95. package/src/schemas/schema-preparation.test.js +0 -370
  96. package/src/schemas/validate.js +0 -314
  97. package/src/schemas/validate.test.js +0 -264
  98. package/src/scripts/abort.js +0 -127
  99. package/src/scripts/ant_web_search.js +0 -130
  100. package/src/scripts/ant_web_search_edge_case.js +0 -133
  101. package/src/scripts/ant_web_search_error_edge_case.js +0 -119
  102. package/src/scripts/args.js +0 -41
  103. package/src/scripts/bedrock-cache-debug.js +0 -186
  104. package/src/scripts/bedrock-content-aggregation-test.js +0 -195
  105. package/src/scripts/bedrock-merge-test.js +0 -80
  106. package/src/scripts/bedrock-parallel-tools-test.js +0 -150
  107. package/src/scripts/caching.js +0 -106
  108. package/src/scripts/cli.js +0 -152
  109. package/src/scripts/cli2.js +0 -119
  110. package/src/scripts/cli3.js +0 -163
  111. package/src/scripts/cli4.js +0 -165
  112. package/src/scripts/cli5.js +0 -165
  113. package/src/scripts/code_exec.js +0 -171
  114. package/src/scripts/code_exec_files.js +0 -180
  115. package/src/scripts/code_exec_multi_session.js +0 -185
  116. package/src/scripts/code_exec_ptc.js +0 -265
  117. package/src/scripts/code_exec_session.js +0 -217
  118. package/src/scripts/code_exec_simple.js +0 -120
  119. package/src/scripts/content.js +0 -111
  120. package/src/scripts/empty_input.js +0 -125
  121. package/src/scripts/handoff-test.js +0 -96
  122. package/src/scripts/image.js +0 -138
  123. package/src/scripts/memory.js +0 -83
  124. package/src/scripts/multi-agent-chain.js +0 -271
  125. package/src/scripts/multi-agent-conditional.js +0 -185
  126. package/src/scripts/multi-agent-document-review-chain.js +0 -171
  127. package/src/scripts/multi-agent-hybrid-flow.js +0 -264
  128. package/src/scripts/multi-agent-parallel-start.js +0 -214
  129. package/src/scripts/multi-agent-parallel.js +0 -346
  130. package/src/scripts/multi-agent-sequence.js +0 -184
  131. package/src/scripts/multi-agent-supervisor.js +0 -324
  132. package/src/scripts/multi-agent-test.js +0 -147
  133. package/src/scripts/parallel-asymmetric-tools-test.js +0 -202
  134. package/src/scripts/parallel-full-metadata-test.js +0 -176
  135. package/src/scripts/parallel-tools-test.js +0 -256
  136. package/src/scripts/programmatic_exec.js +0 -277
  137. package/src/scripts/programmatic_exec_agent.js +0 -168
  138. package/src/scripts/search.js +0 -118
  139. package/src/scripts/sequential-full-metadata-test.js +0 -143
  140. package/src/scripts/simple.js +0 -174
  141. package/src/scripts/single-agent-metadata-test.js +0 -152
  142. package/src/scripts/stream.js +0 -113
  143. package/src/scripts/test-custom-prompt-key.js +0 -132
  144. package/src/scripts/test-handoff-input.js +0 -143
  145. package/src/scripts/test-handoff-preamble.js +0 -227
  146. package/src/scripts/test-handoff-steering.js +0 -353
  147. package/src/scripts/test-multi-agent-list-handoff.js +0 -318
  148. package/src/scripts/test-parallel-agent-labeling.js +0 -253
  149. package/src/scripts/test-parallel-handoffs.js +0 -229
  150. package/src/scripts/test-thinking-handoff-bedrock.js +0 -132
  151. package/src/scripts/test-thinking-handoff.js +0 -132
  152. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +0 -140
  153. package/src/scripts/test-tool-before-handoff-role-order.js +0 -223
  154. package/src/scripts/test-tools-before-handoff.js +0 -187
  155. package/src/scripts/test_code_api.js +0 -263
  156. package/src/scripts/thinking-bedrock.js +0 -128
  157. package/src/scripts/thinking-vertexai.js +0 -130
  158. package/src/scripts/thinking.js +0 -134
  159. package/src/scripts/tool_search.js +0 -114
  160. package/src/scripts/tools.js +0 -125
  161. package/src/specs/agent-handoffs-bedrock.integration.test.js +0 -280
  162. package/src/specs/agent-handoffs.test.js +0 -924
  163. package/src/specs/anthropic.simple.test.js +0 -287
  164. package/src/specs/azure.simple.test.js +0 -381
  165. package/src/specs/cache.simple.test.js +0 -282
  166. package/src/specs/custom-event-await.test.js +0 -148
  167. package/src/specs/deepseek.simple.test.js +0 -189
  168. package/src/specs/emergency-prune.test.js +0 -308
  169. package/src/specs/moonshot.simple.test.js +0 -237
  170. package/src/specs/observability.integration.test.js +0 -1337
  171. package/src/specs/openai.simple.test.js +0 -233
  172. package/src/specs/openrouter.simple.test.js +0 -202
  173. package/src/specs/prune.test.js +0 -733
  174. package/src/specs/reasoning.test.js +0 -144
  175. package/src/specs/spec.utils.js +0 -4
  176. package/src/specs/thinking-handoff.test.js +0 -486
  177. package/src/specs/thinking-prune.test.js +0 -600
  178. package/src/specs/token-distribution-edge-case.test.js +0 -246
  179. package/src/specs/token-memoization.test.js +0 -32
  180. package/src/specs/tokens.test.js +0 -49
  181. package/src/specs/tool-error.test.js +0 -139
  182. package/src/splitStream.js +0 -204
  183. package/src/splitStream.test.js +0 -504
  184. package/src/stream.js +0 -650
  185. package/src/stream.test.js +0 -225
  186. package/src/test/mockTools.js +0 -340
  187. package/src/tools/BrowserTools.js +0 -245
  188. package/src/tools/Calculator.js +0 -38
  189. package/src/tools/Calculator.test.js +0 -225
  190. package/src/tools/CodeExecutor.js +0 -233
  191. package/src/tools/ProgrammaticToolCalling.js +0 -602
  192. package/src/tools/StreamingToolCallBuffer.js +0 -179
  193. package/src/tools/ToolNode.js +0 -930
  194. package/src/tools/ToolSearch.js +0 -904
  195. package/src/tools/__tests__/BrowserTools.test.js +0 -306
  196. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +0 -276
  197. package/src/tools/__tests__/ProgrammaticToolCalling.test.js +0 -807
  198. package/src/tools/__tests__/StreamingToolCallBuffer.test.js +0 -175
  199. package/src/tools/__tests__/ToolApproval.test.js +0 -675
  200. package/src/tools/__tests__/ToolNode.recovery.test.js +0 -200
  201. package/src/tools/__tests__/ToolNode.session.test.js +0 -319
  202. package/src/tools/__tests__/ToolSearch.integration.test.js +0 -125
  203. package/src/tools/__tests__/ToolSearch.test.js +0 -812
  204. package/src/tools/__tests__/handlers.test.js +0 -799
  205. package/src/tools/__tests__/truncation-recovery.integration.test.js +0 -362
  206. package/src/tools/handlers.js +0 -306
  207. package/src/tools/schema.js +0 -25
  208. package/src/tools/search/anthropic.js +0 -34
  209. package/src/tools/search/content.js +0 -116
  210. package/src/tools/search/content.test.js +0 -133
  211. package/src/tools/search/firecrawl.js +0 -173
  212. package/src/tools/search/format.js +0 -198
  213. package/src/tools/search/highlights.js +0 -241
  214. package/src/tools/search/index.js +0 -3
  215. package/src/tools/search/jina-reranker.test.js +0 -106
  216. package/src/tools/search/rerankers.js +0 -165
  217. package/src/tools/search/schema.js +0 -102
  218. package/src/tools/search/search.js +0 -561
  219. package/src/tools/search/serper-scraper.js +0 -126
  220. package/src/tools/search/test.js +0 -129
  221. package/src/tools/search/tool.js +0 -453
  222. package/src/tools/search/types.js +0 -2
  223. package/src/tools/search/utils.js +0 -59
  224. package/src/types/graph.js +0 -24
  225. package/src/types/graph.test.js +0 -192
  226. package/src/types/index.js +0 -7
  227. package/src/types/llm.js +0 -2
  228. package/src/types/messages.js +0 -2
  229. package/src/types/run.js +0 -2
  230. package/src/types/stream.js +0 -2
  231. package/src/types/tools.js +0 -2
  232. package/src/utils/contextAnalytics.js +0 -79
  233. package/src/utils/contextAnalytics.test.js +0 -166
  234. package/src/utils/events.js +0 -26
  235. package/src/utils/graph.js +0 -11
  236. package/src/utils/handlers.js +0 -65
  237. package/src/utils/index.js +0 -10
  238. package/src/utils/llm.js +0 -21
  239. package/src/utils/llmConfig.js +0 -205
  240. package/src/utils/logging.js +0 -37
  241. package/src/utils/misc.js +0 -51
  242. package/src/utils/run.js +0 -69
  243. package/src/utils/schema.js +0 -21
  244. package/src/utils/title.js +0 -119
  245. package/src/utils/tokens.js +0 -92
  246. 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