🤖 feat: Agent Handoffs (Routing) (#10176)

* feat: Add support for agent handoffs with edges in agent forms and schemas

chore: Mark `agent_ids` field as deprecated in favor of edges across various schemas and types

chore: Update dependencies for @langchain/core and @librechat/agents to latest versions

chore: Update peer dependency for @librechat/agents to version 3.0.0-rc2 in package.json

chore: Update @librechat/agents dependency to version 3.0.0-rc3 in package.json and package-lock.json

feat: first pass, multi-agent handoffs

fix: update output type to ToolMessage in memory handling functions

fix: improve type checking for graphConfig in createRun function

refactor: remove unused content filtering logic in AgentClient

chore: update @librechat/agents dependency to version 3.0.0-rc4 in package.json and package-lock.json

fix: update @langchain/core peer dependency version to ^0.3.72 in package.json and package-lock.json

fix: update @librechat/agents dependency to version 3.0.0-rc6 in package.json and package-lock.json; refactor stream rate handling in various endpoints

feat: Agent handoff UI

chore: update @librechat/agents dependency to version 3.0.0-rc8 in package.json and package-lock.json

fix: improve hasInfo condition and adjust UI element classes in AgentHandoff component

refactor: remove current fixed agent display from AgentHandoffs component due to redundancy

feat: enhance AgentHandoffs UI with localized beta label and improved layout

chore: update @librechat/agents dependency to version 3.0.0-rc10 in package.json and package-lock.json

feat: add `createSequentialChainEdges` function to add back agent chaining via multi-agents

feat: update `createSequentialChainEdges` call to only provide conversation context between agents

feat: deprecate Agent Chain functionality and update related methods for improved clarity

* chore: update @librechat/agents dependency to version 3.0.0-rc11 in package.json and package-lock.json

* refactor: remove unused addCacheControl function and related imports and import from @librechat/agents

* chore: remove unused i18n keys

* refactor: remove unused format export from index.ts

* chore: update @librechat/agents to v3.0.0-rc13

* chore: remove BEDROCK_LEGACY provider from Providers enum

* chore: update @librechat/agents to version 3.0.2 in package.json
This commit is contained in:
Danny Avila 2025-11-05 17:15:17 -05:00 committed by GitHub
parent 958a6c7872
commit 8a4a5a4790
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1108 additions and 3810 deletions

View file

@ -1,6 +1,10 @@
const { logger } = require('@librechat/data-schemas');
const { createContentAggregator } = require('@librechat/agents');
const { validateAgentModel, getCustomEndpointConfig } = require('@librechat/api');
const {
validateAgentModel,
getCustomEndpointConfig,
createSequentialChainEdges,
} = require('@librechat/api');
const {
Constants,
EModelEndpoint,
@ -119,44 +123,90 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
const agent_ids = primaryConfig.agent_ids;
let userMCPAuthMap = primaryConfig.userMCPAuthMap;
if (agent_ids?.length) {
for (const agentId of agent_ids) {
const agent = await getAgent({ id: agentId });
if (!agent) {
throw new Error(`Agent ${agentId} not found`);
async function processAgent(agentId) {
const agent = await getAgent({ id: agentId });
if (!agent) {
throw new Error(`Agent ${agentId} not found`);
}
const validationResult = await validateAgentModel({
req,
res,
agent,
modelsConfig,
logViolation,
});
if (!validationResult.isValid) {
throw new Error(validationResult.error?.message);
}
const config = await initializeAgent({
req,
res,
agent,
loadTools,
requestFiles,
conversationId,
endpointOption,
allowedProviders,
});
if (userMCPAuthMap != null) {
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
} else {
userMCPAuthMap = config.userMCPAuthMap;
}
agentConfigs.set(agentId, config);
}
let edges = primaryConfig.edges;
const checkAgentInit = (agentId) => agentId === primaryConfig.id || agentConfigs.has(agentId);
if ((edges?.length ?? 0) > 0) {
for (const edge of edges) {
if (Array.isArray(edge.to)) {
for (const to of edge.to) {
if (checkAgentInit(to)) {
continue;
}
await processAgent(to);
}
} else if (typeof edge.to === 'string' && checkAgentInit(edge.to)) {
continue;
} else if (typeof edge.to === 'string') {
await processAgent(edge.to);
}
const validationResult = await validateAgentModel({
req,
res,
agent,
modelsConfig,
logViolation,
});
if (!validationResult.isValid) {
throw new Error(validationResult.error?.message);
if (Array.isArray(edge.from)) {
for (const from of edge.from) {
if (checkAgentInit(from)) {
continue;
}
await processAgent(from);
}
} else if (typeof edge.from === 'string' && checkAgentInit(edge.from)) {
continue;
} else if (typeof edge.from === 'string') {
await processAgent(edge.from);
}
const config = await initializeAgent({
req,
res,
agent,
loadTools,
requestFiles,
conversationId,
endpointOption,
allowedProviders,
});
if (userMCPAuthMap != null) {
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
} else {
userMCPAuthMap = config.userMCPAuthMap;
}
agentConfigs.set(agentId, config);
}
}
/** @deprecated Agent Chain */
if (agent_ids?.length) {
for (const agentId of agent_ids) {
if (checkAgentInit(agentId)) {
continue;
}
await processAgent(agentId);
}
const chain = await createSequentialChainEdges([primaryConfig.id].concat(agent_ids), '{convo}');
edges = edges ? edges.concat(chain) : chain;
}
primaryConfig.edges = edges;
let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint];
if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) {
try {

View file

@ -27,13 +27,13 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
const anthropicConfig = appConfig.endpoints?.[EModelEndpoint.anthropic];
if (anthropicConfig) {
clientOptions.streamRate = anthropicConfig.streamRate;
clientOptions._lc_stream_delay = anthropicConfig.streamRate;
clientOptions.titleModel = anthropicConfig.titleModel;
}
const allConfig = appConfig.endpoints?.all;
if (allConfig) {
clientOptions.streamRate = allConfig.streamRate;
clientOptions._lc_stream_delay = allConfig.streamRate;
}
if (optionsOnly) {

View file

@ -1,8 +1,6 @@
const { HttpsProxyAgent } = require('https-proxy-agent');
const { createHandleLLMNewToken } = require('@librechat/api');
const {
AuthType,
Constants,
EModelEndpoint,
bedrockInputParser,
bedrockOutputParser,
@ -11,7 +9,6 @@ const {
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const getOptions = async ({ req, overrideModel, endpointOption }) => {
const appConfig = req.config;
const {
BEDROCK_AWS_SECRET_ACCESS_KEY,
BEDROCK_AWS_ACCESS_KEY_ID,
@ -47,10 +44,12 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => {
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
}
/** @type {number} */
/*
Callback for stream rate no longer awaits and may end the stream prematurely
/** @type {number}
let streamRate = Constants.DEFAULT_STREAM_RATE;
/** @type {undefined | TBaseEndpoint} */
/** @type {undefined | TBaseEndpoint}
const bedrockConfig = appConfig.endpoints?.[EModelEndpoint.bedrock];
if (bedrockConfig && bedrockConfig.streamRate) {
@ -61,6 +60,7 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => {
if (allConfig && allConfig.streamRate) {
streamRate = allConfig.streamRate;
}
*/
/** @type {BedrockClientOptions} */
const requestOptions = {
@ -88,12 +88,6 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => {
llmConfig.endpointHost = BEDROCK_REVERSE_PROXY;
}
llmConfig.callbacks = [
{
handleLLMNewToken: createHandleLLMNewToken(streamRate),
},
];
return {
/** @type {BedrockClientOptions} */
llmConfig,

View file

@ -3,7 +3,6 @@ const {
isUserProvided,
getOpenAIConfig,
getCustomEndpointConfig,
createHandleLLMNewToken,
} = require('@librechat/api');
const {
CacheKeys,
@ -157,11 +156,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
if (!clientOptions.streamRate) {
return options;
}
options.llmConfig.callbacks = [
{
handleLLMNewToken: createHandleLLMNewToken(clientOptions.streamRate),
},
];
options.llmConfig._lc_stream_delay = clientOptions.streamRate;
return options;
}

View file

@ -4,7 +4,6 @@ jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
resolveHeaders: jest.fn(),
getOpenAIConfig: jest.fn(),
createHandleLLMNewToken: jest.fn(),
getCustomEndpointConfig: jest.fn().mockReturnValue({
apiKey: 'test-key',
baseURL: 'https://test.com',

View file

@ -5,7 +5,6 @@ const {
isUserProvided,
getOpenAIConfig,
getAzureCredentials,
createHandleLLMNewToken,
} = require('@librechat/api');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const OpenAIClient = require('~/app/clients/OpenAIClient');
@ -151,11 +150,7 @@ const initializeClient = async ({
if (!streamRate) {
return options;
}
options.llmConfig.callbacks = [
{
handleLLMNewToken: createHandleLLMNewToken(streamRate),
},
];
options.llmConfig._lc_stream_delay = streamRate;
return options;
}