@librechat/agents 3.0.62 → 3.0.63

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.
@@ -420,6 +420,9 @@ export class MultiAgentGraph extends StandardGraph {
420
420
  * Returns filtered messages with the transfer tool call/message removed, plus any instructions
421
421
  * extracted from the transfer to be injected as a HumanMessage preamble.
422
422
  *
423
+ * Supports both single handoffs (last message is the transfer) and parallel handoffs
424
+ * (multiple transfer ToolMessages, need to find the one targeting this agent).
425
+ *
423
426
  * @param messages - Current state messages
424
427
  * @param agentId - The agent ID to check for handoff reception
425
428
  * @returns Object with filtered messages and extracted instructions, or null if not a handoff
@@ -430,35 +433,49 @@ export class MultiAgentGraph extends StandardGraph {
430
433
  ): { filteredMessages: BaseMessage[]; instructions: string | null } | null {
431
434
  if (messages.length === 0) return null;
432
435
 
433
- const lastMessage = messages[messages.length - 1];
436
+ /**
437
+ * Search for a transfer ToolMessage targeting this agent.
438
+ * For parallel handoffs, multiple transfer messages may exist - find ours.
439
+ * Search backwards from the end to find the most recent transfer to this agent.
440
+ */
441
+ let toolMessage: ToolMessage | null = null;
442
+ let toolMessageIndex = -1;
443
+
444
+ for (let i = messages.length - 1; i >= 0; i--) {
445
+ const msg = messages[i];
446
+ if (msg.getType() !== 'tool') continue;
434
447
 
435
- /** Check if last message is a transfer ToolMessage targeting this agent */
436
- if (lastMessage.getType() !== 'tool') return null;
448
+ const candidateMsg = msg as ToolMessage;
449
+ const toolName = candidateMsg.name;
437
450
 
438
- const toolMessage = lastMessage as ToolMessage;
439
- const toolName = toolMessage.name;
451
+ if (typeof toolName !== 'string') continue;
440
452
 
441
- if (typeof toolName !== 'string') return null;
453
+ /** Check for standard transfer pattern */
454
+ const isTransferMessage = toolName.startsWith(Constants.LC_TRANSFER_TO_);
455
+ const isConditionalTransfer = toolName === 'conditional_transfer';
442
456
 
443
- /** Check for standard transfer pattern */
444
- const isTransferMessage = toolName.startsWith(Constants.LC_TRANSFER_TO_);
445
- const isConditionalTransfer = toolName === 'conditional_transfer';
457
+ if (!isTransferMessage && !isConditionalTransfer) continue;
446
458
 
447
- if (!isTransferMessage && !isConditionalTransfer) return null;
459
+ /** Extract destination from tool name or additional_kwargs */
460
+ let destinationAgent: string | null = null;
448
461
 
449
- /** Extract destination from tool name or additional_kwargs */
450
- let destinationAgent: string | null = null;
462
+ if (isTransferMessage) {
463
+ destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
464
+ } else if (isConditionalTransfer) {
465
+ const handoffDest = candidateMsg.additional_kwargs.handoff_destination;
466
+ destinationAgent = typeof handoffDest === 'string' ? handoffDest : null;
467
+ }
451
468
 
452
- if (isTransferMessage) {
453
- destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
454
- } else if (isConditionalTransfer) {
455
- /** For conditional transfers, read destination from additional_kwargs */
456
- const handoffDest = toolMessage.additional_kwargs.handoff_destination;
457
- destinationAgent = typeof handoffDest === 'string' ? handoffDest : null;
469
+ /** Check if this transfer targets our agent */
470
+ if (destinationAgent === agentId) {
471
+ toolMessage = candidateMsg;
472
+ toolMessageIndex = i;
473
+ break;
474
+ }
458
475
  }
459
476
 
460
- /** Verify this agent is the intended destination */
461
- if (destinationAgent !== agentId) return null;
477
+ /** No transfer targeting this agent found */
478
+ if (toolMessage === null || toolMessageIndex < 0) return null;
462
479
 
463
480
  /** Extract instructions from the ToolMessage content */
464
481
  const contentStr =
@@ -472,35 +489,59 @@ export class MultiAgentGraph extends StandardGraph {
472
489
  /** Get the tool_call_id to find and filter the AI message's tool call */
473
490
  const toolCallId = toolMessage.tool_call_id;
474
491
 
475
- /** Filter out the transfer messages */
492
+ /**
493
+ * Collect all transfer tool_call_ids to filter out.
494
+ * For parallel handoffs, we filter ALL transfer messages (not just ours)
495
+ * to give the receiving agent a clean context without handoff noise.
496
+ */
497
+ const transferToolCallIds = new Set<string>([toolCallId]);
498
+ for (const msg of messages) {
499
+ if (msg.getType() !== 'tool') continue;
500
+ const tm = msg as ToolMessage;
501
+ const tName = tm.name;
502
+ if (typeof tName !== 'string') continue;
503
+ if (
504
+ tName.startsWith(Constants.LC_TRANSFER_TO_) ||
505
+ tName === 'conditional_transfer'
506
+ ) {
507
+ transferToolCallIds.add(tm.tool_call_id);
508
+ }
509
+ }
510
+
511
+ /** Filter out all transfer messages */
476
512
  const filteredMessages: BaseMessage[] = [];
477
513
 
478
- for (let i = 0; i < messages.length - 1; i++) {
479
- /** Exclude the last message (ToolMessage) by iterating to length - 1 */
514
+ for (let i = 0; i < messages.length; i++) {
480
515
  const msg = messages[i];
481
516
  const msgType = msg.getType();
482
517
 
518
+ /** Skip transfer ToolMessages */
519
+ if (msgType === 'tool') {
520
+ const tm = msg as ToolMessage;
521
+ if (transferToolCallIds.has(tm.tool_call_id)) {
522
+ continue;
523
+ }
524
+ }
525
+
483
526
  if (msgType === 'ai') {
484
- /** Check if this AI message contains the transfer tool call */
527
+ /** Check if this AI message contains any transfer tool calls */
485
528
  const aiMsg = msg as AIMessage | AIMessageChunk;
486
529
  const toolCalls = aiMsg.tool_calls;
487
530
 
488
531
  if (toolCalls && toolCalls.length > 0) {
489
- const transferCallIndex = toolCalls.findIndex(
490
- (tc) => tc.id === toolCallId
532
+ /** Filter out all transfer tool calls */
533
+ const remainingToolCalls = toolCalls.filter(
534
+ (tc) => tc.id == null || !transferToolCallIds.has(tc.id)
491
535
  );
492
536
 
493
- if (transferCallIndex >= 0) {
494
- /** This AI message has the transfer tool call - filter it out */
495
- const remainingToolCalls = toolCalls.filter(
496
- (tc) => tc.id !== toolCallId
497
- );
537
+ const hasTransferCalls = remainingToolCalls.length < toolCalls.length;
498
538
 
539
+ if (hasTransferCalls) {
499
540
  if (
500
541
  remainingToolCalls.length > 0 ||
501
542
  (typeof aiMsg.content === 'string' && aiMsg.content.trim())
502
543
  ) {
503
- /** Keep the message but without the transfer tool call */
544
+ /** Keep the message but without transfer tool calls */
504
545
  const filteredAiMsg = new AIMessage({
505
546
  content: aiMsg.content,
506
547
  tool_calls: remainingToolCalls,
@@ -288,6 +288,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
288
288
  Array.isArray(output.goto) &&
289
289
  output.goto.every((send): send is Send => isSend(send))
290
290
  ) {
291
+ /** Aggregate Send-based commands */
291
292
  if (parentCommand) {
292
293
  (parentCommand.goto as Send[]).push(...(output.goto as Send[]));
293
294
  } else {
@@ -297,6 +298,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
297
298
  });
298
299
  }
299
300
  } else {
301
+ /**
302
+ * Non-Send Commands (including handoff Commands with string goto)
303
+ * are passed through as-is. For single handoffs, this works correctly.
304
+ *
305
+ * Note: Parallel handoffs (LLM calling multiple transfer tools simultaneously)
306
+ * are not yet fully supported. For parallel agent execution, use direct edges
307
+ * with edgeType: 'direct' instead of handoff edges.
308
+ */
300
309
  combinedOutputs.push(output);
301
310
  }
302
311
  } else {