@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.
Files changed (36) hide show
  1. package/dist/cjs/graphs/Graph.cjs +80 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +2 -0
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/messages/format.cjs +242 -6
  6. package/dist/cjs/messages/format.cjs.map +1 -1
  7. package/dist/cjs/stream.cjs +3 -2
  8. package/dist/cjs/stream.cjs.map +1 -1
  9. package/dist/cjs/tools/handlers.cjs +5 -5
  10. package/dist/cjs/tools/handlers.cjs.map +1 -1
  11. package/dist/esm/graphs/Graph.mjs +80 -1
  12. package/dist/esm/graphs/Graph.mjs.map +1 -1
  13. package/dist/esm/main.mjs +1 -1
  14. package/dist/esm/messages/format.mjs +242 -8
  15. package/dist/esm/messages/format.mjs.map +1 -1
  16. package/dist/esm/stream.mjs +3 -2
  17. package/dist/esm/stream.mjs.map +1 -1
  18. package/dist/esm/tools/handlers.mjs +5 -5
  19. package/dist/esm/tools/handlers.mjs.map +1 -1
  20. package/dist/types/graphs/Graph.d.ts +19 -2
  21. package/dist/types/messages/format.d.ts +25 -1
  22. package/dist/types/tools/handlers.d.ts +2 -1
  23. package/dist/types/types/stream.d.ts +1 -0
  24. package/package.json +9 -8
  25. package/src/graphs/Graph.ts +99 -2
  26. package/src/messages/ensureThinkingBlock.test.ts +393 -0
  27. package/src/messages/format.ts +312 -6
  28. package/src/messages/labelContentByAgent.test.ts +887 -0
  29. package/src/scripts/test-multi-agent-list-handoff.ts +169 -13
  30. package/src/scripts/test-parallel-agent-labeling.ts +325 -0
  31. package/src/scripts/test-thinking-handoff-bedrock.ts +153 -0
  32. package/src/scripts/test-thinking-handoff.ts +147 -0
  33. package/src/specs/thinking-handoff.test.ts +620 -0
  34. package/src/stream.ts +19 -10
  35. package/src/tools/handlers.ts +36 -18
  36. package/src/types/stream.ts +1 -0
@@ -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 (!part.tool_call) {
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 (!_tool_call.name && (output == null || output === '')) {
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
- !part.tool_call ||
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
+ }