feat: Agent handoff UI

This commit is contained in:
Danny Avila 2025-09-04 03:21:20 -04:00
parent 6e0e47d5dd
commit e6baecb985
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
8 changed files with 118 additions and 9 deletions

View file

@ -49,7 +49,7 @@
"@langchain/google-vertexai": "^0.2.13",
"@langchain/openai": "^0.5.18",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^3.0.0-rc6",
"@librechat/agents": "^3.0.0-rc7",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",

View file

@ -0,0 +1,91 @@
import React, { useMemo, useState } from 'react';
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import { ChevronDown } from 'lucide-react';
import type { TMessage } from 'librechat-data-provider';
import MessageIcon from '~/components/Share/MessageIcon';
import { useAgentsMapContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface AgentHandoffProps {
name: string;
args: string | Record<string, unknown>;
output?: string | null;
}
const AgentHandoff: React.FC<AgentHandoffProps> = ({ name, args: _args = '' }) => {
const localize = useLocalize();
const agentsMap = useAgentsMapContext();
const [showInfo, setShowInfo] = useState(false);
/** Extracted agent ID from tool name (e.g., "lc_transfer_to_agent_gUV0wMb7zHt3y3Xjz-8_4" -> "agent_gUV0wMb7zHt3y3Xjz-8_4") */
const targetAgentId = useMemo(() => {
if (typeof name !== 'string' || !name.startsWith(Constants.LC_TRANSFER_TO_)) {
return null;
}
return name.replace(Constants.LC_TRANSFER_TO_, '');
}, [name]);
const targetAgent = useMemo(() => {
if (!targetAgentId || !agentsMap) {
return null;
}
return agentsMap[targetAgentId];
}, [agentsMap, targetAgentId]);
const args = useMemo(() => {
if (typeof _args === 'string') {
return _args;
}
try {
return JSON.stringify(_args, null, 2);
} catch {
return '';
}
}, [_args]) as string;
const hasInfo = useMemo(() => (args?.length ?? 0) > 0, [args]);
return (
<div className="my-3">
<div
className={cn(
'flex cursor-pointer items-center gap-2.5 text-sm text-text-secondary',
hasInfo && 'transition-colors hover:text-text-primary',
)}
onClick={() => hasInfo && setShowInfo(!showInfo)}
>
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<MessageIcon
message={
{
endpoint: EModelEndpoint.agents,
isCreatedByUser: false,
} as TMessage
}
agent={targetAgent || undefined}
/>
</div>
<span>{localize('com_ui_transferred_to')}</span>
<span className="select-none font-medium text-text-primary">
{targetAgent?.name || localize('com_ui_agent')}
</span>
{hasInfo && (
<ChevronDown
className={cn('ml-1 h-3 w-3 transition-transform', showInfo && 'rotate-180')}
/>
)}
</div>
{hasInfo && showInfo && (
<div className="ml-8 mt-2 rounded-md bg-surface-secondary p-3 text-xs">
<div className="mb-1 font-medium text-text-secondary">
{localize('com_ui_handoff_instructions')}:
</div>
<pre className="overflow-x-auto whitespace-pre-wrap text-text-primary">{args}</pre>
</div>
)}
</div>
);
};
export default AgentHandoff;

View file

@ -1,5 +1,6 @@
import {
Tools,
Constants,
ContentTypes,
ToolCallTypes,
imageGenTools,
@ -10,6 +11,7 @@ import type { TMessageContentParts, TAttachment } from 'librechat-data-provider'
import { OpenAIImageGen, EmptyText, Reasoning, ExecuteCode, AgentUpdate, Text } from './Parts';
import { ErrorMessage } from './MessageContent';
import RetrievalCall from './RetrievalCall';
import AgentHandoff from './AgentHandoff';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
import WebSearch from './WebSearch';
@ -118,6 +120,14 @@ const Part = memo(
isLast={isLast}
/>
);
} else if (isToolCall && toolCall.name?.startsWith(Constants.LC_TRANSFER_TO_)) {
return (
<AgentHandoff
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
/>
);
} else if (isToolCall) {
return (
<ToolCall

View file

@ -11,8 +11,8 @@ interface AgentUpdateProps {
const AgentUpdate: React.FC<AgentUpdateProps> = ({ currentAgentId }) => {
const localize = useLocalize();
const agentsMap = useAgentsMapContext() || {};
const currentAgent = useMemo(() => agentsMap[currentAgentId], [agentsMap, currentAgentId]);
const agentsMap = useAgentsMapContext();
const currentAgent = useMemo(() => agentsMap?.[currentAgentId], [agentsMap, currentAgentId]);
if (!currentAgentId) {
return null;
}

View file

@ -664,6 +664,10 @@
"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_transferring_to_agent": "Transferring to {{0}}",
"com_ui_transferred_to_agent": "Transferred to {{0}}",
"com_ui_transferred_to": "Transferred to",
"com_ui_handoff_instructions": "Handoff 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",

10
package-lock.json generated
View file

@ -64,7 +64,7 @@
"@langchain/google-vertexai": "^0.2.13",
"@langchain/openai": "^0.5.18",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^3.0.0-rc6",
"@librechat/agents": "^3.0.0-rc7",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
@ -21909,9 +21909,9 @@
}
},
"node_modules/@librechat/agents": {
"version": "3.0.0-rc6",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.0.0-rc6.tgz",
"integrity": "sha512-MAE+HdoRw/XKWIzhoYOUiJrPjN6xicOiLRlDarYAZe4JewLKV2MuBGhRJW9TCn0kwyvGJsMQkTX8xQIXZw7OuA==",
"version": "3.0.0-rc7",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.0.0-rc7.tgz",
"integrity": "sha512-cwXAr6c9knEglcMLuGv4FYQ4U4kJM2jvnyjHxwYRA7JYmaALKi7D5X1NMstWDUrLCOK1UIxGqtEagcUJu6mvCA==",
"license": "MIT",
"dependencies": {
"@langchain/anthropic": "^0.3.26",
@ -51984,7 +51984,7 @@
},
"peerDependencies": {
"@langchain/core": "^0.3.72",
"@librechat/agents": "^3.0.0-rc6",
"@librechat/agents": "^3.0.0-rc7",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.8.2",

View file

@ -74,7 +74,7 @@
},
"peerDependencies": {
"@langchain/core": "^0.3.72",
"@librechat/agents": "^3.0.0-rc6",
"@librechat/agents": "^3.0.0-rc7",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.8.2",

View file

@ -1566,6 +1566,10 @@ export enum Constants {
* This helps inform the UI if the mcp server was previously added.
* */
mcp_server = 'sys__server__sys',
/**
* Handoff Tool Name Prefix
*/
LC_TRANSFER_TO_ = 'lc_transfer_to_',
/** Placeholder Agent ID for Ephemeral Agents */
EPHEMERAL_AGENT_ID = 'ephemeral',
}