@librechat/agents 3.1.78-dev.0 → 3.1.79
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/graphs/Graph.cjs +7 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- 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/local/LocalExecutionEngine.cjs +14 -16
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +30 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -0
- package/dist/esm/graphs/Graph.mjs.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/local/LocalExecutionEngine.mjs +14 -16
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +30 -0
- package/dist/esm/tools/subagent/SubagentExecutor.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/tools/subagent/SubagentExecutor.d.ts +29 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +9 -0
- 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/subagent-configurable-inheritance.ts +252 -0
- package/src/specs/summarization.test.ts +3 -3
- package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +148 -0
- package/src/tools/local/LocalExecutionEngine.ts +55 -54
- package/src/tools/subagent/SubagentExecutor.ts +60 -0
- package/src/types/diff.d.ts +15 -0
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 = [
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
config();
|
|
3
|
+
|
|
4
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
5
|
+
import type * as t from '@/types';
|
|
6
|
+
import { ChatModelStreamHandler } from '@/stream';
|
|
7
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
8
|
+
import { Providers, GraphEvents } from '@/common';
|
|
9
|
+
import { Run } from '@/run';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Live verification that host-set fields on the parent's outer
|
|
13
|
+
* `configurable` (e.g. `requestBody`, `user`, `userMCPAuthMap`)
|
|
14
|
+
* propagate into the subagent's `ON_TOOL_EXECUTE` dispatches.
|
|
15
|
+
*
|
|
16
|
+
* Pass criteria: when the SUBAGENT calls the calculator tool, the
|
|
17
|
+
* `data.configurable` arriving at the parent's ON_TOOL_EXECUTE
|
|
18
|
+
* handler contains every key the parent put on its outer
|
|
19
|
+
* configurable (with `thread_id` overridden to a child run id).
|
|
20
|
+
*/
|
|
21
|
+
const apiKey = process.env.OPENAI_API_KEY!;
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
console.error('Missing OPENAI_API_KEY');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const calculatorDef: t.LCTool = {
|
|
28
|
+
name: 'calculator',
|
|
29
|
+
description: 'Evaluate a math expression. Use for any arithmetic.',
|
|
30
|
+
parameters: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
expression: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: "A JS math expression, e.g. '42 * 58'",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
required: ['expression'],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ConfigurableSnapshot = {
|
|
43
|
+
agentId: string | undefined;
|
|
44
|
+
configurable: Record<string, unknown> | undefined;
|
|
45
|
+
metadata: Record<string, unknown> | undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
console.log('=== Subagent parentConfigurable inheritance — live ===\n');
|
|
50
|
+
|
|
51
|
+
// Parent has NO tools — it can only delegate via the math subagent.
|
|
52
|
+
// The math subagent has the calculator. This forces the spawn-subagent
|
|
53
|
+
// path so we can observe the subagent's `ON_TOOL_EXECUTE` dispatch.
|
|
54
|
+
const mathSubagentInputs: t.AgentInputs = {
|
|
55
|
+
agentId: 'math-worker',
|
|
56
|
+
provider: Providers.OPENAI,
|
|
57
|
+
clientOptions: { modelName: 'gpt-4o', apiKey },
|
|
58
|
+
instructions:
|
|
59
|
+
'You compute arithmetic. Always use the calculator tool — never estimate. Return the final numeric result as plain text.',
|
|
60
|
+
maxContextTokens: 8000,
|
|
61
|
+
toolDefinitions: [calculatorDef],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const parentAgent: t.AgentInputs = {
|
|
65
|
+
agentId: 'supervisor',
|
|
66
|
+
provider: Providers.OPENAI,
|
|
67
|
+
clientOptions: { modelName: 'gpt-4o', apiKey },
|
|
68
|
+
instructions: `You delegate arithmetic to the "math" subagent. You have NO calculator yourself. For any math task, spawn the "math" subagent with the full task as its description, then echo the subagent's text result back to the user.`,
|
|
69
|
+
maxContextTokens: 8000,
|
|
70
|
+
// No toolDefinitions on the parent — only the subagent gets the calculator.
|
|
71
|
+
subagentConfigs: [
|
|
72
|
+
{
|
|
73
|
+
type: 'math',
|
|
74
|
+
name: 'math',
|
|
75
|
+
description:
|
|
76
|
+
'A focused arithmetic worker that uses the calculator tool to compute numerical results.',
|
|
77
|
+
agentInputs: mathSubagentInputs,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const parentSnapshots: ConfigurableSnapshot[] = [];
|
|
83
|
+
const subagentSnapshots: ConfigurableSnapshot[] = [];
|
|
84
|
+
|
|
85
|
+
const customHandlers: Record<string, t.EventHandler> = {
|
|
86
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
87
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
88
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
89
|
+
[GraphEvents.ON_TOOL_EXECUTE]: {
|
|
90
|
+
handle: (_event, rawData): void => {
|
|
91
|
+
const data = rawData as t.ToolExecuteBatchRequest;
|
|
92
|
+
const snapshot: ConfigurableSnapshot = {
|
|
93
|
+
agentId: data.agentId,
|
|
94
|
+
configurable: data.configurable as Record<string, unknown> | undefined,
|
|
95
|
+
metadata: data.metadata as Record<string, unknown> | undefined,
|
|
96
|
+
};
|
|
97
|
+
const callsLabel = data.toolCalls.map((c) => c.name).join(',');
|
|
98
|
+
// Parent and subagent have different agent IDs in this script
|
|
99
|
+
// (parent: 'supervisor', subagent: 'math-worker'). With a self-spawn
|
|
100
|
+
// subagent both would be the same; this script uses a non-self
|
|
101
|
+
// subagent precisely so we can distinguish reliably.
|
|
102
|
+
const isSubagent = data.agentId !== 'supervisor';
|
|
103
|
+
const metadataRunId = (data.metadata as { run_id?: string } | undefined)
|
|
104
|
+
?.run_id;
|
|
105
|
+
if (isSubagent) {
|
|
106
|
+
subagentSnapshots.push(snapshot);
|
|
107
|
+
} else {
|
|
108
|
+
parentSnapshots.push(snapshot);
|
|
109
|
+
}
|
|
110
|
+
console.log(
|
|
111
|
+
`[ON_TOOL_EXECUTE] origin=${isSubagent ? 'SUBAGENT' : 'PARENT'} agentId=${data.agentId} calls=${callsLabel}`
|
|
112
|
+
);
|
|
113
|
+
console.log(
|
|
114
|
+
` metadata keys: ${Object.keys(data.metadata ?? {}).join(',') || '<none>'}`
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
` metadata.run_id="${metadataRunId ?? '<none>'}" configurable.run_id="${(data.configurable as { run_id?: string } | undefined)?.run_id ?? '<none>'}" configurable.thread_id="${(data.configurable as { thread_id?: string } | undefined)?.thread_id ?? '<none>'}"`
|
|
118
|
+
);
|
|
119
|
+
const results: t.ToolExecuteResult[] = data.toolCalls.map((call) => {
|
|
120
|
+
const args = call.args as { expression?: string };
|
|
121
|
+
const expression = args.expression ?? '';
|
|
122
|
+
let content: string;
|
|
123
|
+
try {
|
|
124
|
+
// eslint-disable-next-line no-eval
|
|
125
|
+
const result = eval(expression);
|
|
126
|
+
content = `${expression} = ${result}`;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
content = `Error: ${String(err)}`;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
toolCallId: call.id!,
|
|
132
|
+
status: 'success',
|
|
133
|
+
content,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
data.resolve(results);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const run = await Run.create<t.IState>({
|
|
142
|
+
runId: `sub-cfg-inherit-${Date.now()}`,
|
|
143
|
+
graphConfig: { type: 'standard', agents: [parentAgent] },
|
|
144
|
+
customHandlers,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const question = new HumanMessage(
|
|
148
|
+
'Compute (42 * 58) + (13 ** 3). Use the self subagent, and have it use the calculator.'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Parent's outer configurable carries host-set fields AND explicit
|
|
152
|
+
// run-identity fields so we can verify whether LangGraph respects or
|
|
153
|
+
// overwrites parent's `run_id` / `parent_run_id` when we forward them
|
|
154
|
+
// into the child's `workflow.invoke`.
|
|
155
|
+
const outerConfigurable = {
|
|
156
|
+
thread_id: 'parent-thread-conv-xyz',
|
|
157
|
+
run_id: 'parent-run-id-001',
|
|
158
|
+
parent_run_id: 'grandparent-run-id-000',
|
|
159
|
+
user_id: 'user_abc',
|
|
160
|
+
user: { id: 'user_abc', email: 'a@b.c', role: 'USER' },
|
|
161
|
+
requestBody: {
|
|
162
|
+
messageId: 'msg-response-id-001',
|
|
163
|
+
conversationId: 'parent-thread-conv-xyz',
|
|
164
|
+
parentMessageId: 'user-message-id-000',
|
|
165
|
+
},
|
|
166
|
+
userMCPAuthMap: { 'mcp-github': { token: 'abc' } },
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
console.log('User:', question.content);
|
|
170
|
+
console.log('Parent outer configurable keys:', Object.keys(outerConfigurable));
|
|
171
|
+
console.log();
|
|
172
|
+
|
|
173
|
+
await run.processStream(
|
|
174
|
+
{ messages: [question] },
|
|
175
|
+
{
|
|
176
|
+
configurable: outerConfigurable,
|
|
177
|
+
version: 'v2' as const,
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
console.log('\n=== Verification ===');
|
|
182
|
+
console.log(
|
|
183
|
+
`Parent ON_TOOL_EXECUTE dispatches captured: ${parentSnapshots.length}`
|
|
184
|
+
);
|
|
185
|
+
console.log(
|
|
186
|
+
`Subagent ON_TOOL_EXECUTE dispatches captured: ${subagentSnapshots.length}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (subagentSnapshots.length === 0) {
|
|
190
|
+
console.error(
|
|
191
|
+
'\n❌ FAIL: subagent never invoked a tool — model may not have spawned the subagent.'
|
|
192
|
+
);
|
|
193
|
+
process.exit(2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const expectedHostKeys = ['user_id', 'user', 'requestBody', 'userMCPAuthMap'];
|
|
197
|
+
let allPassed = true;
|
|
198
|
+
subagentSnapshots.forEach((snap, idx) => {
|
|
199
|
+
const cfg = snap.configurable ?? {};
|
|
200
|
+
const meta = snap.metadata ?? {};
|
|
201
|
+
console.log(
|
|
202
|
+
`\nSubagent dispatch #${idx + 1} (agentId=${snap.agentId}, metadata.run_id=${(meta as { run_id?: string }).run_id ?? '-'}):`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Host-set fields must propagate.
|
|
206
|
+
for (const key of expectedHostKeys) {
|
|
207
|
+
const present = key in cfg;
|
|
208
|
+
const value = cfg[key];
|
|
209
|
+
console.log(
|
|
210
|
+
` ${present ? '✅' : '❌'} ${key} = ${JSON.stringify(value)}`
|
|
211
|
+
);
|
|
212
|
+
if (!present) allPassed = false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Run-identity fields: with full inheritance we expect parent's
|
|
216
|
+
// values to flow through. LangGraph runtime MAY overwrite them at
|
|
217
|
+
// child-invoke time — the script logs what actually arrived so we
|
|
218
|
+
// can see empirically what propagates.
|
|
219
|
+
console.log(` ⓘ thread_id observed: "${cfg.thread_id as string}" (parent's: "${outerConfigurable.thread_id}")`);
|
|
220
|
+
console.log(` ⓘ run_id observed: "${cfg.run_id as string}" (parent's: "${outerConfigurable.run_id}")`);
|
|
221
|
+
console.log(` ⓘ parent_run_id observed: "${cfg.parent_run_id as string}" (parent's: "${outerConfigurable.parent_run_id}")`);
|
|
222
|
+
|
|
223
|
+
const threadInherited = cfg.thread_id === outerConfigurable.thread_id;
|
|
224
|
+
const runInherited = cfg.run_id === outerConfigurable.run_id;
|
|
225
|
+
const parentRunInherited =
|
|
226
|
+
cfg.parent_run_id === outerConfigurable.parent_run_id;
|
|
227
|
+
console.log(
|
|
228
|
+
` ${threadInherited ? '✅' : '⚠️ '} thread_id inherited from parent: ${threadInherited}`
|
|
229
|
+
);
|
|
230
|
+
console.log(
|
|
231
|
+
` ${runInherited ? '✅' : '⚠️ '} run_id inherited from parent: ${runInherited}`
|
|
232
|
+
);
|
|
233
|
+
console.log(
|
|
234
|
+
` ${parentRunInherited ? '✅' : '⚠️ '} parent_run_id inherited from parent: ${parentRunInherited}`
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (allPassed) {
|
|
239
|
+
console.log(
|
|
240
|
+
'\n✅ Host-set fields propagate. (Run-identity inheritance is informational — see ⚠️ markers above for any LangGraph-runtime overwrites.)'
|
|
241
|
+
);
|
|
242
|
+
process.exit(0);
|
|
243
|
+
} else {
|
|
244
|
+
console.log('\n❌ FAIL: at least one expected host-set key was missing.');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch((err) => {
|
|
250
|
+
console.error('Script error:', err);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|