@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.
- package/dist/cjs/llm/anthropic/index.cjs +44 -55
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +97 -14
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +10 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +16 -5
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -4
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +63 -40
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +43 -54
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +48 -15
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +97 -14
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +10 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +16 -5
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -4
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +63 -40
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +1 -9
- package/dist/types/messages/anthropicToolCache.d.ts +5 -5
- package/dist/types/types/tools.d.ts +82 -17
- package/package.json +1 -1
- package/src/llm/anthropic/index.ts +55 -64
- package/src/llm/anthropic/llm.spec.ts +585 -0
- package/src/llm/anthropic/utils/message_inputs.ts +36 -21
- package/src/llm/anthropic/utils/message_outputs.ts +0 -4
- package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
- package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
- package/src/messages/anthropicToolCache.ts +70 -25
- package/src/messages/format.ts +117 -18
- package/src/messages/formatAgentMessages.test.ts +202 -1
- package/src/scripts/code_exec_multi_session.ts +4 -4
- package/src/specs/summarization.test.ts +3 -3
- package/src/tools/BashExecutor.ts +11 -3
- package/src/tools/BashProgrammaticToolCalling.ts +6 -6
- package/src/tools/CodeExecutor.ts +17 -6
- package/src/tools/ProgrammaticToolCalling.ts +14 -10
- package/src/tools/ToolNode.ts +85 -48
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +9 -2
- package/src/tools/__tests__/ToolNode.session.test.ts +131 -50
- package/src/tools/local/LocalExecutionEngine.ts +55 -54
- package/src/tools/local/LocalExecutionTools.ts +2 -2
- package/src/tools/local/LocalProgrammaticToolCalling.ts +23 -6
- package/src/types/diff.d.ts +15 -0
- package/src/types/tools.ts +79 -17
package/src/messages/format.ts
CHANGED
|
@@ -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
|
-
|
|
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}${
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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 {
|
|
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} (
|
|
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.
|
|
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
|
|
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} (
|
|
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(
|
|
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(
|
|
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(
|
|
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 [
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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:
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
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) {
|