@librechat/agents 3.0.17 → 3.0.19
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 +80 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +242 -6
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/stream.cjs +3 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +5 -5
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +80 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/format.mjs +242 -8
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/stream.mjs +3 -2
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +5 -5
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +19 -2
- package/dist/types/messages/format.d.ts +25 -1
- package/dist/types/tools/handlers.d.ts +2 -1
- package/dist/types/types/stream.d.ts +1 -0
- package/package.json +9 -8
- package/src/graphs/Graph.ts +99 -2
- package/src/messages/ensureThinkingBlock.test.ts +393 -0
- package/src/messages/format.ts +312 -6
- package/src/messages/labelContentByAgent.test.ts +887 -0
- package/src/scripts/test-multi-agent-list-handoff.ts +169 -13
- package/src/scripts/test-parallel-agent-labeling.ts +325 -0
- package/src/scripts/test-thinking-handoff-bedrock.ts +153 -0
- package/src/scripts/test-thinking-handoff.ts +147 -0
- package/src/specs/thinking-handoff.test.ts +620 -0
- package/src/stream.ts +19 -10
- package/src/tools/handlers.ts +36 -18
- package/src/types/stream.ts +1 -0
package/src/messages/format.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import {
|
|
3
3
|
AIMessage,
|
|
4
|
+
AIMessageChunk,
|
|
4
5
|
ToolMessage,
|
|
5
6
|
BaseMessage,
|
|
6
7
|
HumanMessage,
|
|
@@ -10,7 +11,10 @@ import {
|
|
|
10
11
|
import type { MessageContentImageUrl } from '@langchain/core/messages';
|
|
11
12
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
12
13
|
import type {
|
|
14
|
+
ExtendedMessageContent,
|
|
13
15
|
MessageContentComplex,
|
|
16
|
+
ReasoningContentText,
|
|
17
|
+
ToolCallContent,
|
|
14
18
|
ToolCallPart,
|
|
15
19
|
TPayload,
|
|
16
20
|
TMessage,
|
|
@@ -306,12 +310,12 @@ function formatAssistantMessage(
|
|
|
306
310
|
}
|
|
307
311
|
// Create a new AIMessage with this text and prepare for tool calls
|
|
308
312
|
lastAIMessage = new AIMessage({
|
|
309
|
-
content: part.text
|
|
313
|
+
content: part.text != null ? part.text : '',
|
|
310
314
|
});
|
|
311
315
|
formattedMessages.push(lastAIMessage);
|
|
312
316
|
} else if (part.type === ContentTypes.TOOL_CALL) {
|
|
313
317
|
// Skip malformed tool call entries without tool_call property
|
|
314
|
-
if (
|
|
318
|
+
if (part.tool_call == null) {
|
|
315
319
|
continue;
|
|
316
320
|
}
|
|
317
321
|
|
|
@@ -323,7 +327,10 @@ function formatAssistantMessage(
|
|
|
323
327
|
} = part.tool_call as ToolCallPart;
|
|
324
328
|
|
|
325
329
|
// Skip invalid tool calls that have no name AND no output
|
|
326
|
-
if (
|
|
330
|
+
if (
|
|
331
|
+
_tool_call.name == null ||
|
|
332
|
+
(_tool_call.name === '' && (output == null || output === ''))
|
|
333
|
+
) {
|
|
327
334
|
continue;
|
|
328
335
|
}
|
|
329
336
|
|
|
@@ -356,7 +363,7 @@ function formatAssistantMessage(
|
|
|
356
363
|
new ToolMessage({
|
|
357
364
|
tool_call_id: tool_call.id ?? '',
|
|
358
365
|
name: tool_call.name,
|
|
359
|
-
content: output
|
|
366
|
+
content: output != null ? output : '',
|
|
360
367
|
})
|
|
361
368
|
);
|
|
362
369
|
} else if (part.type === ContentTypes.THINK) {
|
|
@@ -393,6 +400,226 @@ function formatAssistantMessage(
|
|
|
393
400
|
return formattedMessages;
|
|
394
401
|
}
|
|
395
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Labels all agent content for parallel patterns (fan-out/fan-in)
|
|
405
|
+
* Groups consecutive content by agent and wraps with clear labels
|
|
406
|
+
*/
|
|
407
|
+
function labelAllAgentContent(
|
|
408
|
+
contentParts: MessageContentComplex[],
|
|
409
|
+
agentIdMap: Record<number, string>,
|
|
410
|
+
agentNames?: Record<string, string>
|
|
411
|
+
): MessageContentComplex[] {
|
|
412
|
+
const result: MessageContentComplex[] = [];
|
|
413
|
+
let currentAgentId: string | undefined;
|
|
414
|
+
let agentContentBuffer: MessageContentComplex[] = [];
|
|
415
|
+
|
|
416
|
+
const flushAgentBuffer = (): void => {
|
|
417
|
+
if (agentContentBuffer.length === 0) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (currentAgentId != null && currentAgentId !== '') {
|
|
422
|
+
const agentName = (agentNames?.[currentAgentId] ?? '') || currentAgentId;
|
|
423
|
+
const formattedParts: string[] = [];
|
|
424
|
+
|
|
425
|
+
formattedParts.push(`--- ${agentName} ---`);
|
|
426
|
+
|
|
427
|
+
for (const part of agentContentBuffer) {
|
|
428
|
+
if (part.type === ContentTypes.THINK) {
|
|
429
|
+
const thinkContent = (part as ReasoningContentText).think || '';
|
|
430
|
+
if (thinkContent) {
|
|
431
|
+
formattedParts.push(
|
|
432
|
+
`${agentName}: ${JSON.stringify({
|
|
433
|
+
type: 'think',
|
|
434
|
+
think: thinkContent,
|
|
435
|
+
})}`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
} else if (part.type === ContentTypes.TEXT) {
|
|
439
|
+
const textContent: string = part.text ?? '';
|
|
440
|
+
if (textContent) {
|
|
441
|
+
formattedParts.push(`${agentName}: ${textContent}`);
|
|
442
|
+
}
|
|
443
|
+
} else if (part.type === ContentTypes.TOOL_CALL) {
|
|
444
|
+
formattedParts.push(
|
|
445
|
+
`${agentName}: ${JSON.stringify({
|
|
446
|
+
type: 'tool_call',
|
|
447
|
+
tool_call: (part as ToolCallContent).tool_call,
|
|
448
|
+
})}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
formattedParts.push(`--- End of ${agentName} ---`);
|
|
454
|
+
|
|
455
|
+
// Create a single text content part with all agent content
|
|
456
|
+
result.push({
|
|
457
|
+
type: ContentTypes.TEXT,
|
|
458
|
+
text: formattedParts.join('\n\n'),
|
|
459
|
+
} as MessageContentComplex);
|
|
460
|
+
} else {
|
|
461
|
+
// No agent ID, pass through as-is
|
|
462
|
+
result.push(...agentContentBuffer);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
agentContentBuffer = [];
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < contentParts.length; i++) {
|
|
469
|
+
const part = contentParts[i];
|
|
470
|
+
const agentId = agentIdMap[i];
|
|
471
|
+
|
|
472
|
+
// If agent changed, flush previous buffer
|
|
473
|
+
if (agentId !== currentAgentId && currentAgentId !== undefined) {
|
|
474
|
+
flushAgentBuffer();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
currentAgentId = agentId;
|
|
478
|
+
agentContentBuffer.push(part);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Flush any remaining content
|
|
482
|
+
flushAgentBuffer();
|
|
483
|
+
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Groups content parts by agent and formats them with agent labels
|
|
489
|
+
* This preprocesses multi-agent content to prevent identity confusion
|
|
490
|
+
*
|
|
491
|
+
* @param contentParts - The content parts from a run
|
|
492
|
+
* @param agentIdMap - Map of content part index to agent ID
|
|
493
|
+
* @param agentNames - Optional map of agent ID to display name
|
|
494
|
+
* @param options - Configuration options
|
|
495
|
+
* @param options.labelNonTransferContent - If true, labels all agent transitions (for parallel patterns)
|
|
496
|
+
* @returns Modified content parts with agent labels where appropriate
|
|
497
|
+
*/
|
|
498
|
+
export const labelContentByAgent = (
|
|
499
|
+
contentParts: MessageContentComplex[],
|
|
500
|
+
agentIdMap?: Record<number, string>,
|
|
501
|
+
agentNames?: Record<string, string>,
|
|
502
|
+
options?: { labelNonTransferContent?: boolean }
|
|
503
|
+
): MessageContentComplex[] => {
|
|
504
|
+
if (!agentIdMap || Object.keys(agentIdMap).length === 0) {
|
|
505
|
+
return contentParts;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// If labelNonTransferContent is true, use a different strategy for parallel patterns
|
|
509
|
+
if (options?.labelNonTransferContent === true) {
|
|
510
|
+
return labelAllAgentContent(contentParts, agentIdMap, agentNames);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const result: MessageContentComplex[] = [];
|
|
514
|
+
let currentAgentId: string | undefined;
|
|
515
|
+
let agentContentBuffer: MessageContentComplex[] = [];
|
|
516
|
+
let transferToolCallIndex: number | undefined;
|
|
517
|
+
let transferToolCallId: string | undefined;
|
|
518
|
+
|
|
519
|
+
const flushAgentBuffer = (): void => {
|
|
520
|
+
if (agentContentBuffer.length === 0) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// If this is content from a transferred agent, format it specially
|
|
525
|
+
if (
|
|
526
|
+
currentAgentId != null &&
|
|
527
|
+
currentAgentId !== '' &&
|
|
528
|
+
transferToolCallIndex !== undefined
|
|
529
|
+
) {
|
|
530
|
+
const agentName = (agentNames?.[currentAgentId] ?? '') || currentAgentId;
|
|
531
|
+
const formattedParts: string[] = [];
|
|
532
|
+
|
|
533
|
+
formattedParts.push(`--- Transfer to ${agentName} ---`);
|
|
534
|
+
|
|
535
|
+
for (const part of agentContentBuffer) {
|
|
536
|
+
if (part.type === ContentTypes.THINK) {
|
|
537
|
+
formattedParts.push(
|
|
538
|
+
`${agentName}: ${JSON.stringify({
|
|
539
|
+
type: 'think',
|
|
540
|
+
think: (part as ReasoningContentText).think,
|
|
541
|
+
})}`
|
|
542
|
+
);
|
|
543
|
+
} else if ('text' in part && part.type === ContentTypes.TEXT) {
|
|
544
|
+
const textContent: string = part.text ?? '';
|
|
545
|
+
if (textContent) {
|
|
546
|
+
formattedParts.push(
|
|
547
|
+
`${agentName}: ${JSON.stringify({
|
|
548
|
+
type: 'text',
|
|
549
|
+
text: textContent,
|
|
550
|
+
})}`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
} else if (part.type === ContentTypes.TOOL_CALL) {
|
|
554
|
+
formattedParts.push(
|
|
555
|
+
`${agentName}: ${JSON.stringify({
|
|
556
|
+
type: 'tool_call',
|
|
557
|
+
tool_call: (part as ToolCallContent).tool_call,
|
|
558
|
+
})}`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
formattedParts.push(`--- End of ${agentName} response ---`);
|
|
564
|
+
|
|
565
|
+
// Find the tool call that triggered this transfer and update its output
|
|
566
|
+
if (transferToolCallIndex < result.length) {
|
|
567
|
+
const transferToolCall = result[transferToolCallIndex];
|
|
568
|
+
if (
|
|
569
|
+
transferToolCall.type === ContentTypes.TOOL_CALL &&
|
|
570
|
+
transferToolCall.tool_call?.id === transferToolCallId
|
|
571
|
+
) {
|
|
572
|
+
transferToolCall.tool_call.output = formattedParts.join('\n\n');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
// Not from a transfer, add as-is
|
|
577
|
+
result.push(...agentContentBuffer);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
agentContentBuffer = [];
|
|
581
|
+
transferToolCallIndex = undefined;
|
|
582
|
+
transferToolCallId = undefined;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (let i = 0; i < contentParts.length; i++) {
|
|
586
|
+
const part = contentParts[i];
|
|
587
|
+
const agentId = agentIdMap[i];
|
|
588
|
+
|
|
589
|
+
// Check if this is a transfer tool call
|
|
590
|
+
const isTransferTool =
|
|
591
|
+
(part.type === ContentTypes.TOOL_CALL &&
|
|
592
|
+
(part as ToolCallContent).tool_call?.name?.startsWith(
|
|
593
|
+
'lc_transfer_to_'
|
|
594
|
+
)) ??
|
|
595
|
+
false;
|
|
596
|
+
|
|
597
|
+
// If agent changed, flush previous buffer
|
|
598
|
+
if (agentId !== currentAgentId && currentAgentId !== undefined) {
|
|
599
|
+
flushAgentBuffer();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
currentAgentId = agentId;
|
|
603
|
+
|
|
604
|
+
if (isTransferTool) {
|
|
605
|
+
// Flush any existing buffer first
|
|
606
|
+
flushAgentBuffer();
|
|
607
|
+
// Add the transfer tool call to result
|
|
608
|
+
result.push(part);
|
|
609
|
+
// Mark that the next agent's content should be captured
|
|
610
|
+
transferToolCallIndex = result.length - 1;
|
|
611
|
+
transferToolCallId = (part as ToolCallContent).tool_call?.id;
|
|
612
|
+
currentAgentId = undefined; // Reset to capture the next agent
|
|
613
|
+
} else {
|
|
614
|
+
agentContentBuffer.push(part);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
flushAgentBuffer();
|
|
619
|
+
|
|
620
|
+
return result;
|
|
621
|
+
};
|
|
622
|
+
|
|
396
623
|
/**
|
|
397
624
|
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
|
|
398
625
|
*
|
|
@@ -461,7 +688,7 @@ export const formatAgentMessages = (
|
|
|
461
688
|
}
|
|
462
689
|
// Protect against malformed tool call entries
|
|
463
690
|
if (
|
|
464
|
-
|
|
691
|
+
part.tool_call == null ||
|
|
465
692
|
part.tool_call.name == null ||
|
|
466
693
|
part.tool_call.name === ''
|
|
467
694
|
) {
|
|
@@ -493,7 +720,7 @@ export const formatAgentMessages = (
|
|
|
493
720
|
// Check if this is a continuation of the tool sequence
|
|
494
721
|
let isToolResponse = false;
|
|
495
722
|
const content = payload[j].content;
|
|
496
|
-
if (content && Array.isArray(content)) {
|
|
723
|
+
if (content != null && Array.isArray(content)) {
|
|
497
724
|
for (const part of content) {
|
|
498
725
|
if (part.type === ContentTypes.TOOL_CALL) {
|
|
499
726
|
isToolResponse = true;
|
|
@@ -609,3 +836,82 @@ export function shiftIndexTokenCountMap(
|
|
|
609
836
|
|
|
610
837
|
return shiftedMap;
|
|
611
838
|
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Ensures compatibility when switching from a non-thinking agent to a thinking-enabled agent.
|
|
842
|
+
* Converts AI messages with tool calls (that lack thinking blocks) into buffer strings,
|
|
843
|
+
* avoiding the thinking block signature requirement.
|
|
844
|
+
*
|
|
845
|
+
* @param messages - Array of messages to process
|
|
846
|
+
* @param provider - The provider being used (unused but kept for future compatibility)
|
|
847
|
+
* @returns The messages array with tool sequences converted to buffer strings if necessary
|
|
848
|
+
*/
|
|
849
|
+
export function ensureThinkingBlockInMessages(
|
|
850
|
+
messages: BaseMessage[],
|
|
851
|
+
_provider: Providers
|
|
852
|
+
): BaseMessage[] {
|
|
853
|
+
const result: BaseMessage[] = [];
|
|
854
|
+
let i = 0;
|
|
855
|
+
|
|
856
|
+
while (i < messages.length) {
|
|
857
|
+
const msg = messages[i];
|
|
858
|
+
const isAI = msg instanceof AIMessage || msg instanceof AIMessageChunk;
|
|
859
|
+
|
|
860
|
+
if (!isAI) {
|
|
861
|
+
result.push(msg);
|
|
862
|
+
i++;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const aiMsg = msg as AIMessage | AIMessageChunk;
|
|
867
|
+
const hasToolCalls = aiMsg.tool_calls && aiMsg.tool_calls.length > 0;
|
|
868
|
+
const contentIsArray = Array.isArray(aiMsg.content);
|
|
869
|
+
|
|
870
|
+
// Check if the message has tool calls or tool_use content
|
|
871
|
+
let hasToolUse = hasToolCalls ?? false;
|
|
872
|
+
let firstContentType: string | undefined;
|
|
873
|
+
|
|
874
|
+
if (contentIsArray && aiMsg.content.length > 0) {
|
|
875
|
+
const content = aiMsg.content as ExtendedMessageContent[];
|
|
876
|
+
firstContentType = content[0]?.type;
|
|
877
|
+
hasToolUse =
|
|
878
|
+
hasToolUse ||
|
|
879
|
+
content.some((c) => typeof c === 'object' && c.type === 'tool_use');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// If message has tool use but no thinking block, convert to buffer string
|
|
883
|
+
if (
|
|
884
|
+
hasToolUse &&
|
|
885
|
+
firstContentType !== ContentTypes.THINKING &&
|
|
886
|
+
firstContentType !== 'redacted_thinking'
|
|
887
|
+
) {
|
|
888
|
+
// Collect the AI message and any following tool messages
|
|
889
|
+
const toolSequence: BaseMessage[] = [msg];
|
|
890
|
+
let j = i + 1;
|
|
891
|
+
|
|
892
|
+
// Look ahead for tool messages that belong to this AI message
|
|
893
|
+
while (j < messages.length && messages[j] instanceof ToolMessage) {
|
|
894
|
+
toolSequence.push(messages[j]);
|
|
895
|
+
j++;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Convert the sequence to a buffer string and wrap in a HumanMessage
|
|
899
|
+
// This avoids the thinking block requirement which only applies to AI messages
|
|
900
|
+
const bufferString = getBufferString(toolSequence);
|
|
901
|
+
result.push(
|
|
902
|
+
new HumanMessage({
|
|
903
|
+
content: `[Previous agent context]\n${bufferString}`,
|
|
904
|
+
})
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// Skip the messages we've processed
|
|
908
|
+
i = j;
|
|
909
|
+
} else {
|
|
910
|
+
// Keep the message as is
|
|
911
|
+
result.push(msg);
|
|
912
|
+
i++;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return result;
|
|
917
|
+
}
|