@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,2278 +0,0 @@
1
- import { HumanMessage, AIMessage, SystemMessage, ToolMessage, } from '@langchain/core/messages';
2
- import { formatAgentMessages } from './format';
3
- import { ContentTypes } from '@/common';
4
- describe('formatAgentMessages', () => {
5
- it('should format simple user and AI messages', () => {
6
- const payload = [
7
- { role: 'user', content: 'Hello' },
8
- { role: 'assistant', content: 'Hi there!' },
9
- ];
10
- const result = formatAgentMessages(payload);
11
- expect(result.messages).toHaveLength(2);
12
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
13
- expect(result.messages[1]).toBeInstanceOf(AIMessage);
14
- });
15
- it('should handle system messages', () => {
16
- const payload = [
17
- { role: 'system', content: 'You are a helpful assistant.' },
18
- ];
19
- const result = formatAgentMessages(payload);
20
- expect(result.messages).toHaveLength(1);
21
- expect(result.messages[0]).toBeInstanceOf(SystemMessage);
22
- });
23
- it('should format messages with content arrays', () => {
24
- const payload = [
25
- {
26
- role: 'user',
27
- content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello' }],
28
- },
29
- ];
30
- const result = formatAgentMessages(payload);
31
- expect(result.messages).toHaveLength(1);
32
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
33
- });
34
- it('should handle tool calls and create ToolMessages', () => {
35
- const payload = [
36
- {
37
- role: 'assistant',
38
- content: [
39
- {
40
- type: ContentTypes.TEXT,
41
- [ContentTypes.TEXT]: 'Let me check that for you.',
42
- tool_call_ids: ['123'],
43
- },
44
- {
45
- type: ContentTypes.TOOL_CALL,
46
- tool_call: {
47
- id: '123',
48
- name: 'search',
49
- args: '{"query":"weather"}',
50
- output: 'The weather is sunny.',
51
- },
52
- },
53
- ],
54
- },
55
- ];
56
- const result = formatAgentMessages(payload);
57
- expect(result.messages).toHaveLength(2);
58
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
59
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
60
- expect(result.messages[0].tool_calls).toHaveLength(1);
61
- expect(result.messages[1].tool_call_id).toBe('123');
62
- });
63
- it('should handle malformed tool call entries with missing tool_call property', () => {
64
- const tools = new Set(['search']);
65
- const payload = [
66
- {
67
- role: 'assistant',
68
- content: [
69
- {
70
- type: ContentTypes.TEXT,
71
- [ContentTypes.TEXT]: 'Let me check that.',
72
- tool_call_ids: ['123'],
73
- },
74
- {
75
- type: ContentTypes.TOOL_CALL,
76
- // Missing tool_call property - should not crash
77
- },
78
- {
79
- type: ContentTypes.TOOL_CALL,
80
- tool_call: {
81
- id: '123',
82
- name: 'search',
83
- args: '{"query":"test"}',
84
- output: 'Result',
85
- },
86
- },
87
- ],
88
- },
89
- ];
90
- // Should not throw error
91
- const result = formatAgentMessages(payload, undefined, tools);
92
- expect(result.messages).toBeDefined();
93
- expect(result.messages.length).toBeGreaterThan(0);
94
- });
95
- it('should handle malformed tool call entries with missing name', () => {
96
- const tools = new Set(['search']);
97
- const payload = [
98
- {
99
- role: 'assistant',
100
- content: [
101
- {
102
- type: ContentTypes.TEXT,
103
- [ContentTypes.TEXT]: 'Checking...',
104
- },
105
- {
106
- type: ContentTypes.TOOL_CALL,
107
- tool_call: {
108
- id: '456',
109
- // Missing name property
110
- args: '{}',
111
- },
112
- },
113
- ],
114
- },
115
- ];
116
- // Should not throw error
117
- const result = formatAgentMessages(payload, undefined, tools);
118
- expect(result.messages).toBeDefined();
119
- expect(result.messages.length).toBeGreaterThan(0);
120
- });
121
- it('should handle multiple content parts in assistant messages', () => {
122
- const payload = [
123
- {
124
- role: 'assistant',
125
- content: [
126
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 1' },
127
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 2' },
128
- ],
129
- },
130
- ];
131
- const result = formatAgentMessages(payload);
132
- expect(result.messages).toHaveLength(1);
133
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
134
- expect(result.messages[0].content).toHaveLength(2);
135
- });
136
- it('should heal invalid tool call structure by creating a preceding AIMessage', () => {
137
- const payload = [
138
- {
139
- role: 'assistant',
140
- content: [
141
- {
142
- type: ContentTypes.TOOL_CALL,
143
- tool_call: {
144
- id: '123',
145
- name: 'search',
146
- args: '{"query":"weather"}',
147
- output: 'The weather is sunny.',
148
- },
149
- },
150
- ],
151
- },
152
- ];
153
- const result = formatAgentMessages(payload);
154
- // Should have 2 messages: an AIMessage and a ToolMessage
155
- expect(result.messages).toHaveLength(2);
156
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
157
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
158
- // The AIMessage should have an empty content and the tool_call
159
- expect(result.messages[0].content).toBe('');
160
- expect(result.messages[0].tool_calls).toHaveLength(1);
161
- expect(result.messages[0].tool_calls?.[0]).toEqual({
162
- id: '123',
163
- name: 'search',
164
- args: { query: 'weather' },
165
- });
166
- // The ToolMessage should have the correct properties
167
- expect(result.messages[1].tool_call_id).toBe('123');
168
- expect(result.messages[1].name).toBe('search');
169
- expect(result.messages[1].content).toBe('The weather is sunny.');
170
- });
171
- it('should handle tool calls with non-JSON args', () => {
172
- const payload = [
173
- {
174
- role: 'assistant',
175
- content: [
176
- {
177
- type: ContentTypes.TEXT,
178
- [ContentTypes.TEXT]: 'Checking...',
179
- tool_call_ids: ['123'],
180
- },
181
- {
182
- type: ContentTypes.TOOL_CALL,
183
- tool_call: {
184
- id: '123',
185
- name: 'search',
186
- args: 'non-json-string',
187
- output: 'Result',
188
- },
189
- },
190
- ],
191
- },
192
- ];
193
- const result = formatAgentMessages(payload);
194
- expect(result.messages).toHaveLength(2);
195
- expect(result.messages[0].tool_calls?.[0].args).toStrictEqual({ input: 'non-json-string' });
196
- });
197
- it('should handle complex tool calls with multiple steps', () => {
198
- const payload = [
199
- {
200
- role: 'assistant',
201
- content: [
202
- {
203
- type: ContentTypes.TEXT,
204
- [ContentTypes.TEXT]: 'I\'ll search for that information.',
205
- tool_call_ids: ['search_1'],
206
- },
207
- {
208
- type: ContentTypes.TOOL_CALL,
209
- tool_call: {
210
- id: 'search_1',
211
- name: 'search',
212
- args: '{"query":"weather in New York"}',
213
- output: 'The weather in New York is currently sunny with a temperature of 75°F.',
214
- },
215
- },
216
- {
217
- type: ContentTypes.TEXT,
218
- [ContentTypes.TEXT]: 'Now, I\'ll convert the temperature.',
219
- tool_call_ids: ['convert_1'],
220
- },
221
- {
222
- type: ContentTypes.TOOL_CALL,
223
- tool_call: {
224
- id: 'convert_1',
225
- name: 'convert_temperature',
226
- args: '{"temperature": 75, "from": "F", "to": "C"}',
227
- output: '23.89°C',
228
- },
229
- },
230
- {
231
- type: ContentTypes.TEXT,
232
- [ContentTypes.TEXT]: 'Here\'s your answer.',
233
- },
234
- ],
235
- },
236
- ];
237
- const result = formatAgentMessages(payload);
238
- expect(result.messages).toHaveLength(5);
239
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
240
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
241
- expect(result.messages[2]).toBeInstanceOf(AIMessage);
242
- expect(result.messages[3]).toBeInstanceOf(ToolMessage);
243
- expect(result.messages[4]).toBeInstanceOf(AIMessage);
244
- // Check first AIMessage
245
- expect(result.messages[0].content).toBe('I\'ll search for that information.');
246
- expect(result.messages[0].tool_calls).toHaveLength(1);
247
- expect(result.messages[0].tool_calls?.[0]).toEqual({
248
- id: 'search_1',
249
- name: 'search',
250
- args: { query: 'weather in New York' },
251
- });
252
- // Check first ToolMessage
253
- expect(result.messages[1].tool_call_id).toBe('search_1');
254
- expect(result.messages[1].name).toBe('search');
255
- expect(result.messages[1].content).toBe('The weather in New York is currently sunny with a temperature of 75°F.');
256
- // Check second AIMessage
257
- expect(result.messages[2].content).toBe('Now, I\'ll convert the temperature.');
258
- expect(result.messages[2].tool_calls).toHaveLength(1);
259
- expect(result.messages[2].tool_calls?.[0]).toEqual({
260
- id: 'convert_1',
261
- name: 'convert_temperature',
262
- args: { temperature: 75, from: 'F', to: 'C' },
263
- });
264
- // Check second ToolMessage
265
- expect(result.messages[3].tool_call_id).toBe('convert_1');
266
- expect(result.messages[3].name).toBe('convert_temperature');
267
- expect(result.messages[3].content).toBe('23.89°C');
268
- // Check final AIMessage
269
- expect(result.messages[4].content).toStrictEqual([
270
- { [ContentTypes.TEXT]: 'Here\'s your answer.', type: ContentTypes.TEXT },
271
- ]);
272
- });
273
- it('should dynamically discover tools from tool_search output and keep their tool calls', () => {
274
- const tools = new Set(['tool_search', 'calculator']);
275
- const payload = [
276
- {
277
- role: 'user',
278
- content: 'Search for commits and list them',
279
- },
280
- {
281
- role: 'assistant',
282
- content: [
283
- {
284
- type: ContentTypes.TEXT,
285
- [ContentTypes.TEXT]: 'I\'ll search for tools first.',
286
- tool_call_ids: ['ts_1'],
287
- },
288
- {
289
- type: ContentTypes.TOOL_CALL,
290
- tool_call: {
291
- id: 'ts_1',
292
- name: 'tool_search',
293
- args: '{"query":"commits"}',
294
- output: '{"found": 1, "tools": [{"name": "list_commits"}]}',
295
- },
296
- },
297
- {
298
- type: ContentTypes.TEXT,
299
- [ContentTypes.TEXT]: 'Now listing commits.',
300
- tool_call_ids: ['lc_1'],
301
- },
302
- {
303
- type: ContentTypes.TOOL_CALL,
304
- tool_call: {
305
- id: 'lc_1',
306
- name: 'list_commits',
307
- args: '{"repo":"test"}',
308
- output: '[{"sha":"abc123"}]',
309
- },
310
- },
311
- {
312
- type: ContentTypes.TEXT,
313
- [ContentTypes.TEXT]: 'Here are the results.',
314
- },
315
- ],
316
- },
317
- {
318
- role: 'user',
319
- content: 'Thanks!',
320
- },
321
- ];
322
- const result = formatAgentMessages(payload, undefined, tools);
323
- /**
324
- * Since tool_search discovered list_commits, both should be kept.
325
- * The dynamic discovery adds list_commits to the valid tools set.
326
- */
327
- const toolMessages = result.messages.filter((m) => m._getType() === 'tool');
328
- expect(toolMessages.length).toBe(2);
329
- const toolNames = toolMessages.map((m) => m.name).sort();
330
- expect(toolNames).toEqual(['list_commits', 'tool_search']);
331
- });
332
- it('should filter out tool calls not in set and not discovered by tool_search', () => {
333
- const tools = new Set(['tool_search', 'calculator']);
334
- const payload = [
335
- {
336
- role: 'assistant',
337
- content: [
338
- {
339
- type: ContentTypes.TEXT,
340
- [ContentTypes.TEXT]: 'I\'ll call an unknown tool.',
341
- tool_call_ids: ['uk_1'],
342
- },
343
- {
344
- type: ContentTypes.TOOL_CALL,
345
- tool_call: {
346
- id: 'uk_1',
347
- name: 'unknown_tool',
348
- args: '{}',
349
- output: 'result',
350
- },
351
- },
352
- {
353
- type: ContentTypes.TEXT,
354
- [ContentTypes.TEXT]: 'Done.',
355
- },
356
- ],
357
- },
358
- ];
359
- const result = formatAgentMessages(payload, undefined, tools);
360
- /** unknown_tool should be filtered out since it's not in tools set and not discovered */
361
- const toolMessages = result.messages.filter((m) => m._getType() === 'tool');
362
- expect(toolMessages.length).toBe(0);
363
- });
364
- it('should keep all tool calls when all are in the tools set', () => {
365
- const tools = new Set(['search', 'calculator']);
366
- const payload = [
367
- {
368
- role: 'assistant',
369
- content: [
370
- {
371
- type: ContentTypes.TEXT,
372
- [ContentTypes.TEXT]: 'Let me help.',
373
- tool_call_ids: ['s1', 'c1'],
374
- },
375
- {
376
- type: ContentTypes.TOOL_CALL,
377
- tool_call: {
378
- id: 's1',
379
- name: 'search',
380
- args: '{"q":"test"}',
381
- output: 'Search results',
382
- },
383
- },
384
- {
385
- type: ContentTypes.TOOL_CALL,
386
- tool_call: {
387
- id: 'c1',
388
- name: 'calculator',
389
- args: '{"expr":"2+2"}',
390
- output: '4',
391
- },
392
- },
393
- ],
394
- },
395
- ];
396
- const result = formatAgentMessages(payload, undefined, tools);
397
- const toolMessages = result.messages.filter((m) => m._getType() === 'tool');
398
- expect(toolMessages.length).toBe(2);
399
- expect(toolMessages.map((m) => m.name).sort()).toEqual([
400
- 'calculator',
401
- 'search',
402
- ]);
403
- });
404
- it('should preserve discovered tools across multiple assistant messages', () => {
405
- /**
406
- * This test verifies that once tool_search discovers a tool, it remains valid
407
- * for all subsequent messages in the conversation, not just the current message.
408
- */
409
- const tools = new Set(['tool_search']);
410
- const payload = [
411
- {
412
- role: 'user',
413
- content: 'Find me a tool to list commits and use it',
414
- },
415
- {
416
- role: 'assistant',
417
- content: [
418
- {
419
- type: ContentTypes.TEXT,
420
- [ContentTypes.TEXT]: 'Let me search for that tool.',
421
- tool_call_ids: ['ts_1'],
422
- },
423
- {
424
- type: ContentTypes.TOOL_CALL,
425
- tool_call: {
426
- id: 'ts_1',
427
- name: 'tool_search',
428
- args: '{"query":"commits"}',
429
- output: '{"found": 1, "tools": [{"name": "list_commits_mcp_github"}]}',
430
- },
431
- },
432
- ],
433
- },
434
- {
435
- role: 'assistant',
436
- content: [
437
- {
438
- type: ContentTypes.TEXT,
439
- [ContentTypes.TEXT]: 'Now using the discovered tool.',
440
- tool_call_ids: ['lc_1'],
441
- },
442
- {
443
- type: ContentTypes.TOOL_CALL,
444
- tool_call: {
445
- id: 'lc_1',
446
- name: 'list_commits_mcp_github',
447
- args: '{"repo":"test"}',
448
- output: '[{"sha":"abc123","message":"Initial commit"}]',
449
- },
450
- },
451
- ],
452
- },
453
- {
454
- role: 'user',
455
- content: 'Show me more commits',
456
- },
457
- {
458
- role: 'assistant',
459
- content: [
460
- {
461
- type: ContentTypes.TEXT,
462
- [ContentTypes.TEXT]: 'Fetching more commits.',
463
- tool_call_ids: ['lc_2'],
464
- },
465
- {
466
- type: ContentTypes.TOOL_CALL,
467
- tool_call: {
468
- id: 'lc_2',
469
- name: 'list_commits_mcp_github',
470
- args: '{"repo":"test","page":2}',
471
- output: '[{"sha":"def456","message":"Second commit"}]',
472
- },
473
- },
474
- ],
475
- },
476
- ];
477
- const result = formatAgentMessages(payload, undefined, tools);
478
- /** All three tool calls should be preserved as ToolMessages */
479
- const toolMessages = result.messages.filter((m) => m._getType() === 'tool');
480
- expect(toolMessages.length).toBe(3);
481
- expect(toolMessages[0].name).toBe('tool_search');
482
- expect(toolMessages[1].name).toBe('list_commits_mcp_github');
483
- expect(toolMessages[2].name).toBe('list_commits_mcp_github');
484
- });
485
- it('should convert invalid tools to string while keeping valid tools as ToolMessages', () => {
486
- /**
487
- * This test documents the hybrid behavior:
488
- * - Valid tools remain as proper AIMessage + ToolMessage structures
489
- * - Invalid tools are converted to string and appended to text content
490
- * (preserving context without losing information)
491
- */
492
- const tools = new Set(['calculator']);
493
- const payload = [
494
- {
495
- role: 'assistant',
496
- content: [
497
- {
498
- type: ContentTypes.TEXT,
499
- [ContentTypes.TEXT]: 'I will use two tools.',
500
- tool_call_ids: ['calc_1', 'unknown_1'],
501
- },
502
- {
503
- type: ContentTypes.TOOL_CALL,
504
- tool_call: {
505
- id: 'calc_1',
506
- name: 'calculator',
507
- args: '{"expr":"2+2"}',
508
- output: '4',
509
- },
510
- },
511
- {
512
- type: ContentTypes.TOOL_CALL,
513
- tool_call: {
514
- id: 'unknown_1',
515
- name: 'some_unknown_tool',
516
- args: '{"query":"test"}',
517
- output: 'This is the result from unknown tool',
518
- },
519
- },
520
- ],
521
- },
522
- ];
523
- const result = formatAgentMessages(payload, undefined, tools);
524
- /** Should have AIMessage + ToolMessage for calculator */
525
- expect(result.messages.length).toBe(2);
526
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
527
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
528
- /** The valid tool should be kept */
529
- expect(result.messages[0].tool_calls).toHaveLength(1);
530
- expect(result.messages[0].tool_calls?.[0].name).toBe('calculator');
531
- expect(result.messages[1].name).toBe('calculator');
532
- /** The invalid tool should be converted to string in the content */
533
- const aiContent = result.messages[0].content;
534
- const aiContentStr = typeof aiContent === 'string' ? aiContent : JSON.stringify(aiContent);
535
- expect(aiContentStr).toContain('some_unknown_tool');
536
- expect(aiContentStr).toContain('This is the result from unknown tool');
537
- });
538
- it('should simulate realistic deferred tools flow with tool_search', () => {
539
- /**
540
- * This test simulates the real-world use case:
541
- * 1. Agent only has tool_search initially (deferred tools not in set)
542
- * 2. User asks to do something that requires a deferred tool
543
- * 3. Agent uses tool_search to discover the tool
544
- * 4. Agent then uses the discovered tool
545
- * 5. On subsequent conversation turns, both tool calls should be valid
546
- */
547
- const tools = new Set(['tool_search', 'execute_code']);
548
- const payload = [
549
- { role: 'user', content: 'List the recent commits from the repo' },
550
- {
551
- role: 'assistant',
552
- content: [
553
- {
554
- type: ContentTypes.TEXT,
555
- [ContentTypes.TEXT]: 'I need to find a tool for listing commits. Let me search.',
556
- tool_call_ids: ['search_1'],
557
- },
558
- {
559
- type: ContentTypes.TOOL_CALL,
560
- tool_call: {
561
- id: 'search_1',
562
- name: 'tool_search',
563
- args: '{"query":"git commits list"}',
564
- output: '{\n "found": 1,\n "tools": [\n {\n "name": "list_commits_mcp_github",\n "score": 0.95,\n "matched_in": "name",\n "snippet": "Lists commits from a GitHub repository"\n }\n ],\n "total_searched": 50,\n "query": "git commits list"\n}',
565
- },
566
- },
567
- {
568
- type: ContentTypes.TEXT,
569
- [ContentTypes.TEXT]: 'Found the tool! Now I will list the commits.',
570
- tool_call_ids: ['commits_1'],
571
- },
572
- {
573
- type: ContentTypes.TOOL_CALL,
574
- tool_call: {
575
- id: 'commits_1',
576
- name: 'list_commits_mcp_github',
577
- args: '{"owner":"librechat","repo":"librechat"}',
578
- output: '[{"sha":"abc123","message":"feat: add deferred tools"},{"sha":"def456","message":"fix: tool loading"}]',
579
- },
580
- },
581
- ],
582
- },
583
- ];
584
- const result = formatAgentMessages(payload, undefined, tools);
585
- /** Both tool_search and list_commits_mcp_github should be preserved */
586
- const toolMessages = result.messages.filter((m) => m._getType() === 'tool');
587
- expect(toolMessages.length).toBe(2);
588
- expect(toolMessages[0].name).toBe('tool_search');
589
- expect(toolMessages[1].name).toBe('list_commits_mcp_github');
590
- /** The AI messages should have proper tool_calls */
591
- const aiMessages = result.messages.filter((m) => m._getType() === 'ai');
592
- const toolCallNames = aiMessages.flatMap((m) => m.tool_calls?.map((tc) => tc.name) ?? []);
593
- expect(toolCallNames).toContain('tool_search');
594
- expect(toolCallNames).toContain('list_commits_mcp_github');
595
- });
596
- it.skip('should not produce two consecutive assistant messages and format content correctly', () => {
597
- const payload = [
598
- { role: 'user', content: 'Hello' },
599
- {
600
- role: 'assistant',
601
- content: [
602
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hi there!' },
603
- ],
604
- },
605
- {
606
- role: 'assistant',
607
- content: [
608
- {
609
- type: ContentTypes.TEXT,
610
- [ContentTypes.TEXT]: 'How can I help you?',
611
- },
612
- ],
613
- },
614
- { role: 'user', content: 'What\'s the weather?' },
615
- {
616
- role: 'assistant',
617
- content: [
618
- {
619
- type: ContentTypes.TEXT,
620
- [ContentTypes.TEXT]: 'Let me check that for you.',
621
- tool_call_ids: ['weather_1'],
622
- },
623
- {
624
- type: ContentTypes.TOOL_CALL,
625
- tool_call: {
626
- id: 'weather_1',
627
- name: 'check_weather',
628
- args: '{"location":"New York"}',
629
- output: 'Sunny, 75°F',
630
- },
631
- },
632
- ],
633
- },
634
- {
635
- role: 'assistant',
636
- content: [
637
- {
638
- type: ContentTypes.TEXT,
639
- [ContentTypes.TEXT]: 'Here\'s the weather information.',
640
- },
641
- ],
642
- },
643
- ];
644
- const result = formatAgentMessages(payload);
645
- // Check correct message count and types
646
- expect(result.messages).toHaveLength(6);
647
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
648
- expect(result.messages[1]).toBeInstanceOf(AIMessage);
649
- expect(result.messages[2]).toBeInstanceOf(HumanMessage);
650
- expect(result.messages[3]).toBeInstanceOf(AIMessage);
651
- expect(result.messages[4]).toBeInstanceOf(ToolMessage);
652
- expect(result.messages[5]).toBeInstanceOf(AIMessage);
653
- // Check content of messages
654
- expect(result.messages[0].content).toStrictEqual([
655
- { [ContentTypes.TEXT]: 'Hello', type: ContentTypes.TEXT },
656
- ]);
657
- expect(result.messages[1].content).toStrictEqual([
658
- { [ContentTypes.TEXT]: 'Hi there!', type: ContentTypes.TEXT },
659
- { [ContentTypes.TEXT]: 'How can I help you?', type: ContentTypes.TEXT },
660
- ]);
661
- expect(result.messages[2].content).toStrictEqual([
662
- { [ContentTypes.TEXT]: 'What\'s the weather?', type: ContentTypes.TEXT },
663
- ]);
664
- expect(result.messages[3].content).toBe('Let me check that for you.');
665
- expect(result.messages[4].content).toBe('Sunny, 75°F');
666
- expect(result.messages[5].content).toStrictEqual([
667
- {
668
- [ContentTypes.TEXT]: 'Here\'s the weather information.',
669
- type: ContentTypes.TEXT,
670
- },
671
- ]);
672
- // Check that there are no consecutive AIMessages
673
- const messageTypes = result.messages.map((message) => message.constructor);
674
- for (let i = 0; i < messageTypes.length - 1; i++) {
675
- expect(messageTypes[i] === AIMessage && messageTypes[i + 1] === AIMessage).toBe(false);
676
- }
677
- // Additional check to ensure the consecutive assistant messages were combined
678
- expect(result.messages[1].content).toHaveLength(2);
679
- });
680
- it('should strip THINK content and join TEXT parts as string', () => {
681
- const payload = [
682
- {
683
- role: 'assistant',
684
- content: [
685
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Initial response' },
686
- {
687
- type: ContentTypes.THINK,
688
- [ContentTypes.THINK]: 'Reasoning about the problem...',
689
- },
690
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
691
- ],
692
- },
693
- ];
694
- const result = formatAgentMessages(payload);
695
- expect(result.messages).toHaveLength(1);
696
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
697
- expect(result.messages[0].content).toEqual('Initial response\nFinal answer');
698
- });
699
- it('should join TEXT content as string when THINK content type is present', () => {
700
- const payload = [
701
- {
702
- role: 'assistant',
703
- content: [
704
- {
705
- type: ContentTypes.THINK,
706
- [ContentTypes.THINK]: 'Analyzing the problem...',
707
- },
708
- {
709
- type: ContentTypes.TEXT,
710
- [ContentTypes.TEXT]: 'First part of response',
711
- },
712
- {
713
- type: ContentTypes.TEXT,
714
- [ContentTypes.TEXT]: 'Second part of response',
715
- },
716
- {
717
- type: ContentTypes.TEXT,
718
- [ContentTypes.TEXT]: 'Final part of response',
719
- },
720
- ],
721
- },
722
- ];
723
- const result = formatAgentMessages(payload);
724
- expect(result.messages).toHaveLength(1);
725
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
726
- expect(typeof result.messages[0].content).toBe('string');
727
- expect(result.messages[0].content).toBe('First part of response\nSecond part of response\nFinal part of response');
728
- expect(result.messages[0].content).not.toContain('Analyzing the problem...');
729
- });
730
- it('should strip reasoning_content blocks and join TEXT parts as string', () => {
731
- const payload = [
732
- {
733
- role: 'assistant',
734
- content: [
735
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: '\n\n' },
736
- {
737
- type: ContentTypes.REASONING_CONTENT,
738
- reasoningText: { text: 'Thinking deeply...', signature: 'sig123' },
739
- index: 0,
740
- },
741
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'The answer is 42.' },
742
- ],
743
- },
744
- ];
745
- const result = formatAgentMessages(payload);
746
- expect(result.messages).toHaveLength(1);
747
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
748
- expect(result.messages[0].content).toBe('The answer is 42.');
749
- expect(JSON.stringify(result.messages[0].content)).not.toContain('reasoning_content');
750
- });
751
- it('should strip thinking blocks and join TEXT parts as string', () => {
752
- const payload = [
753
- {
754
- role: 'assistant',
755
- content: [
756
- {
757
- type: ContentTypes.THINKING,
758
- thinking: 'Internal reasoning...',
759
- signature: 'sig456',
760
- },
761
- {
762
- type: ContentTypes.TEXT,
763
- [ContentTypes.TEXT]: 'Here is my answer.',
764
- },
765
- ],
766
- },
767
- ];
768
- const result = formatAgentMessages(payload);
769
- expect(result.messages).toHaveLength(1);
770
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
771
- expect(result.messages[0].content).toBe('Here is my answer.');
772
- expect(JSON.stringify(result.messages[0].content)).not.toContain('thinking');
773
- });
774
- it('should strip redacted_thinking blocks and join TEXT parts as string', () => {
775
- const payload = [
776
- {
777
- role: 'assistant',
778
- content: [
779
- { type: 'redacted_thinking', data: 'REDACTED_SIGNATURE' },
780
- {
781
- type: ContentTypes.TEXT,
782
- [ContentTypes.TEXT]: 'Here is my answer.',
783
- },
784
- ],
785
- },
786
- ];
787
- const result = formatAgentMessages(payload);
788
- expect(result.messages).toHaveLength(1);
789
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
790
- expect(result.messages[0].content).toBe('Here is my answer.');
791
- expect(JSON.stringify(result.messages[0].content)).not.toContain('redacted_thinking');
792
- });
793
- it('should produce no AIMessage when only reasoning_content and whitespace text are present', () => {
794
- const payload = [
795
- {
796
- role: 'assistant',
797
- content: [
798
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: '\n\n' },
799
- {
800
- type: ContentTypes.REASONING_CONTENT,
801
- reasoningText: { text: 'Silent reasoning', signature: 'sig' },
802
- },
803
- ],
804
- },
805
- ];
806
- const result = formatAgentMessages(payload);
807
- expect(result.messages).toHaveLength(0);
808
- });
809
- it('should drop whitespace-only text parts from non-reasoning messages', () => {
810
- const payload = [
811
- {
812
- role: 'assistant',
813
- content: [
814
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: '\n\n' },
815
- {
816
- type: ContentTypes.TEXT,
817
- [ContentTypes.TEXT]: 'Actual content here.',
818
- },
819
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: ' ' },
820
- ],
821
- },
822
- ];
823
- const result = formatAgentMessages(payload);
824
- expect(result.messages).toHaveLength(1);
825
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
826
- const content = result.messages[0].content;
827
- expect(Array.isArray(content)).toBe(true);
828
- expect(content.every((p) => (p.text ?? '').trim() !== '')).toBe(true);
829
- });
830
- it('should preserve whitespace-only text that has tool_call_ids (common Bedrock pattern)', () => {
831
- const payload = [
832
- {
833
- role: 'assistant',
834
- content: [
835
- {
836
- type: ContentTypes.TEXT,
837
- [ContentTypes.TEXT]: '\n\n',
838
- tool_call_ids: ['tc-1'],
839
- },
840
- {
841
- type: ContentTypes.TOOL_CALL,
842
- tool_call: {
843
- id: 'tc-1',
844
- name: 'search',
845
- args: '{"query":"test"}',
846
- output: 'Results here',
847
- },
848
- },
849
- ],
850
- },
851
- ];
852
- const result = formatAgentMessages(payload);
853
- expect(result.messages).toHaveLength(2);
854
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
855
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
856
- expect(result.messages[0].tool_calls).toHaveLength(1);
857
- expect(result.messages[1].tool_call_id).toBe('tc-1');
858
- });
859
- it('should handle whitespace-only text without tool_call_ids before a tool call', () => {
860
- const payload = [
861
- {
862
- role: 'assistant',
863
- content: [
864
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: '\n\n' },
865
- {
866
- type: ContentTypes.TOOL_CALL,
867
- tool_call: {
868
- id: 'tc-2',
869
- name: 'search',
870
- args: '{"query":"test"}',
871
- output: 'Results here',
872
- },
873
- },
874
- ],
875
- },
876
- ];
877
- const result = formatAgentMessages(payload);
878
- expect(result.messages).toHaveLength(2);
879
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
880
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
881
- expect(result.messages[0].tool_calls).toHaveLength(1);
882
- });
883
- it('should exclude ERROR type content parts', () => {
884
- const payload = [
885
- {
886
- role: 'assistant',
887
- content: [
888
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
889
- {
890
- type: ContentTypes.ERROR,
891
- [ContentTypes.ERROR]: 'An error occurred while processing the request: Something went wrong',
892
- },
893
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
894
- ],
895
- },
896
- ];
897
- const result = formatAgentMessages(payload);
898
- expect(result.messages).toHaveLength(1);
899
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
900
- expect(result.messages[0].content).toEqual([
901
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
902
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
903
- ]);
904
- const hasErrorContent = Array.isArray(result.messages[0].content) &&
905
- result.messages[0].content.some((item) => item.type === ContentTypes.ERROR ||
906
- JSON.stringify(item).includes('An error occurred'));
907
- expect(hasErrorContent).toBe(false);
908
- });
909
- it('should handle indexTokenCountMap and return updated map', () => {
910
- const payload = [
911
- { role: 'user', content: 'Hello' },
912
- { role: 'assistant', content: 'Hi there!' },
913
- ];
914
- const indexTokenCountMap = {
915
- 0: 5, // 5 tokens for "Hello"
916
- 1: 10, // 10 tokens for "Hi there!"
917
- };
918
- const result = formatAgentMessages(payload, indexTokenCountMap);
919
- expect(result.messages).toHaveLength(2);
920
- expect(result.indexTokenCountMap).toBeDefined();
921
- expect(result.indexTokenCountMap?.[0]).toBe(5);
922
- expect(result.indexTokenCountMap?.[1]).toBe(10);
923
- });
924
- it('should handle complex message transformations with indexTokenCountMap', () => {
925
- const payload = [
926
- { role: 'user', content: 'What\'s the weather?' },
927
- {
928
- role: 'assistant',
929
- content: [
930
- {
931
- type: ContentTypes.TEXT,
932
- [ContentTypes.TEXT]: 'Let me check that for you.',
933
- tool_call_ids: ['weather_1'],
934
- },
935
- {
936
- type: ContentTypes.TOOL_CALL,
937
- tool_call: {
938
- id: 'weather_1',
939
- name: 'check_weather',
940
- args: '{"location":"New York"}',
941
- output: 'Sunny, 75°F',
942
- },
943
- },
944
- ],
945
- },
946
- ];
947
- const indexTokenCountMap = {
948
- 0: 10, // 10 tokens for "What's the weather?"
949
- 1: 50, // 50 tokens for the assistant message with tool call
950
- };
951
- const result = formatAgentMessages(payload, indexTokenCountMap);
952
- // The original message at index 1 should be split into two messages
953
- expect(result.messages).toHaveLength(3);
954
- expect(result.indexTokenCountMap).toBeDefined();
955
- expect(result.indexTokenCountMap?.[0]).toBe(10); // User message stays the same
956
- // The assistant message tokens should be distributed across the resulting messages
957
- const totalAssistantTokens = Object.values(result.indexTokenCountMap || {}).reduce((sum, count) => sum + count, 0) - 10; // Subtract user message tokens
958
- expect(totalAssistantTokens).toBe(50); // Should match the original token count
959
- });
960
- it('should handle one-to-many message expansion with tool calls', () => {
961
- // One message with multiple tool calls expands to multiple messages
962
- const payload = [
963
- {
964
- role: 'assistant',
965
- content: [
966
- {
967
- type: ContentTypes.TEXT,
968
- [ContentTypes.TEXT]: 'First tool call:',
969
- tool_call_ids: ['tool_1'],
970
- },
971
- {
972
- type: ContentTypes.TOOL_CALL,
973
- tool_call: {
974
- id: 'tool_1',
975
- name: 'search',
976
- args: '{"query":"test"}',
977
- output: 'Search result',
978
- },
979
- },
980
- {
981
- type: ContentTypes.TEXT,
982
- [ContentTypes.TEXT]: 'Second tool call:',
983
- tool_call_ids: ['tool_2'],
984
- },
985
- {
986
- type: ContentTypes.TOOL_CALL,
987
- tool_call: {
988
- id: 'tool_2',
989
- name: 'calculate',
990
- args: '{"expression":"1+1"}',
991
- output: '2',
992
- },
993
- },
994
- {
995
- type: ContentTypes.TEXT,
996
- [ContentTypes.TEXT]: 'Final response',
997
- },
998
- ],
999
- },
1000
- ];
1001
- const indexTokenCountMap = {
1002
- 0: 100, // 100 tokens for the complex assistant message
1003
- };
1004
- const result = formatAgentMessages(payload, indexTokenCountMap);
1005
- // One message expands to 5 messages (2 tool calls + text before, between, and after)
1006
- expect(result.messages).toHaveLength(5);
1007
- expect(result.indexTokenCountMap).toBeDefined();
1008
- // The sum of all token counts should equal the original
1009
- const totalTokens = Object.values(result.indexTokenCountMap || {}).reduce((sum, count) => sum + count, 0);
1010
- expect(totalTokens).toBe(100);
1011
- // Check that each resulting message has a token count
1012
- for (let i = 0; i < result.messages.length; i++) {
1013
- expect(result.indexTokenCountMap?.[i]).toBeDefined();
1014
- }
1015
- });
1016
- it('should handle content filtering that reduces message count', () => {
1017
- // Message with THINK and ERROR parts that get filtered out
1018
- const payload = [
1019
- {
1020
- role: 'assistant',
1021
- content: [
1022
- { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Thinking...' },
1023
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Visible response' },
1024
- { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'Error occurred' },
1025
- ],
1026
- },
1027
- ];
1028
- const indexTokenCountMap = {
1029
- 0: 60, // 60 tokens for the message with filtered content
1030
- };
1031
- const result = formatAgentMessages(payload, indexTokenCountMap);
1032
- // Only one message should remain after filtering
1033
- expect(result.messages).toHaveLength(1);
1034
- expect(result.indexTokenCountMap).toBeDefined();
1035
- // All tokens should be assigned to the remaining message
1036
- expect(result.indexTokenCountMap?.[0]).toBe(60);
1037
- });
1038
- it('should handle empty result after content filtering', () => {
1039
- // Message with only THINK and ERROR parts that all get filtered out
1040
- const payload = [
1041
- {
1042
- role: 'assistant',
1043
- content: [
1044
- { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Thinking...' },
1045
- { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'Error occurred' },
1046
- { type: ContentTypes.AGENT_UPDATE, update: 'Processing...' },
1047
- ],
1048
- },
1049
- ];
1050
- const indexTokenCountMap = {
1051
- 0: 40, // 40 tokens for the message with filtered content
1052
- };
1053
- const result = formatAgentMessages(payload, indexTokenCountMap);
1054
- // No messages should remain after filtering
1055
- expect(result.messages).toHaveLength(0);
1056
- expect(result.indexTokenCountMap).toBeDefined();
1057
- // The token count map should be empty since there are no messages
1058
- expect(Object.keys(result.indexTokenCountMap || {})).toHaveLength(0);
1059
- });
1060
- it('should demonstrate how 2 input messages can become more than 2 output messages', () => {
1061
- // Two input messages where one contains tool calls
1062
- const payload = [
1063
- { role: 'user', content: 'Can you help me with something?' },
1064
- {
1065
- role: 'assistant',
1066
- content: [
1067
- {
1068
- type: ContentTypes.TEXT,
1069
- [ContentTypes.TEXT]: 'I\'ll help you with that.',
1070
- tool_call_ids: ['tool_1'],
1071
- },
1072
- {
1073
- type: ContentTypes.TOOL_CALL,
1074
- tool_call: {
1075
- id: 'tool_1',
1076
- name: 'search',
1077
- args: '{"query":"help topics"}',
1078
- output: 'Found several help topics.',
1079
- },
1080
- },
1081
- ],
1082
- },
1083
- ];
1084
- const indexTokenCountMap = {
1085
- 0: 15, // 15 tokens for the user message
1086
- 1: 45, // 45 tokens for the assistant message with tool call
1087
- };
1088
- const result = formatAgentMessages(payload, indexTokenCountMap);
1089
- // 2 input messages become 3 output messages (user + assistant + tool)
1090
- expect(payload).toHaveLength(2);
1091
- expect(result.messages).toHaveLength(3);
1092
- expect(result.indexTokenCountMap).toBeDefined();
1093
- expect(Object.keys(result.indexTokenCountMap ?? {}).length).toBe(3);
1094
- // Check message types
1095
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1096
- expect(result.messages[1]).toBeInstanceOf(AIMessage);
1097
- expect(result.messages[2]).toBeInstanceOf(ToolMessage);
1098
- // The sum of all token counts should equal the original total
1099
- const totalTokens = Object.values(result.indexTokenCountMap || {}).reduce((sum, count) => sum + count, 0);
1100
- expect(totalTokens).toBe(60); // 15 + 45
1101
- });
1102
- it('should handle an AI message with 5 tool calls in a single message', () => {
1103
- const payload = [
1104
- {
1105
- role: 'assistant',
1106
- content: [
1107
- {
1108
- type: ContentTypes.TEXT,
1109
- [ContentTypes.TEXT]: 'I\'ll perform multiple operations for you.',
1110
- tool_call_ids: ['tool_1', 'tool_2', 'tool_3', 'tool_4', 'tool_5'],
1111
- },
1112
- {
1113
- type: ContentTypes.TOOL_CALL,
1114
- tool_call: {
1115
- id: 'tool_1',
1116
- name: 'search',
1117
- args: '{"query":"latest news"}',
1118
- output: 'Found several news articles.',
1119
- },
1120
- },
1121
- {
1122
- type: ContentTypes.TOOL_CALL,
1123
- tool_call: {
1124
- id: 'tool_2',
1125
- name: 'check_weather',
1126
- args: '{"location":"New York"}',
1127
- output: 'Sunny, 75°F',
1128
- },
1129
- },
1130
- {
1131
- type: ContentTypes.TOOL_CALL,
1132
- tool_call: {
1133
- id: 'tool_3',
1134
- name: 'calculate',
1135
- args: '{"expression":"356 * 24"}',
1136
- output: '8544',
1137
- },
1138
- },
1139
- {
1140
- type: ContentTypes.TOOL_CALL,
1141
- tool_call: {
1142
- id: 'tool_4',
1143
- name: 'translate',
1144
- args: '{"text":"Hello world","source":"en","target":"fr"}',
1145
- output: 'Bonjour le monde',
1146
- },
1147
- },
1148
- {
1149
- type: ContentTypes.TOOL_CALL,
1150
- tool_call: {
1151
- id: 'tool_5',
1152
- name: 'fetch_data',
1153
- args: '{"endpoint":"/api/users","params":{"limit":5}}',
1154
- output: '{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"},{"id":4,"name":"David"},{"id":5,"name":"Eve"}]}',
1155
- },
1156
- },
1157
- ],
1158
- },
1159
- ];
1160
- const result = formatAgentMessages(payload);
1161
- // Should have 6 messages: 1 AIMessage and 5 ToolMessages
1162
- expect(result.messages).toHaveLength(6);
1163
- // Check message types in the correct sequence
1164
- expect(result.messages[0]).toBeInstanceOf(AIMessage); // Initial message with all tool calls
1165
- expect(result.messages[1]).toBeInstanceOf(ToolMessage); // Tool 1 response
1166
- expect(result.messages[2]).toBeInstanceOf(ToolMessage); // Tool 2 response
1167
- expect(result.messages[3]).toBeInstanceOf(ToolMessage); // Tool 3 response
1168
- expect(result.messages[4]).toBeInstanceOf(ToolMessage); // Tool 4 response
1169
- expect(result.messages[5]).toBeInstanceOf(ToolMessage); // Tool 5 response
1170
- // Check AIMessage has all 5 tool calls
1171
- expect(result.messages[0].content).toBe('I\'ll perform multiple operations for you.');
1172
- expect(result.messages[0].tool_calls).toHaveLength(5);
1173
- // Verify each tool call in the AIMessage
1174
- expect(result.messages[0].tool_calls?.[0]).toEqual({
1175
- id: 'tool_1',
1176
- name: 'search',
1177
- args: { query: 'latest news' },
1178
- });
1179
- expect(result.messages[0].tool_calls?.[1]).toEqual({
1180
- id: 'tool_2',
1181
- name: 'check_weather',
1182
- args: { location: 'New York' },
1183
- });
1184
- expect(result.messages[0].tool_calls?.[2]).toEqual({
1185
- id: 'tool_3',
1186
- name: 'calculate',
1187
- args: { expression: '356 * 24' },
1188
- });
1189
- expect(result.messages[0].tool_calls?.[3]).toEqual({
1190
- id: 'tool_4',
1191
- name: 'translate',
1192
- args: { text: 'Hello world', source: 'en', target: 'fr' },
1193
- });
1194
- expect(result.messages[0].tool_calls?.[4]).toEqual({
1195
- id: 'tool_5',
1196
- name: 'fetch_data',
1197
- args: { endpoint: '/api/users', params: { limit: 5 } },
1198
- });
1199
- // Check each ToolMessage
1200
- expect(result.messages[1].tool_call_id).toBe('tool_1');
1201
- expect(result.messages[1].name).toBe('search');
1202
- expect(result.messages[1].content).toBe('Found several news articles.');
1203
- expect(result.messages[2].tool_call_id).toBe('tool_2');
1204
- expect(result.messages[2].name).toBe('check_weather');
1205
- expect(result.messages[2].content).toBe('Sunny, 75°F');
1206
- expect(result.messages[3].tool_call_id).toBe('tool_3');
1207
- expect(result.messages[3].name).toBe('calculate');
1208
- expect(result.messages[3].content).toBe('8544');
1209
- expect(result.messages[4].tool_call_id).toBe('tool_4');
1210
- expect(result.messages[4].name).toBe('translate');
1211
- expect(result.messages[4].content).toBe('Bonjour le monde');
1212
- expect(result.messages[5].tool_call_id).toBe('tool_5');
1213
- expect(result.messages[5].name).toBe('fetch_data');
1214
- expect(result.messages[5].content).toBe('{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"},{"id":4,"name":"David"},{"id":5,"name":"Eve"}]}');
1215
- });
1216
- it('should heal tool call structure with thinking content', () => {
1217
- const payload = [
1218
- {
1219
- role: 'assistant',
1220
- content: [
1221
- {
1222
- type: ContentTypes.THINK,
1223
- [ContentTypes.THINK]: 'I\'ll add this agreement as an observation to our existing troubleshooting task in the project memory system.',
1224
- },
1225
- {
1226
- type: ContentTypes.TOOL_CALL,
1227
- tool_call: {
1228
- id: 'tooluse_Zz-mw_wHTrWTvDHaCbfaZg',
1229
- name: 'add_observations_mcp_project-memory',
1230
- args: '{"observations":[{"entityName":"MCP_Tool_Error_Troubleshooting","contents":["Agreement established: Document all future tests in the project memory system to maintain a comprehensive troubleshooting log","This will provide a structured record of the entire troubleshooting process and help identify patterns in the error behavior"]}]}',
1231
- type: 'tool_call',
1232
- progress: 1,
1233
- output: '[\n {\n "entityName": "MCP_Tool_Error_Troubleshooting",\n "addedObservations": [\n {\n "content": "Agreement established: Document all future tests in the project memory system to maintain a comprehensive troubleshooting log",\n "timestamp": "2025-03-26T00:46:42.154Z"\n },\n {\n "content": "This will provide a structured record of the entire troubleshooting process and help identify patterns in the error behavior",\n "timestamp": "2025-03-26T00:46:42.154Z"\n }\n ]\n }\n]',
1234
- },
1235
- },
1236
- {
1237
- type: ContentTypes.TEXT,
1238
- [ContentTypes.TEXT]: '\n\nI\'ve successfully added our agreement to the project memory system. The observation has been recorded in the "MCP_Tool_Error_Troubleshooting" entity with the current timestamp.\n\nGoing forward, I will:\n\n1. Document each test we perform\n2. Record the methodology and results\n3. Update the project memory with our findings\n4. Establish appropriate relationships between tests and related components\n5. Provide a summary of what we\'ve learned from each test\n\nThis structured approach will help us build a comprehensive knowledge base of the error behavior and our troubleshooting process, which may prove valuable for resolving similar issues in the future or for other developers facing similar challenges.\n\nWhat test would you like to perform next in our troubleshooting process?',
1239
- },
1240
- ],
1241
- },
1242
- ];
1243
- const result = formatAgentMessages(payload);
1244
- // Should have 3 messages: an AIMessage with empty content, a ToolMessage, and a final AIMessage with the text
1245
- expect(result.messages).toHaveLength(3);
1246
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
1247
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1248
- expect(result.messages[2]).toBeInstanceOf(AIMessage);
1249
- // The first AIMessage should have an empty content and the tool_call
1250
- expect(result.messages[0].content).toBe('');
1251
- expect(result.messages[0].tool_calls).toHaveLength(1);
1252
- expect(result.messages[0].tool_calls?.[0].name).toBe('add_observations_mcp_project-memory');
1253
- // The ToolMessage should have the correct properties
1254
- expect(result.messages[1].tool_call_id).toBe('tooluse_Zz-mw_wHTrWTvDHaCbfaZg');
1255
- expect(result.messages[1].name).toBe('add_observations_mcp_project-memory');
1256
- expect(result.messages[1].content).toContain('MCP_Tool_Error_Troubleshooting');
1257
- // The final AIMessage should contain the text response
1258
- expect(typeof result.messages[2].content).toBe('string');
1259
- expect(result.messages[2].content.trim()).toContain('I\'ve successfully added our agreement to the project memory system');
1260
- });
1261
- it('should demonstrate how messages can be filtered out, reducing count', () => {
1262
- // Two input messages where one gets completely filtered out
1263
- const payload = [
1264
- { role: 'user', content: 'Hello there' },
1265
- {
1266
- role: 'assistant',
1267
- content: [
1268
- {
1269
- type: ContentTypes.THINK,
1270
- [ContentTypes.THINK]: 'Thinking about response...',
1271
- },
1272
- {
1273
- type: ContentTypes.ERROR,
1274
- [ContentTypes.ERROR]: 'Error in processing',
1275
- },
1276
- { type: ContentTypes.AGENT_UPDATE, update: 'Working on it...' },
1277
- ],
1278
- },
1279
- ];
1280
- const indexTokenCountMap = {
1281
- 0: 10, // 10 tokens for the user message
1282
- 1: 30, // 30 tokens for the assistant message that will be filtered out
1283
- };
1284
- const result = formatAgentMessages(payload, indexTokenCountMap);
1285
- // 2 input messages become 1 output message (only the user message remains)
1286
- expect(payload).toHaveLength(2);
1287
- expect(result.messages).toHaveLength(1);
1288
- expect(result.indexTokenCountMap).toBeDefined();
1289
- expect(Object.keys(result.indexTokenCountMap ?? {}).length).toBe(1);
1290
- // Check message type
1291
- expect(result.messages[0]).toBeInstanceOf(HumanMessage);
1292
- // Only the user message tokens should remain
1293
- expect(result.indexTokenCountMap?.[0]).toBe(10);
1294
- // The total tokens should be just the user message tokens
1295
- const totalTokens = Object.values(result.indexTokenCountMap || {}).reduce((sum, count) => sum + count, 0);
1296
- expect(totalTokens).toBe(10);
1297
- });
1298
- it('should skip invalid tool calls with no name AND no output', () => {
1299
- const payload = [
1300
- {
1301
- role: 'assistant',
1302
- content: [
1303
- {
1304
- type: ContentTypes.TEXT,
1305
- [ContentTypes.TEXT]: 'Let me help you with that.',
1306
- tool_call_ids: ['valid_tool_1'],
1307
- },
1308
- {
1309
- type: ContentTypes.TOOL_CALL,
1310
- tool_call: {
1311
- id: 'invalid_tool_1',
1312
- name: '',
1313
- args: '{"query":"test"}',
1314
- output: '',
1315
- },
1316
- },
1317
- {
1318
- type: ContentTypes.TOOL_CALL,
1319
- tool_call: {
1320
- id: 'valid_tool_1',
1321
- name: 'search',
1322
- args: '{"query":"weather"}',
1323
- output: 'The weather is sunny.',
1324
- },
1325
- },
1326
- ],
1327
- },
1328
- ];
1329
- const result = formatAgentMessages(payload);
1330
- // Should have 2 messages: AIMessage and ToolMessage (invalid tool call is skipped)
1331
- expect(result.messages).toHaveLength(2);
1332
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
1333
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1334
- // The AIMessage should only have 1 tool call (the valid one)
1335
- expect(result.messages[0].tool_calls).toHaveLength(1);
1336
- expect(result.messages[0].tool_calls?.[0].name).toBe('search');
1337
- expect(result.messages[0].tool_calls?.[0].id).toBe('valid_tool_1');
1338
- // The ToolMessage should be for the valid tool call
1339
- expect(result.messages[1].tool_call_id).toBe('valid_tool_1');
1340
- expect(result.messages[1].name).toBe('search');
1341
- expect(result.messages[1].content).toBe('The weather is sunny.');
1342
- });
1343
- it('should skip tool calls with no name AND null output', () => {
1344
- const payload = [
1345
- {
1346
- role: 'assistant',
1347
- content: [
1348
- {
1349
- type: ContentTypes.TOOL_CALL,
1350
- tool_call: {
1351
- id: 'invalid_tool_1',
1352
- name: '',
1353
- args: '{"query":"test"}',
1354
- output: null,
1355
- },
1356
- },
1357
- {
1358
- type: ContentTypes.TEXT,
1359
- [ContentTypes.TEXT]: 'Here is the information.',
1360
- },
1361
- ],
1362
- },
1363
- ];
1364
- const result = formatAgentMessages(payload);
1365
- // Should have 1 message: AIMessage (invalid tool call is skipped)
1366
- expect(result.messages).toHaveLength(1);
1367
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
1368
- // The AIMessage should have no tool calls or an empty array
1369
- const toolCalls = result.messages[0].tool_calls;
1370
- expect(toolCalls === undefined || toolCalls.length === 0).toBe(true);
1371
- expect(result.messages[0].content).toStrictEqual([
1372
- {
1373
- type: ContentTypes.TEXT,
1374
- [ContentTypes.TEXT]: 'Here is the information.',
1375
- },
1376
- ]);
1377
- });
1378
- it('should NOT skip tool calls with no name but valid output', () => {
1379
- const payload = [
1380
- {
1381
- role: 'assistant',
1382
- content: [
1383
- {
1384
- type: ContentTypes.TOOL_CALL,
1385
- tool_call: {
1386
- id: 'tool_1',
1387
- name: '',
1388
- args: '{"query":"test"}',
1389
- output: 'Valid output despite missing name',
1390
- },
1391
- },
1392
- ],
1393
- },
1394
- ];
1395
- const result = formatAgentMessages(payload);
1396
- // Should have 2 messages: AIMessage and ToolMessage
1397
- expect(result.messages).toHaveLength(2);
1398
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
1399
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1400
- // The AIMessage should have 1 tool call
1401
- expect(result.messages[0].tool_calls).toHaveLength(1);
1402
- // The ToolMessage should have the output
1403
- expect(result.messages[1].tool_call_id).toBe('tool_1');
1404
- expect(result.messages[1].content).toBe('Valid output despite missing name');
1405
- });
1406
- it('should NOT skip tool calls with valid name but no output', () => {
1407
- const payload = [
1408
- {
1409
- role: 'assistant',
1410
- content: [
1411
- {
1412
- type: ContentTypes.TOOL_CALL,
1413
- tool_call: {
1414
- id: 'tool_1',
1415
- name: 'search',
1416
- args: '{"query":"test"}',
1417
- output: '',
1418
- },
1419
- },
1420
- ],
1421
- },
1422
- ];
1423
- const result = formatAgentMessages(payload);
1424
- // Should have 2 messages: AIMessage and ToolMessage
1425
- expect(result.messages).toHaveLength(2);
1426
- expect(result.messages[0]).toBeInstanceOf(AIMessage);
1427
- expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1428
- // The AIMessage should have 1 tool call
1429
- expect(result.messages[0].tool_calls).toHaveLength(1);
1430
- expect(result.messages[0].tool_calls?.[0].name).toBe('search');
1431
- // The ToolMessage should have empty content
1432
- expect(result.messages[1].tool_call_id).toBe('tool_1');
1433
- expect(result.messages[1].name).toBe('search');
1434
- expect(result.messages[1].content).toBe('');
1435
- });
1436
- describe('proportional token distribution', () => {
1437
- it('should distribute tokens proportionally based on content length', () => {
1438
- const payload = [
1439
- {
1440
- role: 'assistant',
1441
- content: [
1442
- {
1443
- type: ContentTypes.TEXT,
1444
- [ContentTypes.TEXT]: 'Short text',
1445
- tool_call_ids: ['tool_1'],
1446
- },
1447
- {
1448
- type: ContentTypes.TOOL_CALL,
1449
- tool_call: {
1450
- id: 'tool_1',
1451
- name: 'search',
1452
- args: '{"query":"test"}',
1453
- output: 'A much longer tool result that contains significantly more content than the original message text',
1454
- },
1455
- },
1456
- ],
1457
- },
1458
- ];
1459
- const indexTokenCountMap = { 0: 100 };
1460
- const result = formatAgentMessages(payload, indexTokenCountMap);
1461
- expect(result.messages).toHaveLength(2);
1462
- const aiTokens = result.indexTokenCountMap?.[0] ?? 0;
1463
- const toolTokens = result.indexTokenCountMap?.[1] ?? 0;
1464
- expect(aiTokens + toolTokens).toBe(100);
1465
- expect(toolTokens).toBeGreaterThan(aiTokens);
1466
- });
1467
- it('should give the vast majority of tokens to a large tool result vs tiny AI message', () => {
1468
- const bigOutput = 'x'.repeat(10000);
1469
- const payload = [
1470
- {
1471
- role: 'assistant',
1472
- content: [
1473
- {
1474
- type: ContentTypes.TEXT,
1475
- [ContentTypes.TEXT]: 'ok',
1476
- tool_call_ids: ['tool_1'],
1477
- },
1478
- {
1479
- type: ContentTypes.TOOL_CALL,
1480
- tool_call: {
1481
- id: 'tool_1',
1482
- name: 'snapshot',
1483
- args: '{}',
1484
- output: bigOutput,
1485
- },
1486
- },
1487
- ],
1488
- },
1489
- ];
1490
- const indexTokenCountMap = { 0: 5000 };
1491
- const result = formatAgentMessages(payload, indexTokenCountMap);
1492
- expect(result.messages).toHaveLength(2);
1493
- const aiTokens = result.indexTokenCountMap?.[0] ?? 0;
1494
- const toolTokens = result.indexTokenCountMap?.[1] ?? 0;
1495
- expect(aiTokens + toolTokens).toBe(5000);
1496
- expect(toolTokens).toBeGreaterThan(4900);
1497
- expect(aiTokens).toBeLessThan(100);
1498
- });
1499
- it('should fall back to even distribution when all content lengths are zero', () => {
1500
- const payload = [
1501
- {
1502
- role: 'assistant',
1503
- content: [
1504
- {
1505
- type: ContentTypes.TEXT,
1506
- [ContentTypes.TEXT]: '',
1507
- tool_call_ids: ['tool_1'],
1508
- },
1509
- {
1510
- type: ContentTypes.TOOL_CALL,
1511
- tool_call: {
1512
- id: 'tool_1',
1513
- name: 'noop',
1514
- args: '{}',
1515
- output: '',
1516
- },
1517
- },
1518
- ],
1519
- },
1520
- ];
1521
- const indexTokenCountMap = { 0: 20 };
1522
- const result = formatAgentMessages(payload, indexTokenCountMap);
1523
- expect(result.messages).toHaveLength(2);
1524
- const aiTokens = result.indexTokenCountMap?.[0] ?? 0;
1525
- const toolTokens = result.indexTokenCountMap?.[1] ?? 0;
1526
- expect(aiTokens + toolTokens).toBe(20);
1527
- expect(aiTokens).toBeGreaterThanOrEqual(0);
1528
- expect(toolTokens).toBeGreaterThanOrEqual(0);
1529
- });
1530
- it('should handle odd token counts without losing remainder', () => {
1531
- const payload = [
1532
- {
1533
- role: 'assistant',
1534
- content: [
1535
- {
1536
- type: ContentTypes.TEXT,
1537
- [ContentTypes.TEXT]: 'abc',
1538
- tool_call_ids: ['tool_1', 'tool_2', 'tool_3'],
1539
- },
1540
- {
1541
- type: ContentTypes.TOOL_CALL,
1542
- tool_call: {
1543
- id: 'tool_1',
1544
- name: 'a',
1545
- args: '{}',
1546
- output: 'abc',
1547
- },
1548
- },
1549
- {
1550
- type: ContentTypes.TOOL_CALL,
1551
- tool_call: {
1552
- id: 'tool_2',
1553
- name: 'b',
1554
- args: '{}',
1555
- output: 'abc',
1556
- },
1557
- },
1558
- {
1559
- type: ContentTypes.TOOL_CALL,
1560
- tool_call: {
1561
- id: 'tool_3',
1562
- name: 'c',
1563
- args: '{}',
1564
- output: 'abc',
1565
- },
1566
- },
1567
- ],
1568
- },
1569
- ];
1570
- const indexTokenCountMap = { 0: 7 };
1571
- const result = formatAgentMessages(payload, indexTokenCountMap);
1572
- expect(result.messages).toHaveLength(4);
1573
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1574
- expect(total).toBe(7);
1575
- for (let i = 0; i < result.messages.length; i++) {
1576
- expect(result.indexTokenCountMap?.[i]).toBeGreaterThanOrEqual(0);
1577
- }
1578
- });
1579
- it('should never produce negative token counts', () => {
1580
- const payload = [
1581
- {
1582
- role: 'assistant',
1583
- content: [
1584
- {
1585
- type: ContentTypes.TEXT,
1586
- [ContentTypes.TEXT]: 'a',
1587
- tool_call_ids: ['t1', 't2', 't3', 't4', 't5'],
1588
- },
1589
- {
1590
- type: ContentTypes.TOOL_CALL,
1591
- tool_call: { id: 't1', name: 'x', args: '{}', output: 'b' },
1592
- },
1593
- {
1594
- type: ContentTypes.TOOL_CALL,
1595
- tool_call: { id: 't2', name: 'x', args: '{}', output: 'c' },
1596
- },
1597
- {
1598
- type: ContentTypes.TOOL_CALL,
1599
- tool_call: { id: 't3', name: 'x', args: '{}', output: 'd' },
1600
- },
1601
- {
1602
- type: ContentTypes.TOOL_CALL,
1603
- tool_call: { id: 't4', name: 'x', args: '{}', output: 'e' },
1604
- },
1605
- {
1606
- type: ContentTypes.TOOL_CALL,
1607
- tool_call: { id: 't5', name: 'x', args: '{}', output: 'f' },
1608
- },
1609
- ],
1610
- },
1611
- ];
1612
- const indexTokenCountMap = { 0: 3 };
1613
- const result = formatAgentMessages(payload, indexTokenCountMap);
1614
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1615
- expect(total).toBe(3);
1616
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1617
- expect(val).toBeGreaterThanOrEqual(0);
1618
- }
1619
- });
1620
- it('should handle single token budget distributed across many messages', () => {
1621
- const payload = [
1622
- {
1623
- role: 'assistant',
1624
- content: [
1625
- {
1626
- type: ContentTypes.TEXT,
1627
- [ContentTypes.TEXT]: 'hello',
1628
- tool_call_ids: ['t1', 't2'],
1629
- },
1630
- {
1631
- type: ContentTypes.TOOL_CALL,
1632
- tool_call: {
1633
- id: 't1',
1634
- name: 'a',
1635
- args: '{}',
1636
- output: 'result one',
1637
- },
1638
- },
1639
- {
1640
- type: ContentTypes.TOOL_CALL,
1641
- tool_call: {
1642
- id: 't2',
1643
- name: 'b',
1644
- args: '{}',
1645
- output: 'result two',
1646
- },
1647
- },
1648
- ],
1649
- },
1650
- ];
1651
- const indexTokenCountMap = { 0: 1 };
1652
- const result = formatAgentMessages(payload, indexTokenCountMap);
1653
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1654
- expect(total).toBe(1);
1655
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1656
- expect(val).toBeGreaterThanOrEqual(0);
1657
- }
1658
- });
1659
- it('should handle zero token budget', () => {
1660
- const payload = [
1661
- {
1662
- role: 'assistant',
1663
- content: [
1664
- {
1665
- type: ContentTypes.TEXT,
1666
- [ContentTypes.TEXT]: 'hello',
1667
- tool_call_ids: ['t1'],
1668
- },
1669
- {
1670
- type: ContentTypes.TOOL_CALL,
1671
- tool_call: { id: 't1', name: 'a', args: '{}', output: 'world' },
1672
- },
1673
- ],
1674
- },
1675
- ];
1676
- const indexTokenCountMap = { 0: 0 };
1677
- const result = formatAgentMessages(payload, indexTokenCountMap);
1678
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1679
- expect(total).toBe(0);
1680
- });
1681
- it('should distribute tokens proportionally with 5 tool calls of varying sizes', () => {
1682
- const payload = [
1683
- {
1684
- role: 'assistant',
1685
- content: [
1686
- {
1687
- type: ContentTypes.TEXT,
1688
- [ContentTypes.TEXT]: 'I will perform multiple operations.',
1689
- tool_call_ids: ['t1', 't2', 't3', 't4', 't5'],
1690
- },
1691
- {
1692
- type: ContentTypes.TOOL_CALL,
1693
- tool_call: {
1694
- id: 't1',
1695
- name: 'navigate',
1696
- args: '{"url":"https://example.com"}',
1697
- output: 'Navigated successfully.',
1698
- },
1699
- },
1700
- {
1701
- type: ContentTypes.TOOL_CALL,
1702
- tool_call: {
1703
- id: 't2',
1704
- name: 'snapshot',
1705
- args: '{}',
1706
- output: 'x'.repeat(5000),
1707
- },
1708
- },
1709
- {
1710
- type: ContentTypes.TOOL_CALL,
1711
- tool_call: {
1712
- id: 't3',
1713
- name: 'click',
1714
- args: '{"selector":"#btn"}',
1715
- output: 'Clicked.',
1716
- },
1717
- },
1718
- {
1719
- type: ContentTypes.TOOL_CALL,
1720
- tool_call: {
1721
- id: 't4',
1722
- name: 'snapshot',
1723
- args: '{}',
1724
- output: 'y'.repeat(8000),
1725
- },
1726
- },
1727
- {
1728
- type: ContentTypes.TOOL_CALL,
1729
- tool_call: {
1730
- id: 't5',
1731
- name: 'extract',
1732
- args: '{"selector":"h1"}',
1733
- output: 'Page Title',
1734
- },
1735
- },
1736
- ],
1737
- },
1738
- ];
1739
- const indexTokenCountMap = { 0: 3000 };
1740
- const result = formatAgentMessages(payload, indexTokenCountMap);
1741
- expect(result.messages).toHaveLength(6);
1742
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1743
- expect(total).toBe(3000);
1744
- const snapshotIdx1 = 2;
1745
- const snapshotIdx2 = 4;
1746
- const bigSnapshotTokens = (result.indexTokenCountMap?.[snapshotIdx1] ?? 0) +
1747
- (result.indexTokenCountMap?.[snapshotIdx2] ?? 0);
1748
- expect(bigSnapshotTokens).toBeGreaterThan(2500);
1749
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1750
- expect(val).toBeGreaterThanOrEqual(0);
1751
- }
1752
- });
1753
- it('should handle HN-like payload: AI with 18 tool calls and large snapshot results', () => {
1754
- const smallOutput = 'Successfully navigated to page.';
1755
- const hugeSnapshot = 'uid=8_0 RootWebArea ' + 'x'.repeat(20000);
1756
- const toolCalls = [];
1757
- const toolCallIds = [];
1758
- for (let i = 0; i < 18; i++) {
1759
- const id = `tool_${i}`;
1760
- toolCallIds.push(id);
1761
- const isSnapshot = i % 3 === 1;
1762
- toolCalls.push({
1763
- type: ContentTypes.TOOL_CALL,
1764
- tool_call: {
1765
- id,
1766
- name: isSnapshot ? 'take_snapshot' : 'navigate_page',
1767
- args: isSnapshot ? '{}' : `{"url":"https://example.com/${i}"}`,
1768
- output: isSnapshot ? hugeSnapshot : smallOutput,
1769
- },
1770
- });
1771
- }
1772
- const payload = [
1773
- {
1774
- role: 'user',
1775
- content: 'Look up top 5 posts on HN',
1776
- },
1777
- {
1778
- role: 'assistant',
1779
- content: [
1780
- {
1781
- type: ContentTypes.TEXT,
1782
- [ContentTypes.TEXT]: '',
1783
- tool_call_ids: toolCallIds,
1784
- },
1785
- ...toolCalls,
1786
- ],
1787
- },
1788
- ];
1789
- const indexTokenCountMap = { 0: 20, 1: 10000 };
1790
- const result = formatAgentMessages(payload, indexTokenCountMap);
1791
- expect(result.messages.length).toBeGreaterThan(2);
1792
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1793
- expect(total).toBe(10020);
1794
- expect(result.indexTokenCountMap?.[0]).toBe(20);
1795
- let snapshotTokenTotal = 0;
1796
- let navTokenTotal = 0;
1797
- for (let i = 1; i < result.messages.length; i++) {
1798
- const tokens = result.indexTokenCountMap?.[i] ?? 0;
1799
- expect(tokens).toBeGreaterThanOrEqual(0);
1800
- if (result.messages[i] instanceof ToolMessage) {
1801
- const content = result.messages[i].content;
1802
- if (typeof content === 'string' && content.length > 1000) {
1803
- snapshotTokenTotal += tokens;
1804
- }
1805
- else {
1806
- navTokenTotal += tokens;
1807
- }
1808
- }
1809
- }
1810
- expect(snapshotTokenTotal).toBeGreaterThan(navTokenTotal);
1811
- });
1812
- it('should complete proportional distribution within reasonable time for large payloads', () => {
1813
- const toolCalls = [];
1814
- const toolCallIds = [];
1815
- for (let i = 0; i < 50; i++) {
1816
- const id = `tool_${i}`;
1817
- toolCallIds.push(id);
1818
- toolCalls.push({
1819
- type: ContentTypes.TOOL_CALL,
1820
- tool_call: {
1821
- id,
1822
- name: `tool_${i}`,
1823
- args: JSON.stringify({ data: 'x'.repeat(100) }),
1824
- output: 'y'.repeat(Math.floor(Math.random() * 10000)),
1825
- },
1826
- });
1827
- }
1828
- const payload = [
1829
- {
1830
- role: 'assistant',
1831
- content: [
1832
- {
1833
- type: ContentTypes.TEXT,
1834
- [ContentTypes.TEXT]: 'Processing...',
1835
- tool_call_ids: toolCallIds,
1836
- },
1837
- ...toolCalls,
1838
- ],
1839
- },
1840
- ];
1841
- const indexTokenCountMap = { 0: 50000 };
1842
- const start = performance.now();
1843
- const result = formatAgentMessages(payload, indexTokenCountMap);
1844
- const elapsed = performance.now() - start;
1845
- // PERF: Allow headroom for TOON compression applied to 50 tool outputs
1846
- expect(elapsed).toBeLessThan(2000);
1847
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1848
- expect(total).toBe(50000);
1849
- });
1850
- it('should always preserve total token count across multiple original messages', () => {
1851
- const payload = [
1852
- { role: 'user', content: 'Hello' },
1853
- {
1854
- role: 'assistant',
1855
- content: [
1856
- {
1857
- type: ContentTypes.TEXT,
1858
- [ContentTypes.TEXT]: 'Let me search.',
1859
- tool_call_ids: ['t1'],
1860
- },
1861
- {
1862
- type: ContentTypes.TOOL_CALL,
1863
- tool_call: {
1864
- id: 't1',
1865
- name: 'search',
1866
- args: '{"q":"test"}',
1867
- output: 'Found 10 results with detailed descriptions: ' +
1868
- 'z'.repeat(500),
1869
- },
1870
- },
1871
- ],
1872
- },
1873
- { role: 'user', content: 'Thanks' },
1874
- { role: 'assistant', content: 'You are welcome!' },
1875
- ];
1876
- const indexTokenCountMap = { 0: 5, 1: 200, 2: 3, 3: 8 };
1877
- const result = formatAgentMessages(payload, indexTokenCountMap);
1878
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1879
- expect(total).toBe(216);
1880
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1881
- expect(val).toBeGreaterThanOrEqual(0);
1882
- expect(Number.isInteger(val)).toBe(true);
1883
- }
1884
- });
1885
- it('should produce integer token counts (no floating point)', () => {
1886
- const payload = [
1887
- {
1888
- role: 'assistant',
1889
- content: [
1890
- {
1891
- type: ContentTypes.TEXT,
1892
- [ContentTypes.TEXT]: 'abc',
1893
- tool_call_ids: ['t1', 't2', 't3'],
1894
- },
1895
- {
1896
- type: ContentTypes.TOOL_CALL,
1897
- tool_call: { id: 't1', name: 'a', args: '{}', output: 'defgh' },
1898
- },
1899
- {
1900
- type: ContentTypes.TOOL_CALL,
1901
- tool_call: { id: 't2', name: 'b', args: '{}', output: 'ij' },
1902
- },
1903
- {
1904
- type: ContentTypes.TOOL_CALL,
1905
- tool_call: {
1906
- id: 't3',
1907
- name: 'c',
1908
- args: '{}',
1909
- output: 'klmnopqrst',
1910
- },
1911
- },
1912
- ],
1913
- },
1914
- ];
1915
- const indexTokenCountMap = { 0: 97 };
1916
- const result = formatAgentMessages(payload, indexTokenCountMap);
1917
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1918
- expect(Number.isInteger(val)).toBe(true);
1919
- }
1920
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1921
- expect(total).toBe(97);
1922
- });
1923
- it('should account for tool call args in content length calculation', () => {
1924
- const payload = [
1925
- {
1926
- role: 'assistant',
1927
- content: [
1928
- {
1929
- type: ContentTypes.TEXT,
1930
- [ContentTypes.TEXT]: 'x',
1931
- tool_call_ids: ['t1', 't2'],
1932
- },
1933
- {
1934
- type: ContentTypes.TOOL_CALL,
1935
- tool_call: {
1936
- id: 't1',
1937
- name: 'tiny_tool',
1938
- args: '{}',
1939
- output: 'small',
1940
- },
1941
- },
1942
- {
1943
- type: ContentTypes.TOOL_CALL,
1944
- tool_call: {
1945
- id: 't2',
1946
- name: 'big_args_tool',
1947
- args: JSON.stringify({ data: 'a'.repeat(5000) }),
1948
- output: 'small',
1949
- },
1950
- },
1951
- ],
1952
- },
1953
- ];
1954
- const indexTokenCountMap = { 0: 1000 };
1955
- const result = formatAgentMessages(payload, indexTokenCountMap);
1956
- expect(result.messages).toHaveLength(3);
1957
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1958
- expect(total).toBe(1000);
1959
- for (const val of Object.values(result.indexTokenCountMap || {})) {
1960
- expect(val).toBeGreaterThanOrEqual(0);
1961
- }
1962
- });
1963
- it('should not throw when indexTokenCountMap has undefined values for some indices', () => {
1964
- const payload = [
1965
- { role: 'user', content: 'Hello' },
1966
- {
1967
- role: 'assistant',
1968
- content: [
1969
- {
1970
- type: ContentTypes.TEXT,
1971
- [ContentTypes.TEXT]: 'response',
1972
- tool_call_ids: ['t1'],
1973
- },
1974
- {
1975
- type: ContentTypes.TOOL_CALL,
1976
- tool_call: {
1977
- id: 't1',
1978
- name: 'search',
1979
- args: '{}',
1980
- output: 'result',
1981
- },
1982
- },
1983
- ],
1984
- },
1985
- ];
1986
- const indexTokenCountMap = {
1987
- 0: undefined,
1988
- 1: 50,
1989
- };
1990
- expect(() => {
1991
- const result = formatAgentMessages(payload, indexTokenCountMap);
1992
- expect(result.indexTokenCountMap).toBeDefined();
1993
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
1994
- expect(total).toBe(50);
1995
- }).not.toThrow();
1996
- });
1997
- it('should not throw when indexTokenCountMap is sparse (missing indices)', () => {
1998
- const payload = [
1999
- { role: 'user', content: 'Hello' },
2000
- { role: 'assistant', content: 'World' },
2001
- { role: 'user', content: 'Bye' },
2002
- ];
2003
- const indexTokenCountMap = { 0: 5, 2: 3 };
2004
- expect(() => {
2005
- const result = formatAgentMessages(payload, indexTokenCountMap);
2006
- expect(result.indexTokenCountMap).toBeDefined();
2007
- expect(result.indexTokenCountMap?.[0]).toBe(5);
2008
- expect(result.indexTokenCountMap?.[2]).toBe(3);
2009
- }).not.toThrow();
2010
- });
2011
- it('should not throw when indexTokenCountMap has extra indices beyond payload', () => {
2012
- const payload = [{ role: 'user', content: 'Hello' }];
2013
- const indexTokenCountMap = { 0: 5, 1: 10, 2: 15, 99: 999 };
2014
- expect(() => {
2015
- const result = formatAgentMessages(payload, indexTokenCountMap);
2016
- expect(result.indexTokenCountMap?.[0]).toBe(5);
2017
- }).not.toThrow();
2018
- });
2019
- it('should not throw with empty payload and non-empty indexTokenCountMap', () => {
2020
- const payload = [];
2021
- const indexTokenCountMap = { 0: 100 };
2022
- expect(() => {
2023
- const result = formatAgentMessages(payload, indexTokenCountMap);
2024
- expect(result.messages).toHaveLength(0);
2025
- }).not.toThrow();
2026
- });
2027
- it('should not throw when assistant message content is empty array', () => {
2028
- const payload = [
2029
- {
2030
- role: 'assistant',
2031
- content: [],
2032
- },
2033
- ];
2034
- const indexTokenCountMap = { 0: 50 };
2035
- expect(() => {
2036
- formatAgentMessages(payload, indexTokenCountMap);
2037
- }).not.toThrow();
2038
- });
2039
- it('should not throw when tool call output is null or undefined', () => {
2040
- const payload = [
2041
- {
2042
- role: 'assistant',
2043
- content: [
2044
- {
2045
- type: ContentTypes.TEXT,
2046
- [ContentTypes.TEXT]: 'calling tools',
2047
- tool_call_ids: ['t1', 't2'],
2048
- },
2049
- {
2050
- type: ContentTypes.TOOL_CALL,
2051
- tool_call: {
2052
- id: 't1',
2053
- name: 'search',
2054
- args: '{}',
2055
- output: null,
2056
- },
2057
- },
2058
- {
2059
- type: ContentTypes.TOOL_CALL,
2060
- tool_call: {
2061
- id: 't2',
2062
- name: 'fetch',
2063
- args: '{}',
2064
- output: undefined,
2065
- },
2066
- },
2067
- ],
2068
- },
2069
- ];
2070
- const indexTokenCountMap = { 0: 30 };
2071
- expect(() => {
2072
- const result = formatAgentMessages(payload, indexTokenCountMap);
2073
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
2074
- expect(total).toBe(30);
2075
- }).not.toThrow();
2076
- });
2077
- it('should not throw when tool call args are deeply nested objects', () => {
2078
- const deepArgs = { a: { b: { c: { d: { e: { f: 'deep' } } } } } };
2079
- const payload = [
2080
- {
2081
- role: 'assistant',
2082
- content: [
2083
- {
2084
- type: ContentTypes.TEXT,
2085
- [ContentTypes.TEXT]: 'deep call',
2086
- tool_call_ids: ['t1'],
2087
- },
2088
- {
2089
- type: ContentTypes.TOOL_CALL,
2090
- tool_call: {
2091
- id: 't1',
2092
- name: 'deep_tool',
2093
- args: JSON.stringify(deepArgs),
2094
- output: 'done',
2095
- },
2096
- },
2097
- ],
2098
- },
2099
- ];
2100
- const indexTokenCountMap = { 0: 100 };
2101
- expect(() => {
2102
- const result = formatAgentMessages(payload, indexTokenCountMap);
2103
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
2104
- expect(total).toBe(100);
2105
- }).not.toThrow();
2106
- });
2107
- it('should not throw when tool call args are not valid JSON strings', () => {
2108
- const payload = [
2109
- {
2110
- role: 'assistant',
2111
- content: [
2112
- {
2113
- type: ContentTypes.TEXT,
2114
- [ContentTypes.TEXT]: 'bad args',
2115
- tool_call_ids: ['t1'],
2116
- },
2117
- {
2118
- type: ContentTypes.TOOL_CALL,
2119
- tool_call: {
2120
- id: 't1',
2121
- name: 'tool',
2122
- args: '{not valid json!!!',
2123
- output: 'output',
2124
- },
2125
- },
2126
- ],
2127
- },
2128
- ];
2129
- const indexTokenCountMap = { 0: 40 };
2130
- expect(() => {
2131
- const result = formatAgentMessages(payload, indexTokenCountMap);
2132
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
2133
- expect(total).toBe(40);
2134
- }).not.toThrow();
2135
- });
2136
- it('should not throw when content array has mixed types including unexpected values', () => {
2137
- const payload = [
2138
- {
2139
- role: 'assistant',
2140
- content: [
2141
- { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'hello' },
2142
- null,
2143
- undefined,
2144
- { type: 'unknown_type', something: 'weird' },
2145
- ],
2146
- },
2147
- ];
2148
- const indexTokenCountMap = { 0: 25 };
2149
- expect(() => {
2150
- const result = formatAgentMessages(payload, indexTokenCountMap);
2151
- expect(result.indexTokenCountMap?.[0]).toBe(25);
2152
- }).not.toThrow();
2153
- });
2154
- it('should not throw when tool call has empty name and empty args', () => {
2155
- const payload = [
2156
- {
2157
- role: 'assistant',
2158
- content: [
2159
- {
2160
- type: ContentTypes.TEXT,
2161
- [ContentTypes.TEXT]: 'test',
2162
- tool_call_ids: ['t1'],
2163
- },
2164
- {
2165
- type: ContentTypes.TOOL_CALL,
2166
- tool_call: {
2167
- id: 't1',
2168
- name: '',
2169
- args: '',
2170
- output: 'some output',
2171
- },
2172
- },
2173
- ],
2174
- },
2175
- ];
2176
- const indexTokenCountMap = { 0: 50 };
2177
- expect(() => {
2178
- formatAgentMessages(payload, indexTokenCountMap);
2179
- }).not.toThrow();
2180
- });
2181
- it('should not throw when all content parts are filtered out (THINK + ERROR only)', () => {
2182
- const payload = [
2183
- {
2184
- role: 'assistant',
2185
- content: [
2186
- { type: ContentTypes.THINK, [ContentTypes.THINK]: 'thinking...' },
2187
- { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'error...' },
2188
- ],
2189
- },
2190
- ];
2191
- const indexTokenCountMap = { 0: 100 };
2192
- expect(() => {
2193
- const result = formatAgentMessages(payload, indexTokenCountMap);
2194
- expect(Object.keys(result.indexTokenCountMap || {}).length).toBe(0);
2195
- }).not.toThrow();
2196
- });
2197
- it('should not throw with very large token count values', () => {
2198
- const payload = [
2199
- {
2200
- role: 'assistant',
2201
- content: [
2202
- {
2203
- type: ContentTypes.TEXT,
2204
- [ContentTypes.TEXT]: 'big tokens',
2205
- tool_call_ids: ['t1'],
2206
- },
2207
- {
2208
- type: ContentTypes.TOOL_CALL,
2209
- tool_call: { id: 't1', name: 'a', args: '{}', output: 'b' },
2210
- },
2211
- ],
2212
- },
2213
- ];
2214
- const indexTokenCountMap = { 0: Number.MAX_SAFE_INTEGER };
2215
- expect(() => {
2216
- const result = formatAgentMessages(payload, indexTokenCountMap);
2217
- const total = Object.values(result.indexTokenCountMap || {}).reduce((sum, v) => sum + v, 0);
2218
- expect(total).toBe(Number.MAX_SAFE_INTEGER);
2219
- }).not.toThrow();
2220
- });
2221
- it('should not throw when multiple payload messages expand and some have undefined token counts', () => {
2222
- const payload = [
2223
- { role: 'user', content: 'msg1' },
2224
- {
2225
- role: 'assistant',
2226
- content: [
2227
- {
2228
- type: ContentTypes.TEXT,
2229
- [ContentTypes.TEXT]: 'response with tool',
2230
- tool_call_ids: ['t1'],
2231
- },
2232
- {
2233
- type: ContentTypes.TOOL_CALL,
2234
- tool_call: {
2235
- id: 't1',
2236
- name: 'search',
2237
- args: '{}',
2238
- output: 'found',
2239
- },
2240
- },
2241
- ],
2242
- },
2243
- { role: 'user', content: 'msg2' },
2244
- {
2245
- role: 'assistant',
2246
- content: [
2247
- {
2248
- type: ContentTypes.TEXT,
2249
- [ContentTypes.TEXT]: 'another response',
2250
- tool_call_ids: ['t2'],
2251
- },
2252
- {
2253
- type: ContentTypes.TOOL_CALL,
2254
- tool_call: {
2255
- id: 't2',
2256
- name: 'fetch',
2257
- args: '{}',
2258
- output: 'data',
2259
- },
2260
- },
2261
- ],
2262
- },
2263
- ];
2264
- const indexTokenCountMap = {
2265
- 0: 5,
2266
- 1: undefined,
2267
- 2: 3,
2268
- 3: 80,
2269
- };
2270
- expect(() => {
2271
- const result = formatAgentMessages(payload, indexTokenCountMap);
2272
- expect(result.indexTokenCountMap).toBeDefined();
2273
- expect(result.indexTokenCountMap?.[0]).toBe(5);
2274
- }).not.toThrow();
2275
- });
2276
- });
2277
- });
2278
- //# sourceMappingURL=formatAgentMessages.test.js.map