@illuma-ai/agents 1.1.21 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/dist/cjs/graphs/Graph.cjs +12 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +85 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/run.cjs +20 -9
  6. package/dist/cjs/run.cjs.map +1 -1
  7. package/dist/esm/graphs/Graph.mjs +12 -1
  8. package/dist/esm/graphs/Graph.mjs.map +1 -1
  9. package/dist/esm/graphs/MultiAgentGraph.mjs +85 -1
  10. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  11. package/dist/esm/run.mjs +20 -9
  12. package/dist/esm/run.mjs.map +1 -1
  13. package/dist/types/graphs/MultiAgentGraph.d.ts +17 -0
  14. package/package.json +1 -1
  15. package/src/graphs/Graph.ts +12 -1
  16. package/src/graphs/MultiAgentGraph.ts +105 -1
  17. package/src/graphs/__tests__/multi-agent-delegate.test.ts +191 -0
  18. package/src/run.ts +20 -11
  19. package/src/scripts/test-bedrock-handoff-autonomous.ts +231 -0
  20. package/src/agents/AgentContext.js +0 -782
  21. package/src/agents/AgentContext.test.js +0 -421
  22. package/src/agents/__tests__/AgentContext.test.js +0 -678
  23. package/src/agents/__tests__/resolveStructuredOutputMode.test.js +0 -117
  24. package/src/common/enum.js +0 -192
  25. package/src/common/index.js +0 -3
  26. package/src/events.js +0 -166
  27. package/src/graphs/Graph.js +0 -1857
  28. package/src/graphs/MultiAgentGraph.js +0 -1092
  29. package/src/graphs/__tests__/structured-output.integration.test.js +0 -624
  30. package/src/graphs/__tests__/structured-output.test.js +0 -144
  31. package/src/graphs/contextManagement.e2e.test.js +0 -718
  32. package/src/graphs/contextManagement.test.js +0 -485
  33. package/src/graphs/handoffValidation.test.js +0 -276
  34. package/src/graphs/index.js +0 -3
  35. package/src/index.js +0 -28
  36. package/src/instrumentation.js +0 -21
  37. package/src/llm/anthropic/index.js +0 -319
  38. package/src/llm/anthropic/types.js +0 -46
  39. package/src/llm/anthropic/utils/message_inputs.js +0 -627
  40. package/src/llm/anthropic/utils/message_outputs.js +0 -290
  41. package/src/llm/anthropic/utils/output_parsers.js +0 -89
  42. package/src/llm/anthropic/utils/tools.js +0 -25
  43. package/src/llm/bedrock/__tests__/bedrock-caching.test.js +0 -392
  44. package/src/llm/bedrock/index.js +0 -303
  45. package/src/llm/bedrock/types.js +0 -2
  46. package/src/llm/bedrock/utils/index.js +0 -6
  47. package/src/llm/bedrock/utils/message_inputs.js +0 -463
  48. package/src/llm/bedrock/utils/message_outputs.js +0 -269
  49. package/src/llm/fake.js +0 -92
  50. package/src/llm/google/index.js +0 -215
  51. package/src/llm/google/types.js +0 -12
  52. package/src/llm/google/utils/common.js +0 -670
  53. package/src/llm/google/utils/tools.js +0 -111
  54. package/src/llm/google/utils/zod_to_genai_parameters.js +0 -47
  55. package/src/llm/openai/index.js +0 -1033
  56. package/src/llm/openai/types.js +0 -2
  57. package/src/llm/openai/utils/index.js +0 -756
  58. package/src/llm/openai/utils/isReasoningModel.test.js +0 -79
  59. package/src/llm/openrouter/index.js +0 -261
  60. package/src/llm/openrouter/reasoning.test.js +0 -181
  61. package/src/llm/providers.js +0 -36
  62. package/src/llm/text.js +0 -65
  63. package/src/llm/vertexai/index.js +0 -402
  64. package/src/messages/__tests__/tools.test.js +0 -392
  65. package/src/messages/cache.js +0 -404
  66. package/src/messages/cache.test.js +0 -1167
  67. package/src/messages/content.js +0 -48
  68. package/src/messages/content.test.js +0 -314
  69. package/src/messages/core.js +0 -359
  70. package/src/messages/ensureThinkingBlock.test.js +0 -997
  71. package/src/messages/format.js +0 -973
  72. package/src/messages/formatAgentMessages.test.js +0 -2278
  73. package/src/messages/formatAgentMessages.tools.test.js +0 -362
  74. package/src/messages/formatMessage.test.js +0 -608
  75. package/src/messages/ids.js +0 -18
  76. package/src/messages/index.js +0 -9
  77. package/src/messages/labelContentByAgent.test.js +0 -725
  78. package/src/messages/prune.js +0 -438
  79. package/src/messages/reducer.js +0 -60
  80. package/src/messages/shiftIndexTokenCountMap.test.js +0 -63
  81. package/src/messages/summarize.js +0 -146
  82. package/src/messages/summarize.test.js +0 -332
  83. package/src/messages/tools.js +0 -90
  84. package/src/mockStream.js +0 -81
  85. package/src/prompts/collab.js +0 -7
  86. package/src/prompts/index.js +0 -3
  87. package/src/prompts/taskmanager.js +0 -58
  88. package/src/run.js +0 -427
  89. package/src/schemas/index.js +0 -3
  90. package/src/schemas/schema-preparation.test.js +0 -370
  91. package/src/schemas/validate.js +0 -314
  92. package/src/schemas/validate.test.js +0 -264
  93. package/src/scripts/abort.js +0 -127
  94. package/src/scripts/ant_web_search.js +0 -130
  95. package/src/scripts/ant_web_search_edge_case.js +0 -133
  96. package/src/scripts/ant_web_search_error_edge_case.js +0 -119
  97. package/src/scripts/args.js +0 -41
  98. package/src/scripts/bedrock-cache-debug.js +0 -186
  99. package/src/scripts/bedrock-content-aggregation-test.js +0 -195
  100. package/src/scripts/bedrock-merge-test.js +0 -80
  101. package/src/scripts/bedrock-parallel-tools-test.js +0 -150
  102. package/src/scripts/caching.js +0 -106
  103. package/src/scripts/cli.js +0 -152
  104. package/src/scripts/cli2.js +0 -119
  105. package/src/scripts/cli3.js +0 -163
  106. package/src/scripts/cli4.js +0 -165
  107. package/src/scripts/cli5.js +0 -165
  108. package/src/scripts/code_exec.js +0 -171
  109. package/src/scripts/code_exec_files.js +0 -180
  110. package/src/scripts/code_exec_multi_session.js +0 -185
  111. package/src/scripts/code_exec_ptc.js +0 -265
  112. package/src/scripts/code_exec_session.js +0 -217
  113. package/src/scripts/code_exec_simple.js +0 -120
  114. package/src/scripts/content.js +0 -111
  115. package/src/scripts/empty_input.js +0 -125
  116. package/src/scripts/handoff-test.js +0 -96
  117. package/src/scripts/image.js +0 -138
  118. package/src/scripts/memory.js +0 -83
  119. package/src/scripts/multi-agent-chain.js +0 -271
  120. package/src/scripts/multi-agent-conditional.js +0 -185
  121. package/src/scripts/multi-agent-document-review-chain.js +0 -171
  122. package/src/scripts/multi-agent-hybrid-flow.js +0 -264
  123. package/src/scripts/multi-agent-parallel-start.js +0 -214
  124. package/src/scripts/multi-agent-parallel.js +0 -346
  125. package/src/scripts/multi-agent-sequence.js +0 -184
  126. package/src/scripts/multi-agent-supervisor.js +0 -324
  127. package/src/scripts/multi-agent-test.js +0 -147
  128. package/src/scripts/parallel-asymmetric-tools-test.js +0 -202
  129. package/src/scripts/parallel-full-metadata-test.js +0 -176
  130. package/src/scripts/parallel-tools-test.js +0 -256
  131. package/src/scripts/programmatic_exec.js +0 -277
  132. package/src/scripts/programmatic_exec_agent.js +0 -168
  133. package/src/scripts/search.js +0 -118
  134. package/src/scripts/sequential-full-metadata-test.js +0 -143
  135. package/src/scripts/simple.js +0 -174
  136. package/src/scripts/single-agent-metadata-test.js +0 -152
  137. package/src/scripts/stream.js +0 -113
  138. package/src/scripts/test-custom-prompt-key.js +0 -132
  139. package/src/scripts/test-handoff-input.js +0 -143
  140. package/src/scripts/test-handoff-preamble.js +0 -227
  141. package/src/scripts/test-handoff-steering.js +0 -353
  142. package/src/scripts/test-multi-agent-list-handoff.js +0 -318
  143. package/src/scripts/test-parallel-agent-labeling.js +0 -253
  144. package/src/scripts/test-parallel-handoffs.js +0 -229
  145. package/src/scripts/test-thinking-handoff-bedrock.js +0 -132
  146. package/src/scripts/test-thinking-handoff.js +0 -132
  147. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +0 -140
  148. package/src/scripts/test-tool-before-handoff-role-order.js +0 -223
  149. package/src/scripts/test-tools-before-handoff.js +0 -187
  150. package/src/scripts/test_code_api.js +0 -263
  151. package/src/scripts/thinking-bedrock.js +0 -128
  152. package/src/scripts/thinking-vertexai.js +0 -130
  153. package/src/scripts/thinking.js +0 -134
  154. package/src/scripts/tool_search.js +0 -114
  155. package/src/scripts/tools.js +0 -125
  156. package/src/specs/agent-handoffs-bedrock.integration.test.js +0 -280
  157. package/src/specs/agent-handoffs.test.js +0 -924
  158. package/src/specs/anthropic.simple.test.js +0 -287
  159. package/src/specs/azure.simple.test.js +0 -381
  160. package/src/specs/cache.simple.test.js +0 -282
  161. package/src/specs/custom-event-await.test.js +0 -148
  162. package/src/specs/deepseek.simple.test.js +0 -189
  163. package/src/specs/emergency-prune.test.js +0 -308
  164. package/src/specs/moonshot.simple.test.js +0 -237
  165. package/src/specs/observability.integration.test.js +0 -1337
  166. package/src/specs/openai.simple.test.js +0 -233
  167. package/src/specs/openrouter.simple.test.js +0 -202
  168. package/src/specs/prune.test.js +0 -733
  169. package/src/specs/reasoning.test.js +0 -144
  170. package/src/specs/spec.utils.js +0 -4
  171. package/src/specs/thinking-handoff.test.js +0 -486
  172. package/src/specs/thinking-prune.test.js +0 -600
  173. package/src/specs/token-distribution-edge-case.test.js +0 -246
  174. package/src/specs/token-memoization.test.js +0 -32
  175. package/src/specs/tokens.test.js +0 -49
  176. package/src/specs/tool-error.test.js +0 -139
  177. package/src/splitStream.js +0 -204
  178. package/src/splitStream.test.js +0 -504
  179. package/src/stream.js +0 -650
  180. package/src/stream.test.js +0 -225
  181. package/src/test/mockTools.js +0 -340
  182. package/src/tools/BrowserTools.js +0 -245
  183. package/src/tools/Calculator.js +0 -38
  184. package/src/tools/Calculator.test.js +0 -225
  185. package/src/tools/CodeExecutor.js +0 -233
  186. package/src/tools/ProgrammaticToolCalling.js +0 -602
  187. package/src/tools/StreamingToolCallBuffer.js +0 -179
  188. package/src/tools/ToolNode.js +0 -930
  189. package/src/tools/ToolSearch.js +0 -904
  190. package/src/tools/__tests__/BrowserTools.test.js +0 -306
  191. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +0 -276
  192. package/src/tools/__tests__/ProgrammaticToolCalling.test.js +0 -807
  193. package/src/tools/__tests__/StreamingToolCallBuffer.test.js +0 -175
  194. package/src/tools/__tests__/ToolApproval.test.js +0 -675
  195. package/src/tools/__tests__/ToolNode.recovery.test.js +0 -200
  196. package/src/tools/__tests__/ToolNode.session.test.js +0 -319
  197. package/src/tools/__tests__/ToolSearch.integration.test.js +0 -125
  198. package/src/tools/__tests__/ToolSearch.test.js +0 -812
  199. package/src/tools/__tests__/handlers.test.js +0 -799
  200. package/src/tools/__tests__/truncation-recovery.integration.test.js +0 -362
  201. package/src/tools/handlers.js +0 -306
  202. package/src/tools/schema.js +0 -25
  203. package/src/tools/search/anthropic.js +0 -34
  204. package/src/tools/search/content.js +0 -116
  205. package/src/tools/search/content.test.js +0 -133
  206. package/src/tools/search/firecrawl.js +0 -173
  207. package/src/tools/search/format.js +0 -198
  208. package/src/tools/search/highlights.js +0 -241
  209. package/src/tools/search/index.js +0 -3
  210. package/src/tools/search/jina-reranker.test.js +0 -106
  211. package/src/tools/search/rerankers.js +0 -165
  212. package/src/tools/search/schema.js +0 -102
  213. package/src/tools/search/search.js +0 -561
  214. package/src/tools/search/serper-scraper.js +0 -126
  215. package/src/tools/search/test.js +0 -129
  216. package/src/tools/search/tool.js +0 -453
  217. package/src/tools/search/types.js +0 -2
  218. package/src/tools/search/utils.js +0 -59
  219. package/src/types/graph.js +0 -24
  220. package/src/types/graph.test.js +0 -192
  221. package/src/types/index.js +0 -7
  222. package/src/types/llm.js +0 -2
  223. package/src/types/messages.js +0 -2
  224. package/src/types/run.js +0 -2
  225. package/src/types/stream.js +0 -2
  226. package/src/types/tools.js +0 -2
  227. package/src/utils/contextAnalytics.js +0 -79
  228. package/src/utils/contextAnalytics.test.js +0 -166
  229. package/src/utils/events.js +0 -26
  230. package/src/utils/graph.js +0 -11
  231. package/src/utils/handlers.js +0 -65
  232. package/src/utils/index.js +0 -10
  233. package/src/utils/llm.js +0 -21
  234. package/src/utils/llmConfig.js +0 -205
  235. package/src/utils/logging.js +0 -37
  236. package/src/utils/misc.js +0 -51
  237. package/src/utils/run.js +0 -69
  238. package/src/utils/schema.js +0 -21
  239. package/src/utils/title.js +0 -119
  240. package/src/utils/tokens.js +0 -92
  241. package/src/utils/toonFormat.js +0 -379
@@ -1,807 +0,0 @@
1
- // src/tools/__tests__/ProgrammaticToolCalling.test.ts
2
- /**
3
- * Unit tests for Programmatic Tool Calling.
4
- * Tests manual invocation with mock tools and Code API responses.
5
- */
6
- import { describe, it, expect, beforeEach } from '@jest/globals';
7
- import { createProgrammaticToolCallingTool, formatCompletedResponse, extractUsedToolNames, filterToolsByUsage, executeTools, normalizeToPythonIdentifier, unwrapToolResponse, } from '../ProgrammaticToolCalling';
8
- import { createProgrammaticToolRegistry, createGetTeamMembersTool, createGetExpensesTool, createGetWeatherTool, createCalculatorTool, } from '@/test/mockTools';
9
- describe('ProgrammaticToolCalling', () => {
10
- describe('executeTools', () => {
11
- let toolMap;
12
- beforeEach(() => {
13
- const tools = [
14
- createGetTeamMembersTool(),
15
- createGetExpensesTool(),
16
- createGetWeatherTool(),
17
- createCalculatorTool(),
18
- ];
19
- toolMap = new Map(tools.map((t) => [t.name, t]));
20
- });
21
- it('executes a single tool successfully', async () => {
22
- const toolCalls = [
23
- {
24
- id: 'call_001',
25
- name: 'get_weather',
26
- input: { city: 'San Francisco' },
27
- },
28
- ];
29
- const results = await executeTools(toolCalls, toolMap);
30
- expect(results).toHaveLength(1);
31
- expect(results[0].call_id).toBe('call_001');
32
- expect(results[0].is_error).toBe(false);
33
- expect(results[0].result).toEqual({
34
- temperature: 65,
35
- condition: 'Foggy',
36
- });
37
- });
38
- it('executes multiple tools in parallel', async () => {
39
- const toolCalls = [
40
- {
41
- id: 'call_001',
42
- name: 'get_weather',
43
- input: { city: 'San Francisco' },
44
- },
45
- {
46
- id: 'call_002',
47
- name: 'get_weather',
48
- input: { city: 'New York' },
49
- },
50
- {
51
- id: 'call_003',
52
- name: 'get_weather',
53
- input: { city: 'London' },
54
- },
55
- ];
56
- const startTime = Date.now();
57
- const results = await executeTools(toolCalls, toolMap);
58
- const duration = Date.now() - startTime;
59
- // Should execute in parallel (< 150ms total, not 120ms sequential)
60
- expect(duration).toBeLessThan(150);
61
- expect(results).toHaveLength(3);
62
- expect(results[0].is_error).toBe(false);
63
- expect(results[1].is_error).toBe(false);
64
- expect(results[2].is_error).toBe(false);
65
- expect(results[0].result.temperature).toBe(65);
66
- expect(results[1].result.temperature).toBe(75);
67
- expect(results[2].result.temperature).toBe(55);
68
- });
69
- it('handles tool not found error', async () => {
70
- const toolCalls = [
71
- {
72
- id: 'call_001',
73
- name: 'nonexistent_tool',
74
- input: {},
75
- },
76
- ];
77
- const results = await executeTools(toolCalls, toolMap);
78
- expect(results).toHaveLength(1);
79
- expect(results[0].call_id).toBe('call_001');
80
- expect(results[0].is_error).toBe(true);
81
- expect(results[0].error_message).toContain('nonexistent_tool');
82
- expect(results[0].error_message).toContain('Available tools:');
83
- });
84
- it('handles tool execution error', async () => {
85
- const toolCalls = [
86
- {
87
- id: 'call_001',
88
- name: 'get_weather',
89
- input: { city: 'InvalidCity' },
90
- },
91
- ];
92
- const results = await executeTools(toolCalls, toolMap);
93
- expect(results).toHaveLength(1);
94
- expect(results[0].call_id).toBe('call_001');
95
- expect(results[0].is_error).toBe(true);
96
- expect(results[0].error_message).toContain('Weather data not available');
97
- });
98
- it('handles mix of successful and failed tool calls', async () => {
99
- const toolCalls = [
100
- {
101
- id: 'call_001',
102
- name: 'get_weather',
103
- input: { city: 'San Francisco' },
104
- },
105
- {
106
- id: 'call_002',
107
- name: 'get_weather',
108
- input: { city: 'InvalidCity' },
109
- },
110
- {
111
- id: 'call_003',
112
- name: 'get_weather',
113
- input: { city: 'New York' },
114
- },
115
- ];
116
- const results = await executeTools(toolCalls, toolMap);
117
- expect(results).toHaveLength(3);
118
- expect(results[0].is_error).toBe(false);
119
- expect(results[1].is_error).toBe(true);
120
- expect(results[2].is_error).toBe(false);
121
- });
122
- it('executes tools with different parameters', async () => {
123
- const toolCalls = [
124
- {
125
- id: 'call_001',
126
- name: 'get_team_members',
127
- input: {},
128
- },
129
- {
130
- id: 'call_002',
131
- name: 'get_expenses',
132
- input: { user_id: 'u1' },
133
- },
134
- {
135
- id: 'call_003',
136
- name: 'calculator',
137
- input: { expression: '2 + 2 * 3' },
138
- },
139
- ];
140
- const results = await executeTools(toolCalls, toolMap);
141
- expect(results).toHaveLength(3);
142
- expect(results[0].is_error).toBe(false);
143
- expect(results[1].is_error).toBe(false);
144
- expect(results[2].is_error).toBe(false);
145
- expect(Array.isArray(results[0].result)).toBe(true);
146
- expect(results[0].result).toHaveLength(3);
147
- expect(Array.isArray(results[1].result)).toBe(true);
148
- expect(results[2].result.result).toBe(8);
149
- });
150
- });
151
- describe('normalizeToPythonIdentifier', () => {
152
- it('converts hyphens to underscores', () => {
153
- expect(normalizeToPythonIdentifier('my-tool-name')).toBe('my_tool_name');
154
- });
155
- it('converts spaces to underscores', () => {
156
- expect(normalizeToPythonIdentifier('my tool name')).toBe('my_tool_name');
157
- });
158
- it('leaves underscores unchanged', () => {
159
- expect(normalizeToPythonIdentifier('my_tool_name')).toBe('my_tool_name');
160
- });
161
- it('handles mixed hyphens and underscores', () => {
162
- expect(normalizeToPythonIdentifier('my-tool_name-v2')).toBe('my_tool_name_v2');
163
- });
164
- it('handles MCP-style names with hyphens', () => {
165
- expect(normalizeToPythonIdentifier('create_spreadsheet_mcp_Google-Workspace')).toBe('create_spreadsheet_mcp_Google_Workspace');
166
- });
167
- it('removes invalid characters', () => {
168
- expect(normalizeToPythonIdentifier('tool@name!v2')).toBe('toolnamev2');
169
- expect(normalizeToPythonIdentifier('get.data.v2')).toBe('getdatav2');
170
- });
171
- it('prefixes with underscore if starts with number', () => {
172
- expect(normalizeToPythonIdentifier('123tool')).toBe('_123tool');
173
- expect(normalizeToPythonIdentifier('1-tool')).toBe('_1_tool');
174
- });
175
- it('appends _tool suffix for Python keywords', () => {
176
- expect(normalizeToPythonIdentifier('return')).toBe('return_tool');
177
- expect(normalizeToPythonIdentifier('async')).toBe('async_tool');
178
- expect(normalizeToPythonIdentifier('import')).toBe('import_tool');
179
- });
180
- });
181
- describe('unwrapToolResponse', () => {
182
- describe('non-MCP tools', () => {
183
- it('returns result as-is for non-MCP tools', () => {
184
- const result = { temperature: 65, condition: 'Foggy' };
185
- expect(unwrapToolResponse(result, false)).toEqual(result);
186
- });
187
- it('returns string as-is for non-MCP tools', () => {
188
- expect(unwrapToolResponse('plain string', false)).toBe('plain string');
189
- });
190
- it('returns array as-is for non-MCP tools', () => {
191
- const result = [1, 2, 3];
192
- expect(unwrapToolResponse(result, false)).toEqual(result);
193
- });
194
- });
195
- describe('MCP tools - tuple format [content, artifacts]', () => {
196
- it('extracts string content from tuple', () => {
197
- const result = ['Hello world', { artifacts: [] }];
198
- expect(unwrapToolResponse(result, true)).toBe('Hello world');
199
- });
200
- it('parses JSON string content from tuple', () => {
201
- const result = ['{"temperature": 65}', { artifacts: [] }];
202
- expect(unwrapToolResponse(result, true)).toEqual({ temperature: 65 });
203
- });
204
- it('parses JSON array string content from tuple', () => {
205
- const result = ['[1, 2, 3]', { artifacts: [] }];
206
- expect(unwrapToolResponse(result, true)).toEqual([1, 2, 3]);
207
- });
208
- it('extracts text from single content block in tuple', () => {
209
- const result = [{ type: 'text', text: 'Spreadsheet info here' }, {}];
210
- expect(unwrapToolResponse(result, true)).toBe('Spreadsheet info here');
211
- });
212
- it('extracts and parses JSON from single content block in tuple', () => {
213
- const result = [
214
- { type: 'text', text: '{"id": "123", "name": "Test"}' },
215
- {},
216
- ];
217
- expect(unwrapToolResponse(result, true)).toEqual({
218
- id: '123',
219
- name: 'Test',
220
- });
221
- });
222
- it('extracts text from array of content blocks in tuple', () => {
223
- const result = [
224
- [
225
- { type: 'text', text: 'Line 1' },
226
- { type: 'text', text: 'Line 2' },
227
- ],
228
- {},
229
- ];
230
- expect(unwrapToolResponse(result, true)).toBe('Line 1\nLine 2');
231
- });
232
- it('returns object content as-is when not a text block', () => {
233
- const result = [{ temperature: 65, condition: 'Foggy' }, {}];
234
- expect(unwrapToolResponse(result, true)).toEqual({
235
- temperature: 65,
236
- condition: 'Foggy',
237
- });
238
- });
239
- });
240
- describe('MCP tools - single content block (not in tuple)', () => {
241
- it('extracts text from single content block object', () => {
242
- const result = { type: 'text', text: 'No data found in range' };
243
- expect(unwrapToolResponse(result, true)).toBe('No data found in range');
244
- });
245
- it('extracts and parses JSON from single content block object', () => {
246
- const result = {
247
- type: 'text',
248
- text: '{"sheets": [{"name": "raw_data"}]}',
249
- };
250
- expect(unwrapToolResponse(result, true)).toEqual({
251
- sheets: [{ name: 'raw_data' }],
252
- });
253
- });
254
- it('handles real-world MCP spreadsheet response', () => {
255
- const result = {
256
- type: 'text',
257
- text: 'Spreadsheet: "NYC Taxi - Top Pickup Neighborhoods" (ID: abc123)\nSheets (2):\n - "raw_data" (ID: 123) | Size: 1000x26',
258
- };
259
- expect(unwrapToolResponse(result, true)).toBe('Spreadsheet: "NYC Taxi - Top Pickup Neighborhoods" (ID: abc123)\nSheets (2):\n - "raw_data" (ID: 123) | Size: 1000x26');
260
- });
261
- it('handles real-world MCP no data response', () => {
262
- const result = {
263
- type: 'text',
264
- text: 'No data found in range \'raw_data!A1:D25\' for user@example.com.',
265
- };
266
- expect(unwrapToolResponse(result, true)).toBe('No data found in range \'raw_data!A1:D25\' for user@example.com.');
267
- });
268
- });
269
- describe('MCP tools - array of content blocks (not in tuple)', () => {
270
- it('extracts text from array of content blocks', () => {
271
- const result = [
272
- { type: 'text', text: 'First block' },
273
- { type: 'text', text: 'Second block' },
274
- ];
275
- expect(unwrapToolResponse(result, true)).toBe('First block\nSecond block');
276
- });
277
- it('filters out non-text blocks', () => {
278
- const result = [
279
- { type: 'text', text: 'Text content' },
280
- { type: 'image', data: 'base64...' },
281
- { type: 'text', text: 'More text' },
282
- ];
283
- expect(unwrapToolResponse(result, true)).toBe('Text content\nMore text');
284
- });
285
- });
286
- describe('edge cases', () => {
287
- it('returns non-text block object as-is', () => {
288
- const result = { type: 'image', data: 'base64...' };
289
- expect(unwrapToolResponse(result, true)).toEqual(result);
290
- });
291
- it('handles empty array', () => {
292
- expect(unwrapToolResponse([], true)).toEqual([]);
293
- });
294
- it('handles malformed JSON in text block gracefully', () => {
295
- const result = { type: 'text', text: '{ invalid json }' };
296
- expect(unwrapToolResponse(result, true)).toBe('{ invalid json }');
297
- });
298
- it('handles null', () => {
299
- expect(unwrapToolResponse(null, true)).toBe(null);
300
- });
301
- it('handles undefined', () => {
302
- expect(unwrapToolResponse(undefined, true)).toBe(undefined);
303
- });
304
- });
305
- });
306
- describe('extractUsedToolNames', () => {
307
- const createToolMap = (names) => {
308
- const map = new Map();
309
- for (const name of names) {
310
- map.set(normalizeToPythonIdentifier(name), name);
311
- }
312
- return map;
313
- };
314
- const availableTools = createToolMap([
315
- 'get_weather',
316
- 'get_team_members',
317
- 'get_expenses',
318
- 'calculator',
319
- 'search_docs',
320
- ]);
321
- it('extracts single tool name from simple code', () => {
322
- const code = `result = await get_weather(city="SF")
323
- print(result)`;
324
- const used = extractUsedToolNames(code, availableTools);
325
- expect(used.size).toBe(1);
326
- expect(used.has('get_weather')).toBe(true);
327
- });
328
- it('extracts multiple tool names from code', () => {
329
- const code = `team = await get_team_members()
330
- for member in team:
331
- expenses = await get_expenses(user_id=member['id'])
332
- print(f"{member['name']}: {sum(e['amount'] for e in expenses)}")`;
333
- const used = extractUsedToolNames(code, availableTools);
334
- expect(used.size).toBe(2);
335
- expect(used.has('get_team_members')).toBe(true);
336
- expect(used.has('get_expenses')).toBe(true);
337
- });
338
- it('extracts tools from asyncio.gather calls', () => {
339
- const code = `results = await asyncio.gather(
340
- get_weather(city="SF"),
341
- get_weather(city="NYC"),
342
- get_expenses(user_id="u1")
343
- )`;
344
- const used = extractUsedToolNames(code, availableTools);
345
- expect(used.size).toBe(2);
346
- expect(used.has('get_weather')).toBe(true);
347
- expect(used.has('get_expenses')).toBe(true);
348
- });
349
- it('does not match partial tool names', () => {
350
- const code = `# Using get_weather_data instead
351
- result = await get_weather_data(city="SF")`;
352
- const used = extractUsedToolNames(code, availableTools);
353
- expect(used.has('get_weather')).toBe(false);
354
- });
355
- it('matches tool names in different contexts', () => {
356
- const code = `# direct call
357
- x = await calculator(expression="1+1")
358
- # in list comprehension
359
- results = [await get_weather(city=c) for c in cities]
360
- # conditional
361
- if condition:
362
- await get_team_members()`;
363
- const used = extractUsedToolNames(code, availableTools);
364
- expect(used.size).toBe(3);
365
- expect(used.has('calculator')).toBe(true);
366
- expect(used.has('get_weather')).toBe(true);
367
- expect(used.has('get_team_members')).toBe(true);
368
- });
369
- it('returns empty set when no tools are used', () => {
370
- const code = `print("Hello, World!")
371
- x = 1 + 2`;
372
- const used = extractUsedToolNames(code, availableTools);
373
- expect(used.size).toBe(0);
374
- });
375
- it('handles tool names with special characters via normalization', () => {
376
- const specialTools = createToolMap(['get_data.v2', 'calc+plus']);
377
- const code = `await get_datav2()
378
- await calcplus()`;
379
- const used = extractUsedToolNames(code, specialTools);
380
- expect(used.has('get_data.v2')).toBe(true);
381
- expect(used.has('calc+plus')).toBe(true);
382
- });
383
- it('matches hyphenated tool names using underscore in code', () => {
384
- const mcpTools = createToolMap([
385
- 'create_spreadsheet_mcp_Google-Workspace',
386
- 'search_gmail_mcp_Google-Workspace',
387
- ]);
388
- const code = `result = await create_spreadsheet_mcp_Google_Workspace(title="Test")
389
- print(result)`;
390
- const used = extractUsedToolNames(code, mcpTools);
391
- expect(used.size).toBe(1);
392
- expect(used.has('create_spreadsheet_mcp_Google-Workspace')).toBe(true);
393
- });
394
- });
395
- describe('filterToolsByUsage', () => {
396
- const allToolDefs = [
397
- {
398
- name: 'get_weather',
399
- description: 'Get weather for a city',
400
- parameters: {
401
- type: 'object',
402
- properties: { city: { type: 'string' } },
403
- },
404
- },
405
- {
406
- name: 'get_team_members',
407
- description: 'Get team members',
408
- parameters: { type: 'object', properties: {} },
409
- },
410
- {
411
- name: 'get_expenses',
412
- description: 'Get expenses for a user',
413
- parameters: {
414
- type: 'object',
415
- properties: { user_id: { type: 'string' } },
416
- },
417
- },
418
- {
419
- name: 'calculator',
420
- description: 'Evaluate an expression',
421
- parameters: {
422
- type: 'object',
423
- properties: { expression: { type: 'string' } },
424
- },
425
- },
426
- ];
427
- it('filters to only used tools', () => {
428
- const code = `result = await get_weather(city="SF")
429
- print(result)`;
430
- const filtered = filterToolsByUsage(allToolDefs, code);
431
- expect(filtered).toHaveLength(1);
432
- expect(filtered[0].name).toBe('get_weather');
433
- });
434
- it('filters to multiple used tools', () => {
435
- const code = `team = await get_team_members()
436
- for member in team:
437
- expenses = await get_expenses(user_id=member['id'])`;
438
- const filtered = filterToolsByUsage(allToolDefs, code);
439
- expect(filtered).toHaveLength(2);
440
- expect(filtered.map((t) => t.name).sort()).toEqual([
441
- 'get_expenses',
442
- 'get_team_members',
443
- ]);
444
- });
445
- it('returns all tools when no tools are detected', () => {
446
- const code = 'print("Hello, World!")';
447
- const filtered = filterToolsByUsage(allToolDefs, code);
448
- expect(filtered).toHaveLength(4);
449
- });
450
- it('preserves tool definition structure', () => {
451
- const code = 'await calculator(expression="2+2")';
452
- const filtered = filterToolsByUsage(allToolDefs, code);
453
- expect(filtered).toHaveLength(1);
454
- expect(filtered[0]).toEqual(allToolDefs[3]);
455
- expect(filtered[0].parameters).toBeDefined();
456
- expect(filtered[0].description).toBe('Evaluate an expression');
457
- });
458
- it('handles empty tool definitions', () => {
459
- const code = 'await get_weather(city="SF")';
460
- const filtered = filterToolsByUsage([], code);
461
- expect(filtered).toHaveLength(0);
462
- });
463
- });
464
- describe('formatCompletedResponse', () => {
465
- it('formats response with stdout', () => {
466
- const response = {
467
- status: 'completed',
468
- stdout: 'Hello, World!\n',
469
- stderr: '',
470
- files: [],
471
- session_id: 'sess_abc123',
472
- };
473
- const [output, artifact] = formatCompletedResponse(response);
474
- expect(output).toContain('stdout:\nHello, World!');
475
- expect(artifact.session_id).toBe('sess_abc123');
476
- expect(artifact.files).toEqual([]);
477
- });
478
- it('shows empty output message when no stdout', () => {
479
- const response = {
480
- status: 'completed',
481
- stdout: '',
482
- stderr: '',
483
- files: [],
484
- session_id: 'sess_abc123',
485
- };
486
- const [output] = formatCompletedResponse(response);
487
- expect(output).toContain('stdout: Empty. Ensure you\'re writing output explicitly');
488
- });
489
- it('includes stderr when present', () => {
490
- const response = {
491
- status: 'completed',
492
- stdout: 'Output\n',
493
- stderr: 'Warning: deprecated function\n',
494
- files: [],
495
- session_id: 'sess_abc123',
496
- };
497
- const [output] = formatCompletedResponse(response);
498
- expect(output).toContain('stdout:\nOutput');
499
- expect(output).toContain('stderr:\nWarning: deprecated function');
500
- });
501
- it('formats file information correctly', () => {
502
- const response = {
503
- status: 'completed',
504
- stdout: 'Generated report\n',
505
- stderr: '',
506
- files: [
507
- { id: '1', name: 'report.pdf' },
508
- { id: '2', name: 'data.csv' },
509
- ],
510
- session_id: 'sess_abc123',
511
- };
512
- const [output, artifact] = formatCompletedResponse(response);
513
- expect(output).toContain('Generated files:');
514
- expect(output).toContain('report.pdf');
515
- expect(output).toContain('data.csv');
516
- expect(artifact.files).toHaveLength(2);
517
- expect(artifact.files).toEqual(response.files);
518
- });
519
- it('handles image files with special message', () => {
520
- const response = {
521
- status: 'completed',
522
- stdout: '',
523
- stderr: '',
524
- files: [
525
- { id: '1', name: 'chart.png' },
526
- { id: '2', name: 'photo.jpg' },
527
- ],
528
- session_id: 'sess_abc123',
529
- };
530
- const [output] = formatCompletedResponse(response);
531
- expect(output).toContain('chart.png');
532
- expect(output).toContain('Image is already displayed to the user');
533
- });
534
- });
535
- describe('createProgrammaticToolCallingTool - Manual Invocation', () => {
536
- let ptcTool;
537
- let toolMap;
538
- let toolDefinitions;
539
- beforeEach(() => {
540
- const tools = [
541
- createGetTeamMembersTool(),
542
- createGetExpensesTool(),
543
- createGetWeatherTool(),
544
- ];
545
- toolMap = new Map(tools.map((t) => [t.name, t]));
546
- toolDefinitions = Array.from(createProgrammaticToolRegistry().values()).filter((t) => ['get_team_members', 'get_expenses', 'get_weather'].includes(t.name));
547
- ptcTool = createProgrammaticToolCallingTool({
548
- apiKey: 'test-key',
549
- baseUrl: 'http://mock-api',
550
- });
551
- });
552
- it('throws error when no toolMap provided', async () => {
553
- await expect(ptcTool.invoke({
554
- code: 'result = await get_weather(city="SF")\nprint(result)',
555
- tools: toolDefinitions,
556
- toolMap,
557
- })).rejects.toThrow('No toolMap provided');
558
- });
559
- it('throws error when toolMap is empty', async () => {
560
- const args = {
561
- code: 'result = await get_weather(city="SF")\nprint(result)',
562
- tools: toolDefinitions,
563
- toolMap: new Map(),
564
- };
565
- const toolCall = {
566
- name: 'programmatic_tool_calling',
567
- args,
568
- };
569
- await expect(ptcTool.invoke(args, {
570
- toolCall,
571
- })).rejects.toThrow('No toolMap provided');
572
- });
573
- it('throws error when no tool definitions provided', async () => {
574
- const args = {
575
- code: 'result = await get_weather(city="SF")\nprint(result)',
576
- // No tools
577
- };
578
- const toolCall = {
579
- name: 'programmatic_code_execution',
580
- args,
581
- toolMap,
582
- // No `toolDefs`
583
- };
584
- await expect(ptcTool.invoke(args, { toolCall })).rejects.toThrow('No tool definitions provided');
585
- });
586
- it('uses toolDefs from config when tools not provided', async () => {
587
- // Skip this test - requires mocking fetch which has complex typing
588
- // This functionality is tested in the live script tests instead
589
- });
590
- });
591
- describe('Tool Classification', () => {
592
- it('filters tools by allowed_callers', () => {
593
- const registry = createProgrammaticToolRegistry();
594
- const codeExecutionTools = Array.from(registry.values()).filter((t) => (t.allowed_callers ?? ['direct']).includes('code_execution'));
595
- // get_team_members, get_expenses, calculator: code_execution only
596
- const codeOnlyTools = codeExecutionTools.filter((t) => !(t.allowed_callers?.includes('direct') === true));
597
- expect(codeOnlyTools.length).toBeGreaterThanOrEqual(3);
598
- // get_weather: both direct and code_execution
599
- const bothTools = Array.from(registry.values()).filter((t) => t.allowed_callers?.includes('direct') === true &&
600
- t.allowed_callers.includes('code_execution'));
601
- expect(bothTools.length).toBeGreaterThanOrEqual(1);
602
- expect(bothTools.some((t) => t.name === 'get_weather')).toBe(true);
603
- });
604
- });
605
- describe('Error Handling', () => {
606
- let toolMap;
607
- beforeEach(() => {
608
- const tools = [createGetWeatherTool()];
609
- toolMap = new Map(tools.map((t) => [t.name, t]));
610
- });
611
- it('returns error for invalid city without throwing', async () => {
612
- const toolCalls = [
613
- {
614
- id: 'call_001',
615
- name: 'get_weather',
616
- input: { city: 'InvalidCity' },
617
- },
618
- ];
619
- const results = await executeTools(toolCalls, toolMap);
620
- expect(results).toHaveLength(1);
621
- expect(results[0].is_error).toBe(true);
622
- expect(results[0].result).toBeNull();
623
- expect(results[0].error_message).toContain('Weather data not available');
624
- });
625
- it('continues execution when one tool fails', async () => {
626
- const toolCalls = [
627
- {
628
- id: 'call_001',
629
- name: 'get_weather',
630
- input: { city: 'San Francisco' },
631
- },
632
- {
633
- id: 'call_002',
634
- name: 'get_weather',
635
- input: { city: 'InvalidCity' },
636
- },
637
- {
638
- id: 'call_003',
639
- name: 'get_weather',
640
- input: { city: 'London' },
641
- },
642
- ];
643
- const results = await executeTools(toolCalls, toolMap);
644
- expect(results).toHaveLength(3);
645
- expect(results[0].is_error).toBe(false);
646
- expect(results[1].is_error).toBe(true);
647
- expect(results[2].is_error).toBe(false);
648
- });
649
- });
650
- describe('Parallel Execution Performance', () => {
651
- let toolMap;
652
- beforeEach(() => {
653
- const tools = [createGetExpensesTool()];
654
- toolMap = new Map(tools.map((t) => [t.name, t]));
655
- });
656
- it('executes tools in parallel, not sequentially', async () => {
657
- const toolCalls = [
658
- { id: 'call_001', name: 'get_expenses', input: { user_id: 'u1' } },
659
- { id: 'call_002', name: 'get_expenses', input: { user_id: 'u2' } },
660
- { id: 'call_003', name: 'get_expenses', input: { user_id: 'u3' } },
661
- ];
662
- const startTime = Date.now();
663
- const results = await executeTools(toolCalls, toolMap);
664
- const duration = Date.now() - startTime;
665
- // Each tool has 30ms delay
666
- // Sequential would be ~90ms, parallel should be ~30-50ms
667
- expect(duration).toBeLessThan(80);
668
- expect(results).toHaveLength(3);
669
- expect(results.every((r) => r.is_error === false)).toBe(true);
670
- });
671
- });
672
- describe('Response Formatting', () => {
673
- it('formats stdout-only response', () => {
674
- const response = {
675
- status: 'completed',
676
- stdout: 'Team size: 3\n- Alice\n- Bob\n- Charlie\n',
677
- stderr: '',
678
- files: [],
679
- session_id: 'sess_xyz',
680
- };
681
- const [output, artifact] = formatCompletedResponse(response);
682
- expect(output).toBe('stdout:\nTeam size: 3\n- Alice\n- Bob\n- Charlie');
683
- expect(artifact).toEqual({
684
- session_id: 'sess_xyz',
685
- files: [],
686
- });
687
- });
688
- it('formats response with files', () => {
689
- const response = {
690
- status: 'completed',
691
- stdout: 'Report generated\n',
692
- stderr: '',
693
- files: [
694
- { id: '1', name: 'report.csv' },
695
- { id: '2', name: 'chart.png' },
696
- ],
697
- session_id: 'sess_xyz',
698
- };
699
- const [output, artifact] = formatCompletedResponse(response);
700
- expect(output).toContain('Generated files:');
701
- expect(output).toContain('report.csv');
702
- expect(output).toContain('chart.png');
703
- expect(output).toContain('File is already downloaded');
704
- expect(output).toContain('Image is already displayed');
705
- expect(artifact.files).toHaveLength(2);
706
- });
707
- it('handles multiple files with correct separators', () => {
708
- const response = {
709
- status: 'completed',
710
- stdout: 'Done\n',
711
- stderr: '',
712
- files: [
713
- { id: '1', name: 'file1.txt' },
714
- { id: '2', name: 'file2.txt' },
715
- ],
716
- session_id: 'sess_xyz',
717
- };
718
- const [output] = formatCompletedResponse(response);
719
- // 2 files format: "- /mnt/data/file1.txt | ..., - /mnt/data/file2.txt | ..."
720
- expect(output).toContain('file1.txt');
721
- expect(output).toContain('file2.txt');
722
- expect(output).toContain('- /mnt/data/file1.txt');
723
- expect(output).toContain('- /mnt/data/file2.txt');
724
- });
725
- it('handles many files with newline separators', () => {
726
- const response = {
727
- status: 'completed',
728
- stdout: 'Done\n',
729
- stderr: '',
730
- files: [
731
- { id: '1', name: 'file1.txt' },
732
- { id: '2', name: 'file2.txt' },
733
- { id: '3', name: 'file3.txt' },
734
- { id: '4', name: 'file4.txt' },
735
- ],
736
- session_id: 'sess_xyz',
737
- };
738
- const [output] = formatCompletedResponse(response);
739
- // More than 3 files should use newline separators
740
- expect(output).toContain('file1.txt');
741
- expect(output).toContain('file4.txt');
742
- expect(output.match(/,\n/g)?.length).toBeGreaterThanOrEqual(2);
743
- });
744
- });
745
- describe('Tool Data Extraction', () => {
746
- let toolMap;
747
- beforeEach(() => {
748
- const tools = [
749
- createGetTeamMembersTool(),
750
- createGetExpensesTool(),
751
- createCalculatorTool(),
752
- ];
753
- toolMap = new Map(tools.map((t) => [t.name, t]));
754
- });
755
- it('extracts correct data from team members tool', async () => {
756
- const toolCalls = [
757
- { id: 'call_001', name: 'get_team_members', input: {} },
758
- ];
759
- const results = await executeTools(toolCalls, toolMap);
760
- expect(results[0].result).toEqual([
761
- { id: 'u1', name: 'Alice', department: 'Engineering' },
762
- { id: 'u2', name: 'Bob', department: 'Marketing' },
763
- { id: 'u3', name: 'Charlie', department: 'Engineering' },
764
- ]);
765
- });
766
- it('extracts correct data from expenses tool', async () => {
767
- const toolCalls = [
768
- { id: 'call_001', name: 'get_expenses', input: { user_id: 'u1' } },
769
- ];
770
- const results = await executeTools(toolCalls, toolMap);
771
- expect(results[0].result).toEqual([
772
- { amount: 150.0, category: 'travel' },
773
- { amount: 75.5, category: 'meals' },
774
- ]);
775
- });
776
- it('handles empty expense data', async () => {
777
- const toolCalls = [
778
- {
779
- id: 'call_001',
780
- name: 'get_expenses',
781
- input: { user_id: 'nonexistent' },
782
- },
783
- ];
784
- const results = await executeTools(toolCalls, toolMap);
785
- expect(results[0].is_error).toBe(false);
786
- expect(results[0].result).toEqual([]);
787
- });
788
- it('calculates correct result', async () => {
789
- const toolCalls = [
790
- {
791
- id: 'call_001',
792
- name: 'calculator',
793
- input: { expression: '2 + 2 * 3' },
794
- },
795
- {
796
- id: 'call_002',
797
- name: 'calculator',
798
- input: { expression: '(10 + 5) / 3' },
799
- },
800
- ];
801
- const results = await executeTools(toolCalls, toolMap);
802
- expect(results[0].result.result).toBe(8);
803
- expect(results[1].result.result).toBe(5);
804
- });
805
- });
806
- });
807
- //# sourceMappingURL=ProgrammaticToolCalling.test.js.map