illuma-agents 1.0.10 → 1.0.11

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 (141) hide show
  1. package/LICENSE +1 -1
  2. package/dist/cjs/agents/AgentContext.cjs +228 -27
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/common/enum.cjs +2 -0
  5. package/dist/cjs/common/enum.cjs.map +1 -1
  6. package/dist/cjs/events.cjs +3 -11
  7. package/dist/cjs/events.cjs.map +1 -1
  8. package/dist/cjs/graphs/Graph.cjs +27 -18
  9. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  10. package/dist/cjs/instrumentation.cjs +1 -3
  11. package/dist/cjs/instrumentation.cjs.map +1 -1
  12. package/dist/cjs/llm/anthropic/index.cjs +1 -1
  13. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs +122 -7
  15. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  16. package/dist/cjs/llm/google/index.cjs +1 -1
  17. package/dist/cjs/llm/google/index.cjs.map +1 -1
  18. package/dist/cjs/llm/openai/index.cjs +6 -6
  19. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  20. package/dist/cjs/llm/openrouter/index.cjs +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  22. package/dist/cjs/main.cjs +18 -0
  23. package/dist/cjs/main.cjs.map +1 -1
  24. package/dist/cjs/messages/cache.cjs +149 -54
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/tools.cjs +85 -0
  27. package/dist/cjs/messages/tools.cjs.map +1 -0
  28. package/dist/cjs/run.cjs +0 -8
  29. package/dist/cjs/run.cjs.map +1 -1
  30. package/dist/cjs/tools/CodeExecutor.cjs +4 -0
  31. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  32. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
  34. package/dist/cjs/tools/ToolNode.cjs +53 -15
  35. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  36. package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
  37. package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
  38. package/dist/cjs/tools/search/schema.cjs +7 -9
  39. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  40. package/dist/cjs/utils/run.cjs +5 -1
  41. package/dist/cjs/utils/run.cjs.map +1 -1
  42. package/dist/esm/agents/AgentContext.mjs +228 -27
  43. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  44. package/dist/esm/common/enum.mjs +2 -0
  45. package/dist/esm/common/enum.mjs.map +1 -1
  46. package/dist/esm/events.mjs +4 -12
  47. package/dist/esm/events.mjs.map +1 -1
  48. package/dist/esm/graphs/Graph.mjs +27 -18
  49. package/dist/esm/graphs/Graph.mjs.map +1 -1
  50. package/dist/esm/instrumentation.mjs +1 -3
  51. package/dist/esm/instrumentation.mjs.map +1 -1
  52. package/dist/esm/llm/anthropic/index.mjs +1 -1
  53. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  54. package/dist/esm/llm/bedrock/index.mjs +122 -7
  55. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  56. package/dist/esm/llm/google/index.mjs +1 -1
  57. package/dist/esm/llm/google/index.mjs.map +1 -1
  58. package/dist/esm/llm/openai/index.mjs +6 -6
  59. package/dist/esm/llm/openai/index.mjs.map +1 -1
  60. package/dist/esm/llm/openrouter/index.mjs +1 -1
  61. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  62. package/dist/esm/main.mjs +3 -0
  63. package/dist/esm/main.mjs.map +1 -1
  64. package/dist/esm/messages/cache.mjs +149 -54
  65. package/dist/esm/messages/cache.mjs.map +1 -1
  66. package/dist/esm/messages/tools.mjs +82 -0
  67. package/dist/esm/messages/tools.mjs.map +1 -0
  68. package/dist/esm/run.mjs +0 -8
  69. package/dist/esm/run.mjs.map +1 -1
  70. package/dist/esm/tools/CodeExecutor.mjs +4 -0
  71. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
  73. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
  74. package/dist/esm/tools/ToolNode.mjs +53 -15
  75. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  76. package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
  77. package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
  78. package/dist/esm/tools/search/schema.mjs +7 -9
  79. package/dist/esm/tools/search/schema.mjs.map +1 -1
  80. package/dist/esm/utils/run.mjs +5 -1
  81. package/dist/esm/utils/run.mjs.map +1 -1
  82. package/dist/types/agents/AgentContext.d.ts +65 -5
  83. package/dist/types/common/enum.d.ts +2 -0
  84. package/dist/types/graphs/Graph.d.ts +3 -2
  85. package/dist/types/index.d.ts +2 -0
  86. package/dist/types/llm/anthropic/index.d.ts +1 -1
  87. package/dist/types/llm/bedrock/index.d.ts +31 -4
  88. package/dist/types/llm/google/index.d.ts +1 -1
  89. package/dist/types/llm/openai/index.d.ts +3 -3
  90. package/dist/types/llm/openrouter/index.d.ts +1 -1
  91. package/dist/types/messages/cache.d.ts +23 -8
  92. package/dist/types/messages/index.d.ts +1 -0
  93. package/dist/types/messages/tools.d.ts +17 -0
  94. package/dist/types/test/mockTools.d.ts +28 -0
  95. package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
  96. package/dist/types/tools/ToolNode.d.ts +10 -2
  97. package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
  98. package/dist/types/types/graph.d.ts +7 -1
  99. package/dist/types/types/tools.d.ts +138 -0
  100. package/package.json +7 -2
  101. package/src/agents/AgentContext.ts +267 -27
  102. package/src/agents/__tests__/AgentContext.test.ts +805 -0
  103. package/src/common/enum.ts +2 -0
  104. package/src/events.ts +5 -12
  105. package/src/graphs/Graph.ts +33 -19
  106. package/src/index.ts +2 -0
  107. package/src/instrumentation.ts +1 -4
  108. package/src/llm/anthropic/index.ts +2 -2
  109. package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
  110. package/src/llm/bedrock/index.ts +150 -13
  111. package/src/llm/google/index.ts +2 -2
  112. package/src/llm/openai/index.ts +9 -9
  113. package/src/llm/openrouter/index.ts +2 -2
  114. package/src/messages/__tests__/tools.test.ts +473 -0
  115. package/src/messages/cache.ts +163 -61
  116. package/src/messages/index.ts +1 -0
  117. package/src/messages/tools.ts +99 -0
  118. package/src/run.ts +0 -9
  119. package/src/scripts/code_exec_ptc.ts +334 -0
  120. package/src/scripts/image.ts +178 -0
  121. package/src/scripts/programmatic_exec.ts +396 -0
  122. package/src/scripts/programmatic_exec_agent.ts +231 -0
  123. package/src/scripts/test-tools-before-handoff.ts +5 -1
  124. package/src/scripts/tool_search_regex.ts +162 -0
  125. package/src/scripts/tools.ts +4 -1
  126. package/src/specs/thinking-prune.test.ts +52 -118
  127. package/src/test/mockTools.ts +366 -0
  128. package/src/tools/CodeExecutor.ts +4 -0
  129. package/src/tools/ProgrammaticToolCalling.ts +558 -0
  130. package/src/tools/ToolNode.ts +59 -18
  131. package/src/tools/ToolSearchRegex.ts +535 -0
  132. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
  133. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
  134. package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
  135. package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
  136. package/src/tools/search/jina-reranker.test.ts +16 -16
  137. package/src/tools/search/schema.ts +7 -9
  138. package/src/types/graph.ts +7 -1
  139. package/src/types/tools.ts +166 -0
  140. package/src/utils/run.ts +5 -1
  141. package/src/tools/search/direct-url.test.ts +0 -530
@@ -0,0 +1,558 @@
1
+ // src/tools/ProgrammaticToolCalling.ts
2
+ import { z } from 'zod';
3
+ import { config } from 'dotenv';
4
+ import fetch, { RequestInit } from 'node-fetch';
5
+ import { HttpsProxyAgent } from 'https-proxy-agent';
6
+ import { getEnvironmentVariable } from '@langchain/core/utils/env';
7
+ import { tool, DynamicStructuredTool } from '@langchain/core/tools';
8
+ import type { ToolCall } from '@langchain/core/messages/tool';
9
+ import type * as t from '@/types';
10
+ import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
11
+ import { EnvVar, Constants } from '@/common';
12
+
13
+ config();
14
+
15
+ // ============================================================================
16
+ // Constants
17
+ // ============================================================================
18
+
19
+ const imageMessage = 'Image is already displayed to the user';
20
+ const otherMessage = 'File is already downloaded by the user';
21
+ const accessMessage =
22
+ 'Note: Files are READ-ONLY. Save changes to NEW filenames. To access these files in future executions, provide the `session_id` as a parameter (not in your code).';
23
+ const emptyOutputMessage =
24
+ 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
25
+
26
+ /** Default max round-trips to prevent infinite loops */
27
+ const DEFAULT_MAX_ROUND_TRIPS = 20;
28
+
29
+ /** Default execution timeout in milliseconds */
30
+ const DEFAULT_TIMEOUT = 60000;
31
+
32
+ // ============================================================================
33
+ // Schema
34
+ // ============================================================================
35
+
36
+ const ProgrammaticToolCallingSchema = z.object({
37
+ code: z
38
+ .string()
39
+ .min(1)
40
+ .describe(
41
+ `Python code that calls tools programmatically. Tools are automatically available as async Python functions - DO NOT define them yourself.
42
+
43
+ The Code API generates async function stubs from the tool definitions. Just call them directly:
44
+
45
+ Example (Simple call):
46
+ result = await get_weather(city="San Francisco")
47
+ print(result)
48
+
49
+ Example (Parallel - Fastest):
50
+ results = await asyncio.gather(
51
+ get_weather(city="SF"),
52
+ get_weather(city="NYC"),
53
+ get_weather(city="London")
54
+ )
55
+ for city, weather in zip(["SF", "NYC", "London"], results):
56
+ print(f"{city}: {weather['temperature']}°F")
57
+
58
+ Example (Loop with processing):
59
+ team = await get_team_members()
60
+ for member in team:
61
+ expenses = await get_expenses(user_id=member['id'])
62
+ total = sum(e['amount'] for e in expenses)
63
+ print(f"{member['name']}: \${total:.2f}")
64
+
65
+ Example (Conditional logic):
66
+ data = await fetch_data(source="primary")
67
+ if not data:
68
+ data = await fetch_data(source="backup")
69
+ print(f"Got {len(data)} records")
70
+
71
+ Requirements:
72
+ - Tools are pre-defined as async functions - DO NOT write function definitions
73
+ - Use await for all tool calls
74
+ - Use asyncio.gather() for parallel execution of independent calls
75
+ - Only print() output flows back to the context window
76
+ - Tool results from programmatic calls do NOT consume context tokens`
77
+ ),
78
+ session_id: z
79
+ .string()
80
+ .optional()
81
+ .describe(
82
+ 'Session ID for file access (same as regular code execution). Files load into /mnt/data/ and are READ-ONLY.'
83
+ ),
84
+ timeout: z
85
+ .number()
86
+ .int()
87
+ .min(1000)
88
+ .max(300000)
89
+ .optional()
90
+ .default(DEFAULT_TIMEOUT)
91
+ .describe(
92
+ 'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.'
93
+ ),
94
+ });
95
+
96
+ // ============================================================================
97
+ // Helper Functions
98
+ // ============================================================================
99
+
100
+ /** Python reserved keywords that get `_tool` suffix in Code API */
101
+ const PYTHON_KEYWORDS = new Set([
102
+ 'False',
103
+ 'None',
104
+ 'True',
105
+ 'and',
106
+ 'as',
107
+ 'assert',
108
+ 'async',
109
+ 'await',
110
+ 'break',
111
+ 'class',
112
+ 'continue',
113
+ 'def',
114
+ 'del',
115
+ 'elif',
116
+ 'else',
117
+ 'except',
118
+ 'finally',
119
+ 'for',
120
+ 'from',
121
+ 'global',
122
+ 'if',
123
+ 'import',
124
+ 'in',
125
+ 'is',
126
+ 'lambda',
127
+ 'nonlocal',
128
+ 'not',
129
+ 'or',
130
+ 'pass',
131
+ 'raise',
132
+ 'return',
133
+ 'try',
134
+ 'while',
135
+ 'with',
136
+ 'yield',
137
+ ]);
138
+
139
+ /**
140
+ * Normalizes a tool name to Python identifier format.
141
+ * Must match the Code API's `normalizePythonFunctionName` exactly:
142
+ * 1. Replace hyphens and spaces with underscores
143
+ * 2. Remove any other invalid characters
144
+ * 3. Prefix with underscore if starts with number
145
+ * 4. Append `_tool` if it's a Python keyword
146
+ * @param name - The tool name to normalize
147
+ * @returns Normalized Python-safe identifier
148
+ */
149
+ export function normalizeToPythonIdentifier(name: string): string {
150
+ let normalized = name.replace(/[-\s]/g, '_');
151
+
152
+ normalized = normalized.replace(/[^a-zA-Z0-9_]/g, '');
153
+
154
+ if (/^[0-9]/.test(normalized)) {
155
+ normalized = '_' + normalized;
156
+ }
157
+
158
+ if (PYTHON_KEYWORDS.has(normalized)) {
159
+ normalized = normalized + '_tool';
160
+ }
161
+
162
+ return normalized;
163
+ }
164
+
165
+ /**
166
+ * Extracts tool names that are actually called in the Python code.
167
+ * Handles hyphen/underscore conversion since Python identifiers use underscores.
168
+ * @param code - The Python code to analyze
169
+ * @param toolNameMap - Map from normalized Python name to original tool name
170
+ * @returns Set of original tool names found in the code
171
+ */
172
+ export function extractUsedToolNames(
173
+ code: string,
174
+ toolNameMap: Map<string, string>
175
+ ): Set<string> {
176
+ const usedTools = new Set<string>();
177
+
178
+ for (const [pythonName, originalName] of toolNameMap) {
179
+ const escapedName = pythonName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
180
+ const pattern = new RegExp(`\\b${escapedName}\\s*\\(`, 'g');
181
+
182
+ if (pattern.test(code)) {
183
+ usedTools.add(originalName);
184
+ }
185
+ }
186
+
187
+ return usedTools;
188
+ }
189
+
190
+ /**
191
+ * Filters tool definitions to only include tools actually used in the code.
192
+ * Handles the hyphen-to-underscore conversion for Python compatibility.
193
+ * @param toolDefs - All available tool definitions
194
+ * @param code - The Python code to analyze
195
+ * @param debug - Enable debug logging
196
+ * @returns Filtered array of tool definitions
197
+ */
198
+ export function filterToolsByUsage(
199
+ toolDefs: t.LCTool[],
200
+ code: string,
201
+ debug = false
202
+ ): t.LCTool[] {
203
+ const toolNameMap = new Map<string, string>();
204
+ for (const tool of toolDefs) {
205
+ const pythonName = normalizeToPythonIdentifier(tool.name);
206
+ toolNameMap.set(pythonName, tool.name);
207
+ }
208
+
209
+ const usedToolNames = extractUsedToolNames(code, toolNameMap);
210
+
211
+ if (debug) {
212
+ // eslint-disable-next-line no-console
213
+ console.log(
214
+ `[PTC Debug] Tool filtering: found ${usedToolNames.size}/${toolDefs.length} tools in code`
215
+ );
216
+ if (usedToolNames.size > 0) {
217
+ // eslint-disable-next-line no-console
218
+ console.log(
219
+ `[PTC Debug] Matched tools: ${Array.from(usedToolNames).join(', ')}`
220
+ );
221
+ }
222
+ }
223
+
224
+ if (usedToolNames.size === 0) {
225
+ if (debug) {
226
+ // eslint-disable-next-line no-console
227
+ console.log(
228
+ '[PTC Debug] No tools detected in code - sending all tools as fallback'
229
+ );
230
+ }
231
+ return toolDefs;
232
+ }
233
+
234
+ return toolDefs.filter((tool) => usedToolNames.has(tool.name));
235
+ }
236
+
237
+ /**
238
+ * Makes an HTTP request to the Code API.
239
+ * @param endpoint - The API endpoint URL
240
+ * @param apiKey - The API key for authentication
241
+ * @param body - The request body
242
+ * @param proxy - Optional HTTP proxy URL
243
+ * @returns The parsed API response
244
+ */
245
+ export async function makeRequest(
246
+ endpoint: string,
247
+ apiKey: string,
248
+ body: Record<string, unknown>,
249
+ proxy?: string
250
+ ): Promise<t.ProgrammaticExecutionResponse> {
251
+ const fetchOptions: RequestInit = {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ 'User-Agent': 'Illuma/1.0',
256
+ 'X-API-Key': apiKey,
257
+ },
258
+ body: JSON.stringify(body),
259
+ };
260
+
261
+ if (proxy != null && proxy !== '') {
262
+ fetchOptions.agent = new HttpsProxyAgent(proxy);
263
+ }
264
+
265
+ const response = await fetch(endpoint, fetchOptions);
266
+
267
+ if (!response.ok) {
268
+ const errorText = await response.text();
269
+ throw new Error(
270
+ `HTTP error! status: ${response.status}, body: ${errorText}`
271
+ );
272
+ }
273
+
274
+ return (await response.json()) as t.ProgrammaticExecutionResponse;
275
+ }
276
+
277
+ /**
278
+ * Executes tools in parallel when requested by the API.
279
+ * Uses Promise.all for parallel execution, catching individual errors.
280
+ * @param toolCalls - Array of tool calls from the API
281
+ * @param toolMap - Map of tool names to executable tools
282
+ * @returns Array of tool results
283
+ */
284
+ export async function executeTools(
285
+ toolCalls: t.PTCToolCall[],
286
+ toolMap: t.ToolMap
287
+ ): Promise<t.PTCToolResult[]> {
288
+ const executions = toolCalls.map(async (call): Promise<t.PTCToolResult> => {
289
+ const tool = toolMap.get(call.name);
290
+
291
+ if (!tool) {
292
+ return {
293
+ call_id: call.id,
294
+ result: null,
295
+ is_error: true,
296
+ error_message: `Tool '${call.name}' not found. Available tools: ${Array.from(toolMap.keys()).join(', ')}`,
297
+ };
298
+ }
299
+
300
+ try {
301
+ const result = await tool.invoke(call.input, {
302
+ metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
303
+ });
304
+ return {
305
+ call_id: call.id,
306
+ result,
307
+ is_error: false,
308
+ };
309
+ } catch (error) {
310
+ return {
311
+ call_id: call.id,
312
+ result: null,
313
+ is_error: true,
314
+ error_message: (error as Error).message || 'Tool execution failed',
315
+ };
316
+ }
317
+ });
318
+
319
+ return await Promise.all(executions);
320
+ }
321
+
322
+ /**
323
+ * Formats the completed response for the agent.
324
+ * @param response - The completed API response
325
+ * @returns Tuple of [formatted string, artifact]
326
+ */
327
+ export function formatCompletedResponse(
328
+ response: t.ProgrammaticExecutionResponse
329
+ ): [string, t.ProgrammaticExecutionArtifact] {
330
+ let formatted = '';
331
+
332
+ if (response.stdout != null && response.stdout !== '') {
333
+ formatted += `stdout:\n${response.stdout}\n`;
334
+ } else {
335
+ formatted += emptyOutputMessage;
336
+ }
337
+
338
+ if (response.stderr != null && response.stderr !== '') {
339
+ formatted += `stderr:\n${response.stderr}\n`;
340
+ }
341
+
342
+ if (response.files && response.files.length > 0) {
343
+ formatted += 'Generated files:\n';
344
+
345
+ const fileCount = response.files.length;
346
+ for (let i = 0; i < fileCount; i++) {
347
+ const file = response.files[i];
348
+ const isImage = imageExtRegex.test(file.name);
349
+ formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
350
+
351
+ if (i < fileCount - 1) {
352
+ formatted += fileCount <= 3 ? ', ' : ',\n';
353
+ }
354
+ }
355
+
356
+ formatted += `\nsession_id: ${response.session_id}\n\n${accessMessage}`;
357
+ }
358
+
359
+ return [
360
+ formatted.trim(),
361
+ {
362
+ session_id: response.session_id,
363
+ files: response.files,
364
+ },
365
+ ];
366
+ }
367
+
368
+ // ============================================================================
369
+ // Tool Factory
370
+ // ============================================================================
371
+
372
+ /**
373
+ * Creates a Programmatic Tool Calling tool for complex multi-tool workflows.
374
+ *
375
+ * This tool enables AI agents to write Python code that orchestrates multiple
376
+ * tool calls programmatically, reducing LLM round-trips and token usage.
377
+ *
378
+ * The tool map must be provided at runtime via config.configurable.toolMap.
379
+ *
380
+ * @param params - Configuration parameters (apiKey, baseUrl, maxRoundTrips, proxy)
381
+ * @returns A LangChain DynamicStructuredTool for programmatic tool calling
382
+ *
383
+ * @example
384
+ * const ptcTool = createProgrammaticToolCallingTool({
385
+ * apiKey: process.env.CODE_API_KEY,
386
+ * maxRoundTrips: 20
387
+ * });
388
+ *
389
+ * const [output, artifact] = await ptcTool.invoke(
390
+ * { code, tools },
391
+ * { configurable: { toolMap } }
392
+ * );
393
+ */
394
+ export function createProgrammaticToolCallingTool(
395
+ initParams: t.ProgrammaticToolCallingParams = {}
396
+ ): DynamicStructuredTool<typeof ProgrammaticToolCallingSchema> {
397
+ const apiKey =
398
+ (initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
399
+ initParams.apiKey ??
400
+ getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
401
+ '';
402
+
403
+ if (!apiKey) {
404
+ throw new Error(
405
+ 'No API key provided for programmatic tool calling. ' +
406
+ 'Set CODE_API_KEY environment variable or pass apiKey in initParams.'
407
+ );
408
+ }
409
+
410
+ const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
411
+ const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
412
+ const proxy = initParams.proxy ?? process.env.PROXY;
413
+ const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
414
+ const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
415
+
416
+ const description = `
417
+ Run tools by writing Python code. Tools are available as async functions - just call them with await.
418
+
419
+ This is different from execute_code: here you can call your tools (like get_weather, get_expenses, etc.) directly in Python code.
420
+
421
+ Usage:
422
+ - Tools are pre-defined as async functions - call them with await
423
+ - Use asyncio.gather() to run multiple tools in parallel
424
+ - Only print() output is returned - tool results stay in Python
425
+
426
+ Examples:
427
+ - Simple: result = await get_weather(city="NYC")
428
+ - Loop: for user in users: data = await get_expenses(user_id=user['id'])
429
+ - Parallel: sf, ny = await asyncio.gather(get_weather(city="SF"), get_weather(city="NY"))
430
+
431
+ When to use this instead of calling tools directly:
432
+ - You need to call tools in a loop (process many items)
433
+ - You want parallel execution (asyncio.gather)
434
+ - You need conditionals based on tool results
435
+ - You want to aggregate/filter data before returning
436
+ `.trim();
437
+
438
+ return tool<typeof ProgrammaticToolCallingSchema>(
439
+ async (params, config) => {
440
+ const { code, session_id, timeout = DEFAULT_TIMEOUT } = params;
441
+
442
+ // Extra params injected by ToolNode (follows web_search pattern)
443
+ const { toolMap, toolDefs } = (config.toolCall ?? {}) as ToolCall &
444
+ Partial<t.ProgrammaticCache>;
445
+
446
+ if (toolMap == null || toolMap.size === 0) {
447
+ throw new Error(
448
+ 'No toolMap provided. ' +
449
+ 'ToolNode should inject this from AgentContext when invoked through the graph.'
450
+ );
451
+ }
452
+
453
+ if (toolDefs == null || toolDefs.length === 0) {
454
+ throw new Error(
455
+ 'No tool definitions provided. ' +
456
+ 'Either pass tools in the input or ensure ToolNode injects toolDefs.'
457
+ );
458
+ }
459
+
460
+ let roundTrip = 0;
461
+
462
+ try {
463
+ // ====================================================================
464
+ // Phase 1: Filter tools and make initial request
465
+ // ====================================================================
466
+
467
+ const effectiveTools = filterToolsByUsage(toolDefs, code, debug);
468
+
469
+ if (debug) {
470
+ // eslint-disable-next-line no-console
471
+ console.log(
472
+ `[PTC Debug] Sending ${effectiveTools.length} tools to API ` +
473
+ `(filtered from ${toolDefs.length})`
474
+ );
475
+ }
476
+
477
+ let response = await makeRequest(
478
+ EXEC_ENDPOINT,
479
+ apiKey,
480
+ {
481
+ code,
482
+ tools: effectiveTools,
483
+ session_id,
484
+ timeout,
485
+ },
486
+ proxy
487
+ );
488
+
489
+ // ====================================================================
490
+ // Phase 2: Handle response loop
491
+ // ====================================================================
492
+
493
+ while (response.status === 'tool_call_required') {
494
+ roundTrip++;
495
+
496
+ if (roundTrip > maxRoundTrips) {
497
+ throw new Error(
498
+ `Exceeded maximum round trips (${maxRoundTrips}). ` +
499
+ 'This may indicate an infinite loop, excessive tool calls, ' +
500
+ 'or a logic error in your code.'
501
+ );
502
+ }
503
+
504
+ if (debug) {
505
+ // eslint-disable-next-line no-console
506
+ console.log(
507
+ `[PTC Debug] Round trip ${roundTrip}: ${response.tool_calls?.length ?? 0} tool(s) to execute`
508
+ );
509
+ }
510
+
511
+ const toolResults = await executeTools(
512
+ response.tool_calls ?? [],
513
+ toolMap
514
+ );
515
+
516
+ response = await makeRequest(
517
+ EXEC_ENDPOINT,
518
+ apiKey,
519
+ {
520
+ continuation_token: response.continuation_token,
521
+ tool_results: toolResults,
522
+ },
523
+ proxy
524
+ );
525
+ }
526
+
527
+ // ====================================================================
528
+ // Phase 3: Handle final state
529
+ // ====================================================================
530
+
531
+ if (response.status === 'completed') {
532
+ return formatCompletedResponse(response);
533
+ }
534
+
535
+ if (response.status === 'error') {
536
+ throw new Error(
537
+ `Execution error: ${response.error}` +
538
+ (response.stderr != null && response.stderr !== ''
539
+ ? `\n\nStderr:\n${response.stderr}`
540
+ : '')
541
+ );
542
+ }
543
+
544
+ throw new Error(`Unexpected response status: ${response.status}`);
545
+ } catch (error) {
546
+ throw new Error(
547
+ `Programmatic execution failed: ${(error as Error).message}`
548
+ );
549
+ }
550
+ },
551
+ {
552
+ name: Constants.PROGRAMMATIC_TOOL_CALLING,
553
+ description,
554
+ schema: ProgrammaticToolCallingSchema,
555
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
556
+ }
557
+ );
558
+ }
@@ -20,6 +20,7 @@ import type { BaseMessage, AIMessage } from '@langchain/core/messages';
20
20
  import type { StructuredToolInterface } from '@langchain/core/tools';
21
21
  import type * as t from '@/types';
22
22
  import { RunnableCallable } from '@/utils';
23
+ import { Constants } from '@/common';
23
24
 
24
25
  /**
25
26
  * Helper to check if a value is a Send object
@@ -30,7 +31,6 @@ function isSend(value: unknown): value is Send {
30
31
 
31
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
33
  export class ToolNode<T = any> extends RunnableCallable<T, T> {
33
- tools: t.GenericTool[];
34
34
  private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
35
35
  private loadRuntimeTools?: t.ToolRefGenerator;
36
36
  handleToolErrors = true;
@@ -38,6 +38,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
38
38
  toolCallStepIds?: Map<string, string>;
39
39
  errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
40
40
  private toolUsageCount: Map<string, number>;
41
+ /** Tool registry for filtering (lazy computation of programmatic maps) */
42
+ private toolRegistry?: t.LCToolRegistry;
43
+ /** Cached programmatic tools (computed once on first PTC call) */
44
+ private programmaticCache?: t.ProgrammaticCache;
41
45
 
42
46
  constructor({
43
47
  tools,
@@ -48,15 +52,42 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
48
52
  toolCallStepIds,
49
53
  handleToolErrors,
50
54
  loadRuntimeTools,
55
+ toolRegistry,
51
56
  }: t.ToolNodeConstructorParams) {
52
57
  super({ name, tags, func: (input, config) => this.run(input, config) });
53
- this.tools = tools;
54
58
  this.toolMap = toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
55
59
  this.toolCallStepIds = toolCallStepIds;
56
60
  this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
57
61
  this.loadRuntimeTools = loadRuntimeTools;
58
62
  this.errorHandler = errorHandler;
59
63
  this.toolUsageCount = new Map<string, number>();
64
+ this.toolRegistry = toolRegistry;
65
+ }
66
+
67
+ /**
68
+ * Returns cached programmatic tools, computing once on first access.
69
+ * Single iteration builds both toolMap and toolDefs simultaneously.
70
+ */
71
+ private getProgrammaticTools(): { toolMap: t.ToolMap; toolDefs: t.LCTool[] } {
72
+ if (this.programmaticCache) return this.programmaticCache;
73
+
74
+ const toolMap: t.ToolMap = new Map();
75
+ const toolDefs: t.LCTool[] = [];
76
+
77
+ if (this.toolRegistry) {
78
+ for (const [name, toolDef] of this.toolRegistry) {
79
+ if (
80
+ (toolDef.allowed_callers ?? ['direct']).includes('code_execution')
81
+ ) {
82
+ toolDefs.push(toolDef);
83
+ const tool = this.toolMap.get(name);
84
+ if (tool) toolMap.set(name, tool);
85
+ }
86
+ }
87
+ }
88
+
89
+ this.programmaticCache = { toolMap, toolDefs };
90
+ return this.programmaticCache;
60
91
  }
61
92
 
62
93
  /**
@@ -83,22 +114,32 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
83
114
  this.toolUsageCount.set(call.name, turn + 1);
84
115
  const args = call.args;
85
116
  const stepId = this.toolCallStepIds?.get(call.id!);
86
- const output = await tool.invoke(
87
- { ...call, args, type: 'tool_call', stepId, turn },
88
- config
89
- );
90
-
91
- // Debug logging for image generation
92
- if (call.name === 'image_generation') {
93
- console.log('[ToolNode] image_generation output:', {
94
- isBaseMessage: isBaseMessage(output),
95
- messageType: isBaseMessage(output) ? output._getType() : 'not a message',
96
- isCommand: isCommand(output),
97
- hasArtifact: isBaseMessage(output) && (output as ToolMessage).artifact !== undefined,
98
- outputType: typeof output,
99
- });
117
+
118
+ // Build invoke params - LangChain extracts non-schema fields to config.toolCall
119
+ let invokeParams: Record<string, unknown> = {
120
+ ...call,
121
+ args,
122
+ type: 'tool_call',
123
+ stepId,
124
+ turn,
125
+ };
126
+
127
+ // Inject runtime data for special tools (becomes available at config.toolCall)
128
+ if (call.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
129
+ const { toolMap, toolDefs } = this.getProgrammaticTools();
130
+ invokeParams = {
131
+ ...invokeParams,
132
+ toolMap,
133
+ toolDefs,
134
+ };
135
+ } else if (call.name === Constants.TOOL_SEARCH_REGEX) {
136
+ invokeParams = {
137
+ ...invokeParams,
138
+ toolRegistry: this.toolRegistry,
139
+ };
100
140
  }
101
-
141
+
142
+ const output = await tool.invoke(invokeParams, config);
102
143
  if (
103
144
  (isBaseMessage(output) && output._getType() === 'tool') ||
104
145
  isCommand(output)
@@ -206,9 +247,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
206
247
  const { tools, toolMap } = this.loadRuntimeTools(
207
248
  aiMessage.tool_calls ?? []
208
249
  );
209
- this.tools = tools;
210
250
  this.toolMap =
211
251
  toolMap ?? new Map(tools.map((tool) => [tool.name, tool]));
252
+ this.programmaticCache = undefined; // Invalidate cache on toolMap change
212
253
  }
213
254
 
214
255
  outputs = await Promise.all(