diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts
index a49586b8a0..dd31189484 100644
--- a/client/src/common/agents-types.ts
+++ b/client/src/common/agents-types.ts
@@ -1,9 +1,10 @@
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type {
- Agent,
- AgentProvider,
AgentModelParameters,
SupportContact,
+ AgentProvider,
+ GraphEdge,
+ Agent,
} from 'librechat-data-provider';
import type { OptionWithIcon, ExtendedFile } from './types';
@@ -34,6 +35,7 @@ export type AgentForm = {
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
+ edges?: GraphEdge[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
recursion_limit?: number;
support_contact?: SupportContact;
diff --git a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
index a1a1c91ee6..cd88d0792c 100644
--- a/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
+++ b/client/src/components/SidePanel/Agents/Advanced/AdvancedPanel.tsx
@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import { useAgentPanelContext } from '~/Providers';
import MaxAgentSteps from './MaxAgentSteps';
+import AgentHandoffs from './AgentHandoffs';
import { useLocalize } from '~/hooks';
import AgentChain from './AgentChain';
import { Panel } from '~/common';
@@ -41,6 +42,12 @@ export default function AdvancedPanel() {
+
}
+ />
{chainEnabled && (
;
+ currentAgentId: string;
+}
+
+/** TODO: make configurable */
+const MAX_HANDOFFS = 10;
+
+const AgentHandoffs: React.FC = ({ field, currentAgentId }) => {
+ const localize = useLocalize();
+ const [newAgentId, setNewAgentId] = useState('');
+ const [expandedIndices, setExpandedIndices] = useState>(new Set());
+ const agentsMap = useAgentsMapContext();
+ const edgesValue = field.value;
+ const edges = useMemo(() => edgesValue || [], [edgesValue]);
+
+ const agents = useMemo(() => (agentsMap ? Object.values(agentsMap) : []), [agentsMap]);
+
+ const selectableAgents = useMemo(
+ () =>
+ agents
+ .filter((agent) => agent?.id !== currentAgentId)
+ .map(
+ (agent) =>
+ ({
+ label: agent?.name || '',
+ value: agent?.id || '',
+ icon: (
+
+ ),
+ }) as OptionWithIcon,
+ ),
+ [agents, currentAgentId],
+ );
+
+ const getAgentDetails = useCallback((id: string) => agentsMap?.[id], [agentsMap]);
+
+ useEffect(() => {
+ if (newAgentId && edges.length < MAX_HANDOFFS) {
+ const newEdge: GraphEdge = {
+ from: currentAgentId,
+ to: newAgentId,
+ edgeType: 'handoff',
+ };
+ field.onChange([...edges, newEdge]);
+ setNewAgentId('');
+ }
+ }, [newAgentId, edges, field, currentAgentId]);
+
+ const removeHandoffAt = (index: number) => {
+ field.onChange(edges.filter((_, i) => i !== index));
+ // Also remove from expanded set
+ setExpandedIndices((prev) => {
+ const newSet = new Set(prev);
+ newSet.delete(index);
+ return newSet;
+ });
+ };
+
+ const updateHandoffAt = (index: number, agentId: string) => {
+ const updated = [...edges];
+ updated[index] = { ...updated[index], to: agentId };
+ field.onChange(updated);
+ };
+
+ const updateHandoffDetailsAt = (index: number, updates: Partial) => {
+ const updated = [...edges];
+ updated[index] = { ...updated[index], ...updates };
+ field.onChange(updated);
+ };
+
+ const toggleExpanded = (index: number) => {
+ setExpandedIndices((prev) => {
+ const newSet = new Set(prev);
+ if (newSet.has(index)) {
+ newSet.delete(index);
+ } else {
+ newSet.add(index);
+ }
+ return newSet;
+ });
+ };
+
+ const getTargetAgentId = (to: string | string[]): string => {
+ return Array.isArray(to) ? to[0] : to;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {edges.length} / {MAX_HANDOFFS}
+
+
+
+ {/* Current fixed agent */}
+
+
+
+
+
+
+ {getAgentDetails(currentAgentId)?.name}
+
+
+
+ {edges.length > 0 &&
}
+ {edges.map((edge, idx) => {
+ const targetAgentId = getTargetAgentId(edge.to);
+ const isExpanded = expandedIndices.has(idx);
+
+ return (
+
+
+
+ updateHandoffAt(idx, id)}
+ selectPlaceholder={localize('com_ui_agent_var', {
+ 0: localize('com_ui_select'),
+ })}
+ searchPlaceholder={localize('com_ui_agent_var', {
+ 0: localize('com_ui_search'),
+ })}
+ items={selectableAgents}
+ displayValue={getAgentDetails(targetAgentId)?.name ?? ''}
+ SelectIcon={
+
+ }
+ className="flex-1 border-border-heavy"
+ containerClassName="px-0"
+ />
+
+
+
+
+ {isExpanded && (
+
+
+
+
+ updateHandoffDetailsAt(idx, { description: e.target.value })
+ }
+ className="mt-1 h-8 text-sm"
+ />
+
+
+
+
+
+
+ {edge.prompt && (
+
+
+
+ updateHandoffDetailsAt(idx, { promptKey: e.target.value })
+ }
+ className="mt-1 h-8 text-sm"
+ />
+
+ )}
+
+ )}
+
+ {idx < edges.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+ {edges.length < MAX_HANDOFFS && (
+ <>
+ {edges.length > 0 &&
}
+
}
+ />
+ >
+ )}
+
+ {edges.length >= MAX_HANDOFFS && (
+
+ {localize('com_ui_agent_handoff_max', { 0: MAX_HANDOFFS })}
+
+ )}
+
+
+
+
+
{localize('com_ui_agent_handoff_info')}
+
{localize('com_ui_agent_handoff_info_2')}
+
+
+
+
+ );
+};
+
+export default AgentHandoffs;
diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx
index 082d91bd45..637a9e1818 100644
--- a/client/src/components/SidePanel/Agents/AgentPanel.tsx
+++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx
@@ -177,6 +177,7 @@ export default function AgentPanel() {
model_parameters,
provider: _provider,
agent_ids,
+ edges,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
@@ -201,6 +202,7 @@ export default function AgentPanel() {
provider,
model_parameters,
agent_ids,
+ edges,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
@@ -234,6 +236,7 @@ export default function AgentPanel() {
provider,
model_parameters,
agent_ids,
+ edges,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx
index 6379a64ee1..f1701dd081 100644
--- a/client/src/components/SidePanel/Agents/AgentSelect.tsx
+++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx
@@ -103,6 +103,11 @@ export default function AgentSelect({
return;
}
+ if (name === 'edges' && Array.isArray(value)) {
+ formValues[name] = value;
+ return;
+ }
+
if (!keys.has(name)) {
return;
}
diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json
index 5cc1140b47..4b710ce716 100644
--- a/client/src/locales/en/translation.json
+++ b/client/src/locales/en/translation.json
@@ -653,6 +653,17 @@
"com_ui_agent_chain": "Agent Chain (Mixture-of-Agents)",
"com_ui_agent_chain_info": "Enables creating sequences of agents. Each agent can access outputs from previous agents in the chain. Based on the \"Mixture-of-Agents\" architecture where agents use previous outputs as auxiliary information.",
"com_ui_agent_chain_max": "You have reached the maximum of {{0}} agents.",
+ "com_ui_agent_handoffs": "Agent Handoffs",
+ "com_ui_agent_handoff_add": "Add handoff agent",
+ "com_ui_agent_handoff_description": "Handoff description",
+ "com_ui_agent_handoff_description_placeholder": "e.g., Transfer to data analyst for statistical analysis",
+ "com_ui_agent_handoff_info": "Configure agents that this agent can transfer conversations to when specific expertise is needed.",
+ "com_ui_agent_handoff_info_2": "Each handoff creates a transfer tool that enables seamless routing to specialist agents with context.",
+ "com_ui_agent_handoff_max": "Maximum {{0}} handoff agents reached.",
+ "com_ui_agent_handoff_prompt": "Passthrough content",
+ "com_ui_agent_handoff_prompt_placeholder": "Tell this agent what content to generate and pass to the handoff agent. You need to add something here to enable this feature",
+ "com_ui_agent_handoff_prompt_key": "Content parameter name (default: 'instructions')",
+ "com_ui_agent_handoff_prompt_key_placeholder": "Label the content passed (default: 'instructions')",
"com_ui_agent_delete_error": "There was an error deleting the agent",
"com_ui_agent_deleted": "Successfully deleted agent",
"com_ui_agent_duplicate_error": "There was an error duplicating the agent",
diff --git a/packages/api/src/agents/validation.ts b/packages/api/src/agents/validation.ts
index c1ca01d203..499b124940 100644
--- a/packages/api/src/agents/validation.ts
+++ b/packages/api/src/agents/validation.ts
@@ -38,6 +38,17 @@ export const agentSupportContactSchema = z
})
.optional();
+/** Graph edge schema for agent handoffs */
+export const graphEdgeSchema = z.object({
+ from: z.union([z.string(), z.array(z.string())]),
+ to: z.union([z.string(), z.array(z.string())]),
+ description: z.string().optional(),
+ edgeType: z.enum(['handoff', 'direct']).optional(),
+ prompt: z.union([z.string(), z.function()]).optional(),
+ excludeResults: z.boolean().optional(),
+ promptKey: z.string().optional(),
+});
+
/** Base agent schema with all common fields */
export const agentBaseSchema = z.object({
name: z.string().nullable().optional(),
@@ -47,6 +58,7 @@ export const agentBaseSchema = z.object({
model_parameters: z.record(z.unknown()).optional(),
tools: z.array(z.string()).optional(),
agent_ids: z.array(z.string()).optional(),
+ edges: z.array(graphEdgeSchema).optional(),
end_after_tools: z.boolean().optional(),
hide_sequential_outputs: z.boolean().optional(),
artifacts: z.string().optional(),
diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts
index 0e9548b1d0..e1fffe7914 100644
--- a/packages/data-provider/src/schemas.ts
+++ b/packages/data-provider/src/schemas.ts
@@ -176,6 +176,7 @@ export const defaultAgentFormValues = {
tools: [],
provider: {},
projectIds: [],
+ edges: [],
artifacts: '',
/** @deprecated Use ACL permissions instead */
isCollaborative: false,
diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts
index d8cbbbfa94..756ae51e7f 100644
--- a/packages/data-provider/src/types/agents.ts
+++ b/packages/data-provider/src/types/agents.ts
@@ -355,3 +355,45 @@ export type AgentToolType = {
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id?: string });
export type ToolMetadata = TPlugin;
+
+export interface BaseMessage {
+ content: string;
+ role?: string;
+ [key: string]: unknown;
+}
+
+export interface BaseGraphState {
+ [key: string]: unknown;
+}
+
+export type GraphEdge = {
+ /** Agent ID, use a list for multiple sources */
+ from: string | string[];
+ /** Agent ID, use a list for multiple destinations */
+ to: string | string[];
+ description?: string;
+ /** Can return boolean or specific destination(s) */
+ condition?: (state: BaseGraphState) => boolean | string | string[];
+ /** 'handoff' creates tools for dynamic routing, 'direct' creates direct edges, which also allow parallel execution */
+ edgeType?: 'handoff' | 'direct';
+ /**
+ * For direct edges: Optional prompt to add when transitioning through this edge.
+ * String prompts can include variables like {results} which will be replaced with
+ * messages from startIndex onwards. When {results} is used, excludeResults defaults to true.
+ *
+ * For handoff edges: Description for the input parameter that the handoff tool accepts,
+ * allowing the supervisor to pass specific instructions/context to the transferred agent.
+ */
+ prompt?: string | ((messages: BaseMessage[], runStartIndex: number) => string | undefined);
+ /**
+ * When true, excludes messages from startIndex when adding prompt.
+ * Automatically set to true when {results} variable is used in prompt.
+ */
+ excludeResults?: boolean;
+ /**
+ * For handoff edges: Customizes the parameter name for the handoff input.
+ * Defaults to "instructions" if not specified.
+ * Only applies when prompt is provided for handoff edges.
+ */
+ promptKey?: string;
+};
diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts
index df90097282..02e5cddb40 100644
--- a/packages/data-provider/src/types/assistants.ts
+++ b/packages/data-provider/src/types/assistants.ts
@@ -1,7 +1,7 @@
import type { OpenAPIV3 } from 'openapi-types';
import type { AssistantsEndpoint, AgentProvider } from 'src/schemas';
+import type { Agents, GraphEdge } from './agents';
import type { ContentTypes } from './runs';
-import type { Agents } from './agents';
import type { TFile } from './files';
import { ArtifactModes } from 'src/artifacts';
@@ -226,6 +226,7 @@ export type Agent = {
isCollaborative?: boolean;
tool_resources?: AgentToolResources;
agent_ids?: string[];
+ edges?: GraphEdge[];
end_after_tools?: boolean;
hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes;
@@ -251,6 +252,7 @@ export type AgentCreateParams = {
} & Pick<
Agent,
| 'agent_ids'
+ | 'edges'
| 'end_after_tools'
| 'hide_sequential_outputs'
| 'artifacts'
@@ -276,6 +278,7 @@ export type AgentUpdateParams = {
} & Pick<
Agent,
| 'agent_ids'
+ | 'edges'
| 'end_after_tools'
| 'hide_sequential_outputs'
| 'artifacts'
diff --git a/packages/data-schemas/src/schema/agent.ts b/packages/data-schemas/src/schema/agent.ts
index 1fd330eb07..5c4e5c5e5c 100644
--- a/packages/data-schemas/src/schema/agent.ts
+++ b/packages/data-schemas/src/schema/agent.ts
@@ -71,6 +71,10 @@ const agentSchema = new Schema(
agent_ids: {
type: [String],
},
+ edges: {
+ type: [{ type: Schema.Types.Mixed }],
+ default: [],
+ },
isCollaborative: {
type: Boolean,
default: undefined,
diff --git a/packages/data-schemas/src/types/agent.ts b/packages/data-schemas/src/types/agent.ts
index 29d5191567..9084d7c431 100644
--- a/packages/data-schemas/src/types/agent.ts
+++ b/packages/data-schemas/src/types/agent.ts
@@ -1,4 +1,5 @@
import { Document, Types } from 'mongoose';
+import type { GraphEdge } from 'librechat-data-provider';
export interface ISupportContact {
name?: string;
@@ -28,6 +29,7 @@ export interface IAgent extends Omit {
hide_sequential_outputs?: boolean;
end_after_tools?: boolean;
agent_ids?: string[];
+ edges?: GraphEdge[];
/** @deprecated Use ACL permissions instead */
isCollaborative?: boolean;
conversation_starters?: string[];