From 4682f0e370bc02806911d0e63f341ba52dab2287 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 7 Jan 2026 20:16:15 -0500 Subject: [PATCH] feat: add per-tool configuration options for agents, including deferred loading and allowed callers - Introduced `tool_options` in agent forms to manage tool behavior. - Updated tool classification logic to prioritize agent-level configurations. - Enhanced UI components to support tool deferral functionality. - Added localization strings for new tool options and actions. --- api/server/services/ToolService.js | 1 + client/src/common/agents-types.ts | 3 + .../SidePanel/Agents/AgentPanel.tsx | 9 +- .../SidePanel/Agents/AgentSelect.tsx | 5 + .../components/SidePanel/Agents/MCPTool.tsx | 290 +++++++++++++++--- client/src/locales/en/translation.json | 10 + packages/api/src/tools/classification.ts | 86 +++++- packages/data-provider/src/schemas.ts | 1 + .../data-provider/src/types/assistants.ts | 36 +++ 9 files changed, 388 insertions(+), 53 deletions(-) diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 44ee3a9f6e..c6f19795cb 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -522,6 +522,7 @@ async function loadAgentTools({ loadedTools, userId: req.user.id, agentId: agent.id, + agentToolOptions: agent.tool_options, loadAuthValues, }); agentTools.push(...additionalTools); diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 9ac6b440a3..c3832b7ff8 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,6 +1,7 @@ import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; import type { AgentModelParameters, + AgentToolOptions, SupportContact, AgentProvider, GraphEdge, @@ -33,6 +34,8 @@ export type AgentForm = { model: string | null; model_parameters: AgentModelParameters; tools?: string[]; + /** Per-tool configuration options (deferred loading, allowed callers, etc.) */ + tool_options?: AgentToolOptions; provider?: AgentProvider | OptionWithIcon; /** @deprecated Use edges instead */ agent_ids?: string[]; diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx index 2cf6af3f7d..917e2c04bf 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx @@ -23,6 +23,7 @@ import { import { createProviderOption, getDefaultAgentFormValues } from '~/utils'; import { useResourcePermissions } from '~/hooks/useResourcePermissions'; import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks'; +import type { TranslationKeys } from '~/hooks/useLocalize'; import { useAgentPanelContext } from '~/Providers/AgentPanelContext'; import AgentPanelSkeleton from './AgentPanelSkeleton'; import AdvancedPanel from './Advanced/AdvancedPanel'; @@ -36,8 +37,8 @@ import ModelPanel from './ModelPanel'; function getUpdateToastMessage( noVersionChange: boolean, avatarActionState: AgentForm['avatar_action'], - name: string | undefined, - localize: (key: string, vars?: Record | Array) => string, + name: string | null | undefined, + localize: (key: TranslationKeys, vars?: Record) => string, ): string | null { // If only avatar upload is pending (separate endpoint), suppress the no-changes toast. if (noVersionChange && avatarActionState === 'upload') { @@ -72,6 +73,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n recursion_limit, category, support_contact, + tool_options, avatar_action: avatarActionState, } = data; @@ -97,6 +99,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n recursion_limit, category, support_contact, + tool_options, ...(shouldResetAvatar ? { avatar: null } : {}), }, provider, @@ -545,7 +548,7 @@ export default function AgentPanel() { (); + const { getValues, setValue, control } = useFormContext(); const { getServerStatusIconProps, getConfigDialogProps } = useMCPServerManager(); const [isFocused, setIsFocused] = useState(false); const [isHovering, setIsHovering] = useState(false); const [accordionValue, setAccordionValue] = useState(''); + /** Local state for optimistic updates */ + const [localDeferredTools, setLocalDeferredTools] = useState>(new Set()); + + /** Watch form tool_options for sync */ + const formToolOptions = useWatch({ control, name: 'tool_options' }); + + /** Sync local state with form state */ + useEffect(() => { + const newDeferred = new Set(); + if (formToolOptions) { + for (const [toolId, options] of Object.entries(formToolOptions)) { + if (options?.defer_loading) { + newDeferred.add(toolId); + } + } + } + setLocalDeferredTools(newDeferred); + }, [formToolOptions]); + + /** Check if a specific tool has defer_loading enabled (uses local state for optimistic UI) */ + const isToolDeferred = useCallback( + (toolId: string): boolean => localDeferredTools.has(toolId), + [localDeferredTools], + ); + + /** Toggle defer_loading for a specific tool with optimistic update */ + const toggleToolDefer = useCallback( + (toolId: string) => { + const newDeferred = !localDeferredTools.has(toolId); + + /** Optimistic update */ + setLocalDeferredTools((prev) => { + const next = new Set(prev); + if (newDeferred) { + next.add(toolId); + } else { + next.delete(toolId); + } + return next; + }); + + /** Update form state */ + const currentOptions = getValues('tool_options') || {}; + const currentToolOptions = currentOptions[toolId] || {}; + + const updatedOptions: AgentToolOptions = { + ...currentOptions, + [toolId]: { + ...currentToolOptions, + defer_loading: newDeferred, + }, + }; + + /** Clean up if no options remain for this tool */ + if (!newDeferred && Object.keys(updatedOptions[toolId]).length === 1) { + delete updatedOptions[toolId]; + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [localDeferredTools, getValues, setValue], + ); + + /** Check if all server tools are deferred (uses local state) */ + const areAllToolsDeferred = + serverInfo?.tools && + serverInfo.tools.length > 0 && + serverInfo.tools.every((tool) => localDeferredTools.has(tool.tool_id)); + + /** Toggle defer_loading for all tools from this server with optimistic update */ + const toggleDeferAll = useCallback(() => { + if (!serverInfo?.tools) return; + + const shouldDefer = !areAllToolsDeferred; + + /** Optimistic update */ + setLocalDeferredTools((prev) => { + const next = new Set(prev); + for (const tool of serverInfo.tools!) { + if (shouldDefer) { + next.add(tool.tool_id); + } else { + next.delete(tool.tool_id); + } + } + return next; + }); + + /** Update form state */ + const currentOptions = getValues('tool_options') || {}; + const updatedOptions: AgentToolOptions = { ...currentOptions }; + + for (const tool of serverInfo.tools) { + if (shouldDefer) { + updatedOptions[tool.tool_id] = { + ...(updatedOptions[tool.tool_id] || {}), + defer_loading: true, + }; + } else { + if (updatedOptions[tool.tool_id]) { + delete updatedOptions[tool.tool_id].defer_loading; + /** Clean up empty tool options */ + if (Object.keys(updatedOptions[tool.tool_id]).length === 0) { + delete updatedOptions[tool.tool_id]; + } + } + } + } + + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, [serverInfo?.tools, getValues, setValue, areAllToolsDeferred]); + if (!serverInfo) { return null; } @@ -170,6 +284,47 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo }) /> + {/* Defer All toggle - icon only with tooltip */} + { + e.stopPropagation(); + toggleDeferAll(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + toggleDeferAll(); + } + }} + > + + +
{/* Caret button for accordion */} @@ -230,52 +385,95 @@ export default function MCPTool({ serverInfo }: { serverInfo?: MCPServerInfo })
- {serverInfo.tools?.map((subTool) => ( -