@librechat/agents 3.1.78 → 3.1.80-dev.0

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 (76) hide show
  1. package/dist/cjs/llm/anthropic/index.cjs +44 -55
  2. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
  4. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
  6. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  7. package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
  8. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  9. package/dist/cjs/messages/format.cjs +97 -14
  10. package/dist/cjs/messages/format.cjs.map +1 -1
  11. package/dist/cjs/tools/BashExecutor.cjs +10 -2
  12. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  13. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -1
  14. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/CodeExecutor.cjs +16 -5
  16. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  17. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -4
  18. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +63 -40
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
  22. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  23. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  24. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  25. package/dist/esm/llm/anthropic/index.mjs +43 -54
  26. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  27. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
  28. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  29. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
  30. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  31. package/dist/esm/messages/anthropicToolCache.mjs +48 -15
  32. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  33. package/dist/esm/messages/format.mjs +97 -14
  34. package/dist/esm/messages/format.mjs.map +1 -1
  35. package/dist/esm/tools/BashExecutor.mjs +10 -2
  36. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  37. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -1
  38. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  39. package/dist/esm/tools/CodeExecutor.mjs +16 -5
  40. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  41. package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -4
  42. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  43. package/dist/esm/tools/ToolNode.mjs +63 -40
  44. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  45. package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
  46. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  47. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  48. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  49. package/dist/types/llm/anthropic/index.d.ts +1 -9
  50. package/dist/types/messages/anthropicToolCache.d.ts +5 -5
  51. package/dist/types/types/tools.d.ts +82 -17
  52. package/package.json +1 -1
  53. package/src/llm/anthropic/index.ts +55 -64
  54. package/src/llm/anthropic/llm.spec.ts +585 -0
  55. package/src/llm/anthropic/utils/message_inputs.ts +36 -21
  56. package/src/llm/anthropic/utils/message_outputs.ts +0 -4
  57. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
  58. package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
  59. package/src/messages/anthropicToolCache.ts +70 -25
  60. package/src/messages/format.ts +117 -18
  61. package/src/messages/formatAgentMessages.test.ts +202 -1
  62. package/src/scripts/code_exec_multi_session.ts +4 -4
  63. package/src/specs/summarization.test.ts +3 -3
  64. package/src/tools/BashExecutor.ts +11 -3
  65. package/src/tools/BashProgrammaticToolCalling.ts +6 -6
  66. package/src/tools/CodeExecutor.ts +17 -6
  67. package/src/tools/ProgrammaticToolCalling.ts +14 -10
  68. package/src/tools/ToolNode.ts +85 -48
  69. package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
  70. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +9 -2
  71. package/src/tools/__tests__/ToolNode.session.test.ts +131 -50
  72. package/src/tools/local/LocalExecutionEngine.ts +55 -54
  73. package/src/tools/local/LocalExecutionTools.ts +2 -2
  74. package/src/tools/local/LocalProgrammaticToolCalling.ts +23 -6
  75. package/src/types/diff.d.ts +15 -0
  76. package/src/types/tools.ts +79 -17
@@ -285,6 +285,7 @@ export const formatFromLangChain = (
285
285
 
286
286
  interface FormatAssistantMessageOptions {
287
287
  preserveReasoningContent?: boolean;
288
+ provider?: Providers;
288
289
  }
289
290
 
290
291
  interface FormatAgentMessagesOptions {
@@ -316,6 +317,60 @@ function extractReasoningContent(
316
317
  return '';
317
318
  }
318
319
 
320
+ type ServerToolInput = Exclude<NonNullable<ToolCallPart['args']>, string>;
321
+
322
+ function parseServerToolInput(args: ToolCallPart['args']): ServerToolInput {
323
+ if (typeof args === 'string') {
324
+ try {
325
+ const parsed = JSON.parse(args) as unknown;
326
+ return parsed != null &&
327
+ typeof parsed === 'object' &&
328
+ !Array.isArray(parsed)
329
+ ? (parsed as ServerToolInput)
330
+ : {};
331
+ } catch {
332
+ return {};
333
+ }
334
+ }
335
+ return args != null && typeof args === 'object' ? args : {};
336
+ }
337
+
338
+ function getTextContent(part: MessageContentComplex): string {
339
+ const { text } = part as { text?: unknown };
340
+ return typeof text === 'string' ? text : '';
341
+ }
342
+
343
+ function hasMeaningfulAssistantContent(part: MessageContentComplex): boolean {
344
+ if (part.type === ContentTypes.TEXT) {
345
+ return getTextContent(part).trim().length > 0;
346
+ }
347
+ if (
348
+ part.type === ContentTypes.TOOL_CALL ||
349
+ part.type === ContentTypes.ERROR ||
350
+ part.type === ContentTypes.AGENT_UPDATE ||
351
+ part.type === ContentTypes.SUMMARY
352
+ ) {
353
+ return false;
354
+ }
355
+ if (
356
+ part.type === ContentTypes.THINK ||
357
+ part.type === ContentTypes.THINKING ||
358
+ part.type === ContentTypes.REASONING ||
359
+ part.type === ContentTypes.REASONING_CONTENT ||
360
+ part.type === 'redacted_thinking'
361
+ ) {
362
+ return extractReasoningContent(part).trim().length > 0;
363
+ }
364
+ return part.type != null && part.type !== '';
365
+ }
366
+
367
+ function getToolUseId(part: MessageContentComplex): string | undefined {
368
+ if (!('tool_use_id' in part) || typeof part.tool_use_id !== 'string') {
369
+ return undefined;
370
+ }
371
+ return part.tool_use_id;
372
+ }
373
+
319
374
  /**
320
375
  * Helper function to format an assistant message
321
376
  * @param message The message to format
@@ -331,6 +386,8 @@ function formatAssistantMessage(
331
386
  let lastAIMessage: AIMessage | null = null;
332
387
  let hasReasoning = false;
333
388
  let pendingReasoningContent = '';
389
+ const emittedServerToolUseIds = new Set<string>();
390
+ const pendingServerToolUses = new Map<string, MessageContentComplex>();
334
391
  const shouldPreserveReasoningContent =
335
392
  options?.preserveReasoningContent === true;
336
393
 
@@ -364,13 +421,32 @@ function formatAssistantMessage(
364
421
  : reasoningContent;
365
422
  };
366
423
 
424
+ const flushPendingServerToolUse = (toolUseId: string): void => {
425
+ for (const [id, content] of pendingServerToolUses) {
426
+ pendingServerToolUses.delete(id);
427
+ if (id === toolUseId) {
428
+ currentContent.push(content);
429
+ emittedServerToolUseIds.add(id);
430
+ return;
431
+ }
432
+ }
433
+ };
434
+
367
435
  if (Array.isArray(message.content)) {
368
- for (const part of message.content as Array<
436
+ const contentParts = message.content as Array<
369
437
  MessageContentComplex | undefined | null
370
- >) {
438
+ >;
439
+
440
+ for (const part of contentParts) {
371
441
  if (part == null) {
372
442
  continue;
373
443
  }
444
+ const toolUseId = getToolUseId(part);
445
+ if (toolUseId != null) {
446
+ flushPendingServerToolUse(toolUseId);
447
+ } else if (hasMeaningfulAssistantContent(part)) {
448
+ pendingServerToolUses.clear();
449
+ }
374
450
  if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
375
451
  /*
376
452
  If there's pending content, it needs to be aggregated as a single string to prepare for tool calls.
@@ -379,19 +455,18 @@ function formatAssistantMessage(
379
455
  if (currentContent.length > 0) {
380
456
  let content = currentContent.reduce((acc, curr) => {
381
457
  if (curr.type === ContentTypes.TEXT) {
382
- return `${acc}${String(curr[ContentTypes.TEXT] ?? '')}\n`;
458
+ return `${acc}${getTextContent(curr)}\n`;
383
459
  }
384
460
  return acc;
385
461
  }, '');
386
- content =
387
- `${content}\n${part[ContentTypes.TEXT] ?? part.text ?? ''}`.trim();
462
+ content = `${content}\n${getTextContent(part)}`.trim();
388
463
  lastAIMessage = createAIMessage(content);
389
464
  formattedMessages.push(lastAIMessage);
390
465
  currentContent = [];
391
466
  continue;
392
467
  }
393
468
  // Create a new AIMessage with this text and prepare for tool calls
394
- lastAIMessage = createAIMessage(part.text != null ? part.text : '');
469
+ lastAIMessage = createAIMessage(getTextContent(part));
395
470
  formattedMessages.push(lastAIMessage);
396
471
  } else if (part.type === ContentTypes.TOOL_CALL) {
397
472
  // Skip malformed tool call entries without tool_call property
@@ -414,6 +489,26 @@ function formatAssistantMessage(
414
489
  continue;
415
490
  }
416
491
 
492
+ if (
493
+ options?.provider === Providers.ANTHROPIC &&
494
+ typeof _tool_call.id === 'string' &&
495
+ _tool_call.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
496
+ ) {
497
+ if (
498
+ emittedServerToolUseIds.has(_tool_call.id) ||
499
+ pendingServerToolUses.has(_tool_call.id)
500
+ ) {
501
+ continue;
502
+ }
503
+ pendingServerToolUses.set(_tool_call.id, {
504
+ type: 'server_tool_use',
505
+ id: _tool_call.id,
506
+ name: _tool_call.name,
507
+ input: parseServerToolInput(_args),
508
+ } as MessageContentComplex);
509
+ continue;
510
+ }
511
+
417
512
  if (!lastAIMessage) {
418
513
  // "Heal" the payload by creating an AIMessage to precede the tool call
419
514
  lastAIMessage = createAIMessage('');
@@ -465,26 +560,29 @@ function formatAssistantMessage(
465
560
  ) {
466
561
  continue;
467
562
  } else {
468
- if (
469
- part.type === ContentTypes.TEXT &&
470
- !String(part.text ?? '').trim()
471
- ) {
563
+ if (part.type === ContentTypes.TEXT && !getTextContent(part).trim()) {
472
564
  continue;
473
565
  }
474
566
  currentContent.push(part);
475
567
  }
476
568
  }
569
+ for (const content of pendingServerToolUses.values()) {
570
+ currentContent.push(content);
571
+ }
477
572
  }
478
573
 
479
574
  if (hasReasoning && currentContent.length > 0) {
480
- const content = currentContent
481
- .reduce((acc, curr) => {
482
- if (curr.type === ContentTypes.TEXT) {
483
- return `${acc}${String(curr[ContentTypes.TEXT] ?? '')}\n`;
484
- }
485
- return acc;
486
- }, '')
487
- .trim();
575
+ let content = '';
576
+ for (const part of currentContent) {
577
+ if (part.type !== ContentTypes.TEXT) {
578
+ formattedMessages.push(
579
+ createAIMessage(toLangChainContent(currentContent))
580
+ );
581
+ return formattedMessages;
582
+ }
583
+ content += `${getTextContent(part)}\n`;
584
+ }
585
+ content = content.trim();
488
586
 
489
587
  if (content) {
490
588
  formattedMessages.push(createAIMessage(content));
@@ -1157,6 +1255,7 @@ export const formatAgentMessages = (
1157
1255
 
1158
1256
  const formattedMessages = formatAssistantMessage(processedMessage, {
1159
1257
  preserveReasoningContent: options?.provider === Providers.DEEPSEEK,
1258
+ provider: options?.provider,
1160
1259
  });
1161
1260
  if (sourceMessageId != null && sourceMessageId !== '') {
1162
1261
  for (const formattedMessage of formattedMessages) {
@@ -6,7 +6,8 @@ import {
6
6
  } from '@langchain/core/messages';
7
7
  import type { MessageContentComplex, TPayload } from '@/types';
8
8
  import { formatAgentMessages } from './format';
9
- import { ContentTypes, Providers } from '@/common';
9
+ import { _convertMessagesToAnthropicPayload } from '@/llm/anthropic/utils/message_inputs';
10
+ import { Constants, ContentTypes, Providers } from '@/common';
10
11
 
11
12
  describe('formatAgentMessages', () => {
12
13
  it('should format simple user and AI messages', () => {
@@ -183,6 +184,206 @@ describe('formatAgentMessages', () => {
183
184
  expect((result.messages[1] as ToolMessage).tool_call_id).toBe('123');
184
185
  });
185
186
 
187
+ it('skips persisted Anthropic server tool calls from web search turns', () => {
188
+ const payload: TPayload = [
189
+ {
190
+ role: 'user',
191
+ content:
192
+ 'who is the lowest seed survived in 2026 nba playoffs, only the team name, nothing else',
193
+ },
194
+ {
195
+ role: 'assistant',
196
+ content: [
197
+ {
198
+ type: ContentTypes.TOOL_CALL,
199
+ tool_call: {
200
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}web_search`,
201
+ name: 'web_search',
202
+ args: '{"query":"2026 NBA playoffs lowest seed survived"}',
203
+ },
204
+ },
205
+ {
206
+ type: ContentTypes.TEXT,
207
+ [ContentTypes.TEXT]: 'Philadelphia 76ers',
208
+ },
209
+ ],
210
+ },
211
+ {
212
+ role: 'user',
213
+ content: 'who are 76ers\' opponents in current series?',
214
+ },
215
+ ];
216
+
217
+ const result = formatAgentMessages(
218
+ payload,
219
+ undefined,
220
+ new Set(['web_search']),
221
+ undefined,
222
+ { provider: Providers.ANTHROPIC }
223
+ );
224
+
225
+ expect(result.messages).toHaveLength(3);
226
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
227
+ expect(
228
+ result.messages.some((message) => message instanceof ToolMessage)
229
+ ).toBe(false);
230
+ expect((result.messages[1] as AIMessage).tool_calls).toHaveLength(0);
231
+ expect(result.messages[1].content).toEqual([
232
+ {
233
+ type: ContentTypes.TEXT,
234
+ [ContentTypes.TEXT]: 'Philadelphia 76ers',
235
+ },
236
+ ]);
237
+ });
238
+
239
+ it('preserves paused Anthropic server tool calls without creating ToolMessages', () => {
240
+ const payload: TPayload = [
241
+ {
242
+ role: 'assistant',
243
+ content: [
244
+ {
245
+ type: ContentTypes.TOOL_CALL,
246
+ tool_call: {
247
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`,
248
+ name: 'web_search',
249
+ args: '{"query":"latest Anthropic server tools"}',
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ ];
255
+
256
+ const result = formatAgentMessages(
257
+ payload,
258
+ undefined,
259
+ new Set(['web_search']),
260
+ undefined,
261
+ { provider: Providers.ANTHROPIC }
262
+ );
263
+ const anthropicPayload = _convertMessagesToAnthropicPayload(
264
+ result.messages
265
+ );
266
+
267
+ expect(result.messages).toHaveLength(1);
268
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
269
+ expect(result.messages.some((message) => message instanceof ToolMessage))
270
+ .toBe(false);
271
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(0);
272
+ expect(result.messages[0].content).toEqual([
273
+ {
274
+ type: 'server_tool_use',
275
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`,
276
+ name: 'web_search',
277
+ input: { query: 'latest Anthropic server tools' },
278
+ },
279
+ ]);
280
+ expect(anthropicPayload.messages[0].content).toEqual([
281
+ {
282
+ type: 'server_tool_use',
283
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`,
284
+ name: 'web_search',
285
+ input: { query: 'latest Anthropic server tools' },
286
+ },
287
+ ]);
288
+ });
289
+
290
+ it('keeps srvtoolu tool calls portable for non-Anthropic providers', () => {
291
+ const payload: TPayload = [
292
+ {
293
+ role: 'assistant',
294
+ content: [
295
+ {
296
+ type: ContentTypes.TOOL_CALL,
297
+ tool_call: {
298
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`,
299
+ name: 'web_search',
300
+ args: '{"query":"latest Anthropic server tools"}',
301
+ },
302
+ },
303
+ ],
304
+ },
305
+ ];
306
+
307
+ const result = formatAgentMessages(
308
+ payload,
309
+ undefined,
310
+ new Set(['web_search']),
311
+ undefined,
312
+ { provider: Providers.OPENAI }
313
+ );
314
+
315
+ expect(result.messages).toHaveLength(2);
316
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
317
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
318
+ expect(result.messages[0].content).toBe('');
319
+ expect((result.messages[0] as AIMessage).tool_calls).toEqual([
320
+ {
321
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`,
322
+ name: 'web_search',
323
+ args: { query: 'latest Anthropic server tools' },
324
+ },
325
+ ]);
326
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe(
327
+ `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}paused`
328
+ );
329
+ });
330
+
331
+ it('does not emit empty Anthropic payload content for persisted web search turns', () => {
332
+ const payload: TPayload = [
333
+ {
334
+ role: 'user',
335
+ content:
336
+ 'who is the lowest seed survived in 2026 nba playoffs, only the team name, nothing else',
337
+ },
338
+ {
339
+ role: 'assistant',
340
+ content: [
341
+ {
342
+ type: ContentTypes.TOOL_CALL,
343
+ tool_call: {
344
+ id: `${Constants.ANTHROPIC_SERVER_TOOL_PREFIX}web_search`,
345
+ name: 'web_search',
346
+ args: '{"query":"2026 NBA playoffs lowest seed survived"}',
347
+ },
348
+ },
349
+ {
350
+ type: ContentTypes.TEXT,
351
+ text: 'Philadelphia 76ers',
352
+ },
353
+ ],
354
+ },
355
+ {
356
+ role: 'user',
357
+ content: 'who are 76ers\' opponents in current series?',
358
+ },
359
+ ];
360
+
361
+ const { messages } = formatAgentMessages(
362
+ payload,
363
+ undefined,
364
+ new Set(['web_search']),
365
+ undefined,
366
+ { provider: Providers.ANTHROPIC }
367
+ );
368
+ const anthropicPayload = _convertMessagesToAnthropicPayload(messages);
369
+
370
+ expect(anthropicPayload.messages).toHaveLength(3);
371
+ for (const message of anthropicPayload.messages) {
372
+ expect(Array.isArray(message.content)).toBe(true);
373
+ const content = message.content as Array<{
374
+ text?: unknown;
375
+ type: string;
376
+ }>;
377
+ expect(content.length).toBeGreaterThan(0);
378
+ for (const block of content) {
379
+ if (block.type === ContentTypes.TEXT) {
380
+ expect(typeof block.text).toBe('string');
381
+ expect((block.text as string).trim().length).toBeGreaterThan(0);
382
+ }
383
+ }
384
+ }
385
+ });
386
+
186
387
  it('should handle malformed tool call entries with missing tool_call property', () => {
187
388
  const tools = new Set(['search']);
188
389
  const payload = [
@@ -46,7 +46,7 @@ function printSessionContext(run: Run<t.IState>, label: string): void {
46
46
  console.log(` Latest session_id: ${session.session_id}`);
47
47
  console.log(` Files tracked: ${session.files?.length ?? 0}`);
48
48
  for (const file of session.files ?? []) {
49
- console.log(` - ${file.name} (session: ${file.session_id})`);
49
+ console.log(` - ${file.name} (storage: ${file.storage_session_id})`);
50
50
  }
51
51
  }
52
52
 
@@ -200,13 +200,13 @@ Tell me what version it shows.
200
200
 
201
201
  if (finalSession) {
202
202
  const files = finalSession.files ?? [];
203
- const uniqueSessionIds = new Set(files.map((f) => f.session_id));
203
+ const uniqueSessionIds = new Set(files.map((f) => f.storage_session_id));
204
204
  console.log(`\nTotal files tracked: ${files.length}`);
205
- console.log(`Unique session_ids: ${uniqueSessionIds.size}`);
205
+ console.log(`Unique storage_session_ids: ${uniqueSessionIds.size}`);
206
206
  console.log('\nFiles:');
207
207
  for (const file of files) {
208
208
  console.log(
209
- ` - ${file.name} (session: ${file.session_id?.slice(0, 20)}...)`
209
+ ` - ${file.name} (storage: ${file.storage_session_id?.slice(0, 20)}...)`
210
210
  );
211
211
  }
212
212
 
@@ -428,7 +428,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
428
428
 
429
429
  // Turn 7: absolute minimum context if still nothing
430
430
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
431
- ({ run, contentParts } = await createRun(3100));
431
+ ({ run, contentParts } = await createRun(2200));
432
432
  await runTurn({ run, conversationHistory }, 'What is 1+1?', streamConfig);
433
433
  logTurn('T7', conversationHistory);
434
434
  }
@@ -722,7 +722,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
722
722
  }
723
723
 
724
724
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
725
- run = await createRun(2800);
725
+ run = await createRun(1000);
726
726
  await runTurn(
727
727
  { run, conversationHistory },
728
728
  'What is 9 * 9? Calculator.',
@@ -876,7 +876,7 @@ const hasAnthropic = process.env.ANTHROPIC_API_KEY != null;
876
876
 
877
877
  // Turn 6: squeeze harder if needed
878
878
  if (spies.onSummarizeStartSpy.mock.calls.length === 0) {
879
- ({ run, contentParts } = await createRun(2000));
879
+ ({ run, contentParts } = await createRun(1000));
880
880
  await runTurn(
881
881
  { run, conversationHistory },
882
882
  'What is 42 * 42? Use the calculator.',
@@ -163,7 +163,10 @@ function createBashExecutionTool(
163
163
  const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
164
164
 
165
165
  return {
166
- session_id,
166
+ storage_session_id: session_id,
167
+ /* `/files` fallback returns code-output files belonging
168
+ * to the user; tag them user-private. */
169
+ kind: 'user' as const,
167
170
  id,
168
171
  name: file.metadata['original-filename'],
169
172
  };
@@ -241,11 +244,16 @@ function createBashExecutionTool(
241
244
  {
242
245
  session_id: result.session_id,
243
246
  files: result.files,
244
- },
247
+ } satisfies t.CodeExecutionArtifact,
245
248
  ];
246
249
  }
247
250
 
248
- return [formattedOutput.trim(), { session_id: result.session_id }];
251
+ return [
252
+ formattedOutput.trim(),
253
+ {
254
+ session_id: result.session_id,
255
+ } satisfies t.CodeExecutionArtifact,
256
+ ];
249
257
  } catch (error) {
250
258
  throw new Error(
251
259
  `Execution error:\n\n${(error as Error | undefined)?.message}`
@@ -252,12 +252,12 @@ export function createBashProgrammaticToolCallingTool(
252
252
  const params = rawParams as { code: string; timeout?: number };
253
253
  const { code, timeout = DEFAULT_TIMEOUT } = params;
254
254
 
255
- const { toolMap, toolDefs, session_id, _injected_files } =
256
- (config.toolCall ?? {}) as ToolCall &
257
- Partial<t.ProgrammaticCache> & {
258
- session_id?: string;
259
- _injected_files?: t.CodeEnvFile[];
260
- };
255
+ const toolCall = (config.toolCall ?? {}) as ToolCall &
256
+ Partial<t.ProgrammaticCache> & {
257
+ session_id?: string;
258
+ _injected_files?: t.CodeEnvFile[];
259
+ };
260
+ const { toolMap, toolDefs, session_id, _injected_files } = toolCall;
261
261
 
262
262
  if (toolMap == null || toolMap.size === 0) {
263
263
  throw new Error(
@@ -135,8 +135,8 @@ function createCodeExecutionTool(
135
135
  };
136
136
  /**
137
137
  * Extract session context from config.toolCall (injected by ToolNode).
138
- * - session_id: For API to associate with previous session
139
- * - _injected_files: File refs to pass directly (avoids /files endpoint race condition)
138
+ * - session_id: associates with the previous run.
139
+ * - _injected_files: File refs to pass directly (avoids /files endpoint race condition).
140
140
  */
141
141
  const { session_id, _injected_files } = (config.toolCall ?? {}) as {
142
142
  session_id?: string;
@@ -153,7 +153,10 @@ function createCodeExecutionTool(
153
153
  /**
154
154
  * File injection priority:
155
155
  * 1. Use _injected_files from ToolNode (avoids /files endpoint race condition)
156
- * 2. Fall back to fetching from /files endpoint if session_id provided but no injected files
156
+ * 2. Fall back to fetching from /files endpoint if session_id
157
+ * provided but no injected files. The /files lookup still uses the
158
+ * same id value — codeapi stores output files under the exec id as
159
+ * their storage prefix, so the two values coincide here.
157
160
  */
158
161
  if (_injected_files && _injected_files.length > 0) {
159
162
  postData.files = _injected_files;
@@ -186,7 +189,10 @@ function createCodeExecutionTool(
186
189
  const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
187
190
 
188
191
  return {
189
- session_id,
192
+ storage_session_id: session_id,
193
+ /* `/files` fallback returns code-output files belonging
194
+ * to the user; tag them user-private. */
195
+ kind: 'user' as const,
190
196
  id,
191
197
  name: file.metadata['original-filename'],
192
198
  };
@@ -259,11 +265,16 @@ function createCodeExecutionTool(
259
265
  {
260
266
  session_id: result.session_id,
261
267
  files: result.files,
262
- },
268
+ } satisfies t.CodeExecutionArtifact,
263
269
  ];
264
270
  }
265
271
 
266
- return [formattedOutput.trim(), { session_id: result.session_id }];
272
+ return [
273
+ formattedOutput.trim(),
274
+ {
275
+ session_id: result.session_id,
276
+ } satisfies t.CodeExecutionArtifact,
277
+ ];
267
278
  } catch (error) {
268
279
  throw new Error(
269
280
  `Execution error:\n\n${(error as Error | undefined)?.message}`
@@ -304,7 +304,10 @@ export async function fetchSessionFiles(
304
304
  const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
305
305
 
306
306
  return {
307
- session_id: sessionId,
307
+ storage_session_id: sessionId,
308
+ /* `/files` fallback returns code-output files belonging to
309
+ * the user; tag them user-private. */
310
+ kind: 'user' as const,
308
311
  id,
309
312
  name: (file.metadata as Record<string, unknown>)[
310
313
  'original-filename'
@@ -588,7 +591,7 @@ export function formatCompletedResponse(
588
591
  {
589
592
  session_id: response.session_id,
590
593
  files: response.files,
591
- },
594
+ } satisfies t.ProgrammaticExecutionArtifact,
592
595
  ];
593
596
  }
594
597
 
@@ -629,13 +632,13 @@ export function createProgrammaticToolCallingTool(
629
632
  const params = rawParams as { code: string; timeout?: number };
630
633
  const { code, timeout = DEFAULT_TIMEOUT } = params;
631
634
 
632
- // Extra params injected by ToolNode (follows web_search pattern)
633
- const { toolMap, toolDefs, session_id, _injected_files } =
634
- (config.toolCall ?? {}) as ToolCall &
635
- Partial<t.ProgrammaticCache> & {
636
- session_id?: string;
637
- _injected_files?: t.CodeEnvFile[];
638
- };
635
+ // Extra params injected by ToolNode (follows web_search pattern).
636
+ const toolCall = (config.toolCall ?? {}) as ToolCall &
637
+ Partial<t.ProgrammaticCache> & {
638
+ session_id?: string;
639
+ _injected_files?: t.CodeEnvFile[];
640
+ };
641
+ const { toolMap, toolDefs, session_id, _injected_files } = toolCall;
639
642
 
640
643
  if (toolMap == null || toolMap.size === 0) {
641
644
  throw new Error(
@@ -671,7 +674,8 @@ export function createProgrammaticToolCallingTool(
671
674
  /**
672
675
  * File injection priority:
673
676
  * 1. Use _injected_files from ToolNode (avoids /files endpoint race condition)
674
- * 2. Fall back to fetching from /files endpoint if session_id provided but no injected files
677
+ * 2. Fall back to fetching from /files endpoint if session_id
678
+ * provided but no injected files.
675
679
  */
676
680
  let files: t.CodeEnvFile[] | undefined;
677
681
  if (_injected_files && _injected_files.length > 0) {