@librechat/agents 3.0.18 → 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.
@@ -13,6 +13,8 @@ import type { ToolCall } from '@langchain/core/messages/tool';
13
13
  import type {
14
14
  ExtendedMessageContent,
15
15
  MessageContentComplex,
16
+ ReasoningContentText,
17
+ ToolCallContent,
16
18
  ToolCallPart,
17
19
  TPayload,
18
20
  TMessage,
@@ -308,12 +310,12 @@ function formatAssistantMessage(
308
310
  }
309
311
  // Create a new AIMessage with this text and prepare for tool calls
310
312
  lastAIMessage = new AIMessage({
311
- content: part.text || '',
313
+ content: part.text != null ? part.text : '',
312
314
  });
313
315
  formattedMessages.push(lastAIMessage);
314
316
  } else if (part.type === ContentTypes.TOOL_CALL) {
315
317
  // Skip malformed tool call entries without tool_call property
316
- if (!part.tool_call) {
318
+ if (part.tool_call == null) {
317
319
  continue;
318
320
  }
319
321
 
@@ -325,7 +327,10 @@ function formatAssistantMessage(
325
327
  } = part.tool_call as ToolCallPart;
326
328
 
327
329
  // Skip invalid tool calls that have no name AND no output
328
- if (!_tool_call.name && (output == null || output === '')) {
330
+ if (
331
+ _tool_call.name == null ||
332
+ (_tool_call.name === '' && (output == null || output === ''))
333
+ ) {
329
334
  continue;
330
335
  }
331
336
 
@@ -358,7 +363,7 @@ function formatAssistantMessage(
358
363
  new ToolMessage({
359
364
  tool_call_id: tool_call.id ?? '',
360
365
  name: tool_call.name,
361
- content: output || '',
366
+ content: output != null ? output : '',
362
367
  })
363
368
  );
364
369
  } else if (part.type === ContentTypes.THINK) {
@@ -395,6 +400,226 @@ function formatAssistantMessage(
395
400
  return formattedMessages;
396
401
  }
397
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
+
398
623
  /**
399
624
  * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
400
625
  *
@@ -463,7 +688,7 @@ export const formatAgentMessages = (
463
688
  }
464
689
  // Protect against malformed tool call entries
465
690
  if (
466
- !part.tool_call ||
691
+ part.tool_call == null ||
467
692
  part.tool_call.name == null ||
468
693
  part.tool_call.name === ''
469
694
  ) {
@@ -495,7 +720,7 @@ export const formatAgentMessages = (
495
720
  // Check if this is a continuation of the tool sequence
496
721
  let isToolResponse = false;
497
722
  const content = payload[j].content;
498
- if (content && Array.isArray(content)) {
723
+ if (content != null && Array.isArray(content)) {
499
724
  for (const part of content) {
500
725
  if (part.type === ContentTypes.TOOL_CALL) {
501
726
  isToolResponse = true;