illuma-agents 1.0.16 → 1.0.18
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.
- package/dist/cjs/agents/AgentContext.cjs +3 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +18 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +79 -32
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +5 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +1 -0
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +10 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +7 -8
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +15 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +11 -6
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +16 -8
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +9 -2
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +17 -10
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/stream.cjs +30 -16
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +209 -47
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +73 -3
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -0
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +3 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/contextAnalytics.cjs +66 -0
- package/dist/cjs/utils/contextAnalytics.cjs.map +1 -0
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/cjs/utils/toonFormat.cjs +388 -0
- package/dist/cjs/utils/toonFormat.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +3 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +19 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +81 -34
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +5 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +1 -0
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +10 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +7 -8
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +11 -6
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +18 -10
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +10 -3
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +19 -12
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/stream.mjs +30 -16
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +208 -48
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +73 -3
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -0
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +3 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/contextAnalytics.mjs +64 -0
- package/dist/esm/utils/contextAnalytics.mjs.map +1 -0
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/esm/utils/toonFormat.mjs +381 -0
- package/dist/esm/utils/toonFormat.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +17 -0
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +19 -0
- package/dist/types/types/tools.d.ts +3 -1
- package/dist/types/utils/contextAnalytics.d.ts +37 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/toonFormat.d.ts +111 -0
- package/package.json +3 -2
- package/src/agents/AgentContext.ts +28 -20
- package/src/common/enum.ts +18 -0
- package/src/graphs/Graph.ts +152 -62
- package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +495 -473
- package/src/llm/bedrock/index.ts +47 -35
- package/src/llm/openrouter/index.ts +11 -1
- package/src/llm/vertexai/index.ts +9 -10
- package/src/messages/cache.ts +104 -55
- package/src/messages/core.ts +29 -19
- package/src/messages/format.ts +14 -3
- package/src/messages/tools.ts +20 -13
- package/src/scripts/simple.ts +1 -1
- package/src/specs/emergency-prune.test.ts +407 -355
- package/src/stream.ts +28 -20
- package/src/tools/ProgrammaticToolCalling.ts +246 -52
- package/src/tools/ToolNode.ts +78 -5
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +155 -0
- package/src/tools/search/jina-reranker.test.ts +32 -28
- package/src/tools/search/search.ts +3 -1
- package/src/tools/search/tool.ts +16 -7
- package/src/types/tools.ts +3 -1
- package/src/utils/contextAnalytics.ts +103 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/llmConfig.ts +8 -1
- package/src/utils/run.ts +5 -4
- package/src/utils/toonFormat.ts +475 -0
package/src/stream.ts
CHANGED
|
@@ -107,24 +107,27 @@ export function getChunkContent({
|
|
|
107
107
|
| undefined
|
|
108
108
|
)?.summary?.[0]?.text;
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
/**
|
|
111
|
+
* For OpenRouter, reasoning is stored in additional_kwargs.reasoning (not reasoning_content).
|
|
112
|
+
* NOTE: We intentionally do NOT extract text from reasoning_details here.
|
|
113
|
+
* The reasoning_details array contains the FULL accumulated reasoning text (set only on final chunk),
|
|
114
|
+
* but individual reasoning tokens are already streamed via additional_kwargs.reasoning.
|
|
115
|
+
* Extracting from reasoning_details would cause duplication.
|
|
116
|
+
* The reasoning_details is only used for:
|
|
117
|
+
* 1. Detecting reasoning mode in handleReasoning()
|
|
118
|
+
* 2. Final message storage (for thought signatures)
|
|
119
|
+
*/
|
|
120
|
+
if (provider === Providers.OPENROUTER) {
|
|
121
|
+
// Content presence signals end of reasoning phase - prefer content over reasoning
|
|
122
|
+
// This handles transitional chunks that may have both reasoning and content
|
|
123
|
+
if (typeof chunk?.content === 'string' && chunk.content !== '') {
|
|
124
|
+
return chunk.content;
|
|
125
|
+
}
|
|
126
|
+
const reasoning = chunk?.additional_kwargs?.reasoning as string | undefined;
|
|
127
|
+
if (reasoning != null && reasoning !== '') {
|
|
128
|
+
return reasoning;
|
|
127
129
|
}
|
|
130
|
+
return chunk?.content;
|
|
128
131
|
}
|
|
129
132
|
return (
|
|
130
133
|
((chunk?.additional_kwargs?.[reasoningKey] as string | undefined) ?? '') ||
|
|
@@ -376,9 +379,14 @@ hasToolCallChunks: ${hasToolCallChunks}
|
|
|
376
379
|
reasoning_content = 'valid';
|
|
377
380
|
} else if (
|
|
378
381
|
agentContext.provider === Providers.OPENROUTER &&
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
+
// Only set reasoning as valid if content is NOT present (content signals end of reasoning)
|
|
383
|
+
(chunk.content == null || chunk.content === '') &&
|
|
384
|
+
// Check for reasoning_details (final chunk) OR reasoning string (intermediate chunks)
|
|
385
|
+
((chunk.additional_kwargs?.reasoning_details != null &&
|
|
386
|
+
Array.isArray(chunk.additional_kwargs.reasoning_details) &&
|
|
387
|
+
chunk.additional_kwargs.reasoning_details.length > 0) ||
|
|
388
|
+
(typeof chunk.additional_kwargs?.reasoning === 'string' &&
|
|
389
|
+
chunk.additional_kwargs.reasoning !== ''))
|
|
382
390
|
) {
|
|
383
391
|
reasoning_content = 'valid';
|
|
384
392
|
}
|
|
@@ -38,42 +38,35 @@ const ProgrammaticToolCallingSchema = z.object({
|
|
|
38
38
|
.string()
|
|
39
39
|
.min(1)
|
|
40
40
|
.describe(
|
|
41
|
-
`Python code that calls tools programmatically. Tools are
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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`
|
|
41
|
+
`Python code that calls tools programmatically. Tools are available as async functions.
|
|
42
|
+
|
|
43
|
+
CRITICAL - STATELESS EXECUTION:
|
|
44
|
+
Each call is a fresh Python interpreter. Variables, imports, and data do NOT persist between calls.
|
|
45
|
+
You MUST complete your entire workflow in ONE code block: query → process → output.
|
|
46
|
+
DO NOT split work across multiple calls expecting to reuse variables.
|
|
47
|
+
|
|
48
|
+
Your code is auto-wrapped in async context. Just write logic with await—no boilerplate needed.
|
|
49
|
+
|
|
50
|
+
Example (Complete workflow in one call):
|
|
51
|
+
# Query data
|
|
52
|
+
data = await query_database(sql="SELECT * FROM users")
|
|
53
|
+
# Process it
|
|
54
|
+
df = pd.DataFrame(data)
|
|
55
|
+
summary = df.groupby('region').sum()
|
|
56
|
+
# Output results
|
|
57
|
+
await write_to_sheet(spreadsheet_id=sid, data=summary.to_dict())
|
|
58
|
+
print(f"Wrote {len(summary)} rows")
|
|
59
|
+
|
|
60
|
+
Example (Parallel calls):
|
|
61
|
+
sf, ny = await asyncio.gather(get_weather(city="SF"), get_weather(city="NY"))
|
|
62
|
+
print(f"SF: {sf}, NY: {ny}")
|
|
63
|
+
|
|
64
|
+
Rules:
|
|
65
|
+
- EVERYTHING in one call—no state persists between executions
|
|
66
|
+
- Just write code with await—auto-wrapped in async context
|
|
67
|
+
- DO NOT define async def main() or call asyncio.run()
|
|
68
|
+
- Tools are pre-defined—DO NOT write function definitions
|
|
69
|
+
- Only print() output returns to the model`
|
|
77
70
|
),
|
|
78
71
|
session_id: z
|
|
79
72
|
.string()
|
|
@@ -234,6 +227,67 @@ export function filterToolsByUsage(
|
|
|
234
227
|
return toolDefs.filter((tool) => usedToolNames.has(tool.name));
|
|
235
228
|
}
|
|
236
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Fetches files from a previous session to make them available for the current execution.
|
|
232
|
+
* Files are returned as CodeEnvFile references to be included in the request.
|
|
233
|
+
* @param baseUrl - The base URL for the Code API
|
|
234
|
+
* @param apiKey - The API key for authentication
|
|
235
|
+
* @param sessionId - The session ID to fetch files from
|
|
236
|
+
* @param proxy - Optional HTTP proxy URL
|
|
237
|
+
* @returns Array of CodeEnvFile references, or empty array if fetch fails
|
|
238
|
+
*/
|
|
239
|
+
export async function fetchSessionFiles(
|
|
240
|
+
baseUrl: string,
|
|
241
|
+
apiKey: string,
|
|
242
|
+
sessionId: string,
|
|
243
|
+
proxy?: string
|
|
244
|
+
): Promise<t.CodeEnvFile[]> {
|
|
245
|
+
try {
|
|
246
|
+
const filesEndpoint = `${baseUrl}/files/${sessionId}?detail=full`;
|
|
247
|
+
const fetchOptions: RequestInit = {
|
|
248
|
+
method: 'GET',
|
|
249
|
+
headers: {
|
|
250
|
+
'User-Agent': 'LibreChat/1.0',
|
|
251
|
+
'X-API-Key': apiKey,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (proxy != null && proxy !== '') {
|
|
256
|
+
fetchOptions.agent = new HttpsProxyAgent(proxy);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const response = await fetch(filesEndpoint, fetchOptions);
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
throw new Error(`Failed to fetch files for session: ${response.status}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const files = await response.json();
|
|
265
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return files.map((file: Record<string, unknown>) => {
|
|
270
|
+
// Extract the ID from the file name (part after session ID prefix and before extension)
|
|
271
|
+
const nameParts = (file.name as string).split('/');
|
|
272
|
+
const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
session_id: sessionId,
|
|
276
|
+
id,
|
|
277
|
+
name: (file.metadata as Record<string, unknown>)[
|
|
278
|
+
'original-filename'
|
|
279
|
+
] as string,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// eslint-disable-next-line no-console
|
|
284
|
+
console.warn(
|
|
285
|
+
`Failed to fetch files for session: ${sessionId}, ${(error as Error).message}`
|
|
286
|
+
);
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
237
291
|
/**
|
|
238
292
|
* Makes an HTTP request to the Code API.
|
|
239
293
|
* @param endpoint - The API endpoint URL
|
|
@@ -274,9 +328,140 @@ export async function makeRequest(
|
|
|
274
328
|
return (await response.json()) as t.ProgrammaticExecutionResponse;
|
|
275
329
|
}
|
|
276
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Unwraps tool responses that may be formatted as tuples or content blocks.
|
|
333
|
+
* MCP tools return [content, artifacts], we need to extract the raw data.
|
|
334
|
+
* @param result - The raw result from tool.invoke()
|
|
335
|
+
* @param isMCPTool - Whether this is an MCP tool (has mcp property)
|
|
336
|
+
* @returns Unwrapped raw data (string, object, or parsed JSON)
|
|
337
|
+
*/
|
|
338
|
+
export function unwrapToolResponse(
|
|
339
|
+
result: unknown,
|
|
340
|
+
isMCPTool: boolean
|
|
341
|
+
): unknown {
|
|
342
|
+
// Only unwrap if this is an MCP tool and result is a tuple
|
|
343
|
+
if (!isMCPTool) {
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Checks if a value is a content block object (has type and text).
|
|
349
|
+
*/
|
|
350
|
+
const isContentBlock = (value: unknown): boolean => {
|
|
351
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
const obj = value as Record<string, unknown>;
|
|
355
|
+
return typeof obj.type === 'string';
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Checks if an array is an array of content blocks.
|
|
360
|
+
*/
|
|
361
|
+
const isContentBlockArray = (arr: unknown[]): boolean => {
|
|
362
|
+
return arr.length > 0 && arr.every(isContentBlock);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extracts text from a single content block object.
|
|
367
|
+
* Returns the text if it's a text block, otherwise returns null.
|
|
368
|
+
*/
|
|
369
|
+
const extractTextFromBlock = (block: unknown): string | null => {
|
|
370
|
+
if (typeof block !== 'object' || block === null) return null;
|
|
371
|
+
const b = block as Record<string, unknown>;
|
|
372
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
373
|
+
return b.text;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Extracts text from content blocks (array or single object).
|
|
380
|
+
* Returns combined text or null if no text blocks found.
|
|
381
|
+
*/
|
|
382
|
+
const extractTextFromContent = (content: unknown): string | null => {
|
|
383
|
+
// Single content block object: { type: 'text', text: '...' }
|
|
384
|
+
if (
|
|
385
|
+
typeof content === 'object' &&
|
|
386
|
+
content !== null &&
|
|
387
|
+
!Array.isArray(content)
|
|
388
|
+
) {
|
|
389
|
+
const text = extractTextFromBlock(content);
|
|
390
|
+
if (text !== null) return text;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Array of content blocks: [{ type: 'text', text: '...' }, ...]
|
|
394
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
395
|
+
const texts = content
|
|
396
|
+
.map(extractTextFromBlock)
|
|
397
|
+
.filter((t): t is string => t !== null);
|
|
398
|
+
if (texts.length > 0) {
|
|
399
|
+
return texts.join('\n');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return null;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Tries to parse a string as JSON if it looks like JSON.
|
|
408
|
+
*/
|
|
409
|
+
const maybeParseJSON = (str: string): unknown => {
|
|
410
|
+
const trimmed = str.trim();
|
|
411
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse(trimmed);
|
|
414
|
+
} catch {
|
|
415
|
+
return str;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return str;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// Handle array of content blocks at top level FIRST
|
|
422
|
+
// (before checking for tuple, since both are arrays)
|
|
423
|
+
if (Array.isArray(result) && isContentBlockArray(result)) {
|
|
424
|
+
const extractedText = extractTextFromContent(result);
|
|
425
|
+
if (extractedText !== null) {
|
|
426
|
+
return maybeParseJSON(extractedText);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check if result is a tuple/array with [content, artifacts]
|
|
431
|
+
if (Array.isArray(result) && result.length >= 1) {
|
|
432
|
+
const [content] = result;
|
|
433
|
+
|
|
434
|
+
// If first element is a string, return it (possibly parsed as JSON)
|
|
435
|
+
if (typeof content === 'string') {
|
|
436
|
+
return maybeParseJSON(content);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Try to extract text from content blocks
|
|
440
|
+
const extractedText = extractTextFromContent(content);
|
|
441
|
+
if (extractedText !== null) {
|
|
442
|
+
return maybeParseJSON(extractedText);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// If first element is an object (but not a text block), return it
|
|
446
|
+
if (typeof content === 'object' && content !== null) {
|
|
447
|
+
return content;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Handle single content block object at top level (not in tuple)
|
|
452
|
+
const extractedText = extractTextFromContent(result);
|
|
453
|
+
if (extractedText !== null) {
|
|
454
|
+
return maybeParseJSON(extractedText);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Not a formatted response, return as-is
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
277
461
|
/**
|
|
278
462
|
* Executes tools in parallel when requested by the API.
|
|
279
463
|
* Uses Promise.all for parallel execution, catching individual errors.
|
|
464
|
+
* Unwraps formatted responses (e.g., MCP tool tuples) to raw data.
|
|
280
465
|
* @param toolCalls - Array of tool calls from the API
|
|
281
466
|
* @param toolMap - Map of tool names to executable tools
|
|
282
467
|
* @returns Array of tool results
|
|
@@ -301,9 +486,13 @@ export async function executeTools(
|
|
|
301
486
|
const result = await tool.invoke(call.input, {
|
|
302
487
|
metadata: { [Constants.PROGRAMMATIC_TOOL_CALLING]: true },
|
|
303
488
|
});
|
|
489
|
+
|
|
490
|
+
const isMCPTool = tool.mcp === true;
|
|
491
|
+
const unwrappedResult = unwrapToolResponse(result, isMCPTool);
|
|
492
|
+
|
|
304
493
|
return {
|
|
305
494
|
call_id: call.id,
|
|
306
|
-
result,
|
|
495
|
+
result: unwrappedResult,
|
|
307
496
|
is_error: false,
|
|
308
497
|
};
|
|
309
498
|
} catch (error) {
|
|
@@ -414,25 +603,23 @@ export function createProgrammaticToolCallingTool(
|
|
|
414
603
|
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;
|
|
415
604
|
|
|
416
605
|
const description = `
|
|
417
|
-
Run tools
|
|
606
|
+
Run tools via Python code. Auto-wrapped in async context—just use \`await\` directly.
|
|
418
607
|
|
|
419
|
-
|
|
608
|
+
CRITICAL - STATELESS: Each call is a fresh interpreter. Variables/imports do NOT persist.
|
|
609
|
+
Complete your ENTIRE workflow in ONE call: fetch → process → save. No splitting across calls.
|
|
420
610
|
|
|
421
|
-
|
|
422
|
-
-
|
|
423
|
-
-
|
|
424
|
-
-
|
|
611
|
+
Rules:
|
|
612
|
+
- Everything in ONE code block—no state carries over between executions
|
|
613
|
+
- Do NOT define \`async def main()\` or call \`asyncio.run()\`—just write code with await
|
|
614
|
+
- Tools are pre-defined—DO NOT write function definitions
|
|
615
|
+
- Only \`print()\` output returns; tool results are raw dicts/lists/strings
|
|
616
|
+
- Use \`session_id\` param for file persistence across calls
|
|
617
|
+
- Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix
|
|
425
618
|
|
|
426
|
-
|
|
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"))
|
|
619
|
+
When to use: loops, conditionals, parallel (\`asyncio.gather\`), multi-step pipelines.
|
|
430
620
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
- You want parallel execution (asyncio.gather)
|
|
434
|
-
- You need conditionals based on tool results
|
|
435
|
-
- You want to aggregate/filter data before returning
|
|
621
|
+
Example (complete pipeline):
|
|
622
|
+
data = await query_db(sql="..."); df = process(data); await save_to_sheet(data=df); print("Done")
|
|
436
623
|
`.trim();
|
|
437
624
|
|
|
438
625
|
return tool<typeof ProgrammaticToolCallingSchema>(
|
|
@@ -474,6 +661,12 @@ When to use this instead of calling tools directly:
|
|
|
474
661
|
);
|
|
475
662
|
}
|
|
476
663
|
|
|
664
|
+
// Fetch files from previous session if session_id is provided
|
|
665
|
+
let files: t.CodeEnvFile[] | undefined;
|
|
666
|
+
if (session_id != null && session_id.length > 0) {
|
|
667
|
+
files = await fetchSessionFiles(baseUrl, apiKey, session_id, proxy);
|
|
668
|
+
}
|
|
669
|
+
|
|
477
670
|
let response = await makeRequest(
|
|
478
671
|
EXEC_ENDPOINT,
|
|
479
672
|
apiKey,
|
|
@@ -482,6 +675,7 @@ When to use this instead of calling tools directly:
|
|
|
482
675
|
tools: effectiveTools,
|
|
483
676
|
session_id,
|
|
484
677
|
timeout,
|
|
678
|
+
...(files && files.length > 0 ? { files } : {}),
|
|
485
679
|
},
|
|
486
680
|
proxy
|
|
487
681
|
);
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -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 { processToolOutput } from '@/utils/toonFormat';
|
|
23
24
|
import { Constants } from '@/common';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -29,6 +30,43 @@ function isSend(value: unknown): value is Send {
|
|
|
29
30
|
return value instanceof Send;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Extract text content from a ToolMessage content field.
|
|
35
|
+
* Handles both string and MessageContentComplex[] formats.
|
|
36
|
+
* For array content (e.g., from content_and_artifact tools), extracts text from text blocks.
|
|
37
|
+
*/
|
|
38
|
+
function extractStringContent(content: unknown): string {
|
|
39
|
+
// Already a string - return as is
|
|
40
|
+
if (typeof content === 'string') {
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Array of content blocks - extract text from each
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
const textParts: string[] = [];
|
|
47
|
+
for (const block of content) {
|
|
48
|
+
if (typeof block === 'string') {
|
|
49
|
+
textParts.push(block);
|
|
50
|
+
} else if (block && typeof block === 'object') {
|
|
51
|
+
// Handle {type: 'text', text: '...'} format
|
|
52
|
+
const obj = block as Record<string, unknown>;
|
|
53
|
+
if (obj.type === 'text' && typeof obj.text === 'string') {
|
|
54
|
+
textParts.push(obj.text);
|
|
55
|
+
} else if (typeof obj.text === 'string') {
|
|
56
|
+
// Just has 'text' property
|
|
57
|
+
textParts.push(obj.text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (textParts.length > 0) {
|
|
62
|
+
return textParts.join('\n');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fallback: stringify whatever it is
|
|
67
|
+
return JSON.stringify(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
71
|
export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
34
72
|
private toolMap: Map<string, StructuredToolInterface | RunnableToolLike>;
|
|
@@ -140,16 +178,51 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
const output = await tool.invoke(invokeParams, config);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
) {
|
|
181
|
+
|
|
182
|
+
// Handle Command outputs directly
|
|
183
|
+
if (isCommand(output)) {
|
|
147
184
|
return output;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ========================================================================
|
|
188
|
+
// TOOL OUTPUT PROCESSING - Single point for all tools (MCP and non-MCP)
|
|
189
|
+
// 1. Extract string content from any output format
|
|
190
|
+
// 2. Apply TOON conversion if content contains JSON
|
|
191
|
+
// 3. Apply truncation if still too large
|
|
192
|
+
// 4. Return ToolMessage with processed string content
|
|
193
|
+
// ========================================================================
|
|
194
|
+
|
|
195
|
+
// Step 1: Extract string content from the output
|
|
196
|
+
let rawContent: string;
|
|
197
|
+
if (isBaseMessage(output) && output._getType() === 'tool') {
|
|
198
|
+
const toolMsg = output as ToolMessage;
|
|
199
|
+
rawContent = extractStringContent(toolMsg.content);
|
|
200
|
+
} else {
|
|
201
|
+
rawContent = extractStringContent(output);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Step 2 & 3: Apply TOON conversion and truncation
|
|
205
|
+
const processed = processToolOutput(rawContent, {
|
|
206
|
+
maxLength: 100000, // 100K char limit
|
|
207
|
+
enableToon: true,
|
|
208
|
+
minSizeForToon: 1000,
|
|
209
|
+
minReductionPercent: 10, // Only apply TOON when clearly beneficial
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Step 4: Return ToolMessage with processed string content
|
|
213
|
+
if (isBaseMessage(output) && output._getType() === 'tool') {
|
|
214
|
+
const toolMsg = output as ToolMessage;
|
|
215
|
+
return new ToolMessage({
|
|
216
|
+
status: toolMsg.status,
|
|
217
|
+
name: toolMsg.name,
|
|
218
|
+
content: processed.content,
|
|
219
|
+
tool_call_id: toolMsg.tool_call_id,
|
|
220
|
+
});
|
|
148
221
|
} else {
|
|
149
222
|
return new ToolMessage({
|
|
150
223
|
status: 'success',
|
|
151
224
|
name: tool.name,
|
|
152
|
-
content:
|
|
225
|
+
content: processed.content,
|
|
153
226
|
tool_call_id: call.id!,
|
|
154
227
|
});
|
|
155
228
|
}
|