🫱🏼🫲🏽 refactor: Improve Agent Handoffs (#11172)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* fix: Tool Resources Dropped between Agent Handoffs

* fix: agent deletion process to remove handoff edges

- Added logic to the `deleteAgent` function to remove references to the deleted agent from other agents' handoff edges.
- Implemented error handling to log any issues encountered during the edge removal process.
- Introduced a new test case to verify that handoff edges are correctly removed when an agent is deleted, ensuring data integrity across agent relationships.

* fix: Improve agent loading process by handling orphaned references

- Added logic to track and log agents that fail to load during initialization, preventing errors from interrupting the process.
- Introduced a Set to store skipped agent IDs and updated edge filtering to exclude these orphaned references, enhancing data integrity in agent relationships.

* chore: Update @librechat/agents to version 3.0.62

* feat: Enhance agent initialization with edge collection and filtering

- Introduced new functions for edge collection and filtering orphaned edges, improving the agent loading process.
- Refactored the `initializeClient` function to utilize breadth-first search (BFS) for discovering connected agents, enabling transitive handoffs.
- Added a new module for edge-related utilities, including deduplication and participant extraction, to streamline edge management.
- Updated the agent configuration handling to ensure proper edge processing and integrity during initialization.

* refactor: primary agent ID selection for multi-agent conversations

- Added a new function `findPrimaryAgentId` to determine the primary agent ID from a set of agent IDs based on suffix rules.
- Updated `createMultiAgentMapper` to filter messages by primary agent for parallel agents and handle handoffs appropriately.
- Enhanced message processing logic to ensure correct inclusion of agent content based on group and agent ID presence.
- Improved documentation to clarify the distinctions between parallel execution and handoff scenarios.

* feat: Implement primary agent ID selection for multi-agent content filtering

* chore: Update @librechat/agents to version 3.0.63 in package.json and package-lock.json

* chore: Update @librechat/agents to version 3.0.64 in package.json and package-lock.json

* chore: Update @librechat/agents to version 3.0.65 in package.json and package-lock.json

* feat: Add optional agent name to run creation for improved identification

* chore: Update @librechat/agents to version 3.0.66 in package.json and package-lock.json

* test: Add unit tests for edge utilities including key generation, participant extraction, and orphaned edge filtering

- Implemented tests for `getEdgeKey`, `getEdgeParticipants`, `filterOrphanedEdges`, and `createEdgeCollector` functions.
- Ensured comprehensive coverage for various edge cases, including handling of arrays and default values.
- Verified correct behavior of edge filtering based on skipped agents and deduplication of edges.
This commit is contained in:
Danny Avila 2026-01-01 16:02:51 -05:00 committed by GitHub
parent d3b5020dd9
commit 791dab8f20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 521 additions and 71 deletions

View file

@ -98,12 +98,42 @@ function logToolError(graph, error, toolId) {
/** Regex pattern to match agent ID suffix (____N) */
const AGENT_SUFFIX_PATTERN = /____(\d+)$/;
/**
* Finds the primary agent ID within a set of agent IDs.
* Primary = no suffix (____N) or lowest suffix number.
* @param {Set<string>} agentIds
* @returns {string | null}
*/
function findPrimaryAgentId(agentIds) {
let primaryAgentId = null;
let lowestSuffixIndex = Infinity;
for (const agentId of agentIds) {
const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN);
if (!suffixMatch) {
return agentId;
}
const suffixIndex = parseInt(suffixMatch[1], 10);
if (suffixIndex < lowestSuffixIndex) {
lowestSuffixIndex = suffixIndex;
primaryAgentId = agentId;
}
}
return primaryAgentId;
}
/**
* Creates a mapMethod for getMessagesForConversation that processes agent content.
* - Strips agentId/groupId metadata from all content
* - For multi-agent: filters to primary agent content only (no suffix or lowest suffix)
* - For parallel agents (addedConvo with groupId): filters each group to its primary agent
* - For handoffs (agentId without groupId): keeps all content from all agents
* - For multi-agent: applies agent labels to content
*
* The key distinction:
* - Parallel execution (addedConvo): Parts have both agentId AND groupId
* - Handoffs: Parts only have agentId, no groupId
*
* @param {Agent} primaryAgent - Primary agent configuration
* @param {Map<string, Agent>} [agentConfigs] - Additional agent configurations
* @returns {(message: TMessage) => TMessage} Map method for processing messages
@ -127,40 +157,38 @@ function createMultiAgentMapper(primaryAgent, agentConfigs) {
return message;
}
// Find primary agent ID (no suffix, or lowest suffix number) - only needed for multi-agent
let primaryAgentId = null;
let hasAgentMetadata = false;
if (hasMultipleAgents) {
let lowestSuffixIndex = Infinity;
for (const part of message.content) {
const agentId = part?.agentId;
if (!agentId) {
continue;
}
hasAgentMetadata = true;
const suffixMatch = agentId.match(AGENT_SUFFIX_PATTERN);
if (!suffixMatch) {
primaryAgentId = agentId;
break;
}
const suffixIndex = parseInt(suffixMatch[1], 10);
if (suffixIndex < lowestSuffixIndex) {
lowestSuffixIndex = suffixIndex;
primaryAgentId = agentId;
}
}
} else {
// Single agent: just check if any metadata exists
hasAgentMetadata = message.content.some((part) => part?.agentId || part?.groupId);
}
// Check for metadata
const hasAgentMetadata = message.content.some((part) => part?.agentId || part?.groupId != null);
if (!hasAgentMetadata) {
return message;
}
try {
// Build a map of groupId -> Set of agentIds, to find primary per group
/** @type {Map<number, Set<string>>} */
const groupAgentMap = new Map();
for (const part of message.content) {
const groupId = part?.groupId;
const agentId = part?.agentId;
if (groupId != null && agentId) {
if (!groupAgentMap.has(groupId)) {
groupAgentMap.set(groupId, new Set());
}
groupAgentMap.get(groupId).add(agentId);
}
}
// For each group, find the primary agent
/** @type {Map<number, string>} */
const groupPrimaryMap = new Map();
for (const [groupId, agentIds] of groupAgentMap) {
const primary = findPrimaryAgentId(agentIds);
if (primary) {
groupPrimaryMap.set(groupId, primary);
}
}
/** @type {Array<TMessageContentParts>} */
const filteredContent = [];
/** @type {Record<number, string>} */
@ -168,8 +196,16 @@ function createMultiAgentMapper(primaryAgent, agentConfigs) {
for (const part of message.content) {
const agentId = part?.agentId;
// For single agent: include all parts; for multi-agent: filter to primary
if (!hasMultipleAgents || !agentId || agentId === primaryAgentId) {
const groupId = part?.groupId;
// Filtering logic:
// - No groupId (handoffs): always include
// - Has groupId (parallel): only include if it's the primary for that group
const isParallelPart = groupId != null;
const groupPrimary = isParallelPart ? groupPrimaryMap.get(groupId) : null;
const shouldInclude = !isParallelPart || !agentId || agentId === groupPrimary;
if (shouldInclude) {
const newIndex = filteredContent.length;
const { agentId: _a, groupId: _g, ...cleanPart } = part;
filteredContent.push(cleanPart);
@ -325,11 +361,14 @@ class AgentClient extends BaseClient {
{ instructions = null, additional_instructions = null },
opts,
) {
const hasAddedConvo = this.options.req?.body?.addedConvo != null;
const orderedMessages = this.constructor.getMessagesForConversation({
messages,
parentMessageId,
summary: this.shouldSummarize,
mapMethod: createMultiAgentMapper(this.options.agent, this.agentConfigs),
mapMethod: hasAddedConvo
? createMultiAgentMapper(this.options.agent, this.agentConfigs)
: undefined,
});
let payload;