Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@googleapis/youtube": "^20.0.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.80",
"@librechat/agents": "^3.0.66",
"@librechat/agents": "^3.0.75",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand Down
4 changes: 1 addition & 3 deletions api/server/controllers/agents/__tests__/callbacks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ jest.mock('@librechat/data-schemas', () => ({
}));

jest.mock('@librechat/agents', () => ({
EnvVar: { CODE_API_KEY: 'CODE_API_KEY' },
Providers: { GOOGLE: 'google' },
GraphEvents: {},
...jest.requireActual('@librechat/agents'),
getMessageId: jest.fn(),
ToolEndHandler: jest.fn(),
handleToolCalls: jest.fn(),
Expand Down
11 changes: 6 additions & 5 deletions api/server/controllers/agents/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { nanoid } = require('nanoid');
const { sendEvent, GenerationJobManager } = require('@librechat/api');
const { Constants } = require('@librechat/agents');
const { logger } = require('@librechat/data-schemas');
const { sendEvent, GenerationJobManager } = require('@librechat/api');
const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider');
const {
EnvVar,
Expand Down Expand Up @@ -441,10 +442,10 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null })
return;
}

{
if (output.name !== Tools.execute_code) {
return;
}
const isCodeTool =
output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING;
if (!isCodeTool) {
return;
}

if (!output.artifact.files) {
Expand Down
1 change: 1 addition & 0 deletions api/server/controllers/agents/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ class AgentClient extends BaseClient {

run = await createRun({
agents,
messages,
indexTokenCountMap,
runId: this.responseMessageId,
signal: abortController.signal,
Expand Down
20 changes: 15 additions & 5 deletions api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,23 @@ function createToolLoader(signal, streamId = null) {
* @param {string} params.model
* @param {AgentToolResources} params.tool_resources
* @returns {Promise<{
* tools: StructuredTool[],
* toolContextMap: Record<string, unknown>,
* userMCPAuthMap?: Record<string, Record<string, string>>
* tools: StructuredTool[],
* toolContextMap: Record<string, unknown>,
* userMCPAuthMap?: Record<string, Record<string, string>>,
* toolRegistry?: import('@librechat/agents').LCToolRegistry
* } | undefined>}
*/
return async function loadTools({ req, res, agentId, tools, provider, model, tool_resources }) {
const agent = { id: agentId, tools, provider, model };
return async function loadTools({
req,
res,
tools,
model,
agentId,
provider,
tool_options,
tool_resources,
}) {
const agent = { id: agentId, tools, provider, model, tool_options };
try {
return await loadAgentTools({
req,
Expand Down
1 change: 1 addition & 0 deletions api/server/services/MCP.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ function createToolInstance({
});
toolInstance.mcp = true;
toolInstance.mcpRawServerName = serverName;
toolInstance.mcpJsonSchema = parameters;
return toolInstance;
}

Expand Down
26 changes: 25 additions & 1 deletion api/server/services/ToolService.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
hasCustomUserVars,
getUserMCPAuthMap,
isActionDomainAllowed,
buildToolClassification,
} = require('@librechat/api');
const {
Tools,
Expand Down Expand Up @@ -36,6 +37,7 @@ const { recordUsage } = require('~/server/services/Threads');
const { loadTools } = require('~/app/clients/tools/util');
const { redactMessage } = require('~/config/parsers');
const { findPluginAuthsByKeys } = require('~/models');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
/**
* Processes the required actions by calling the appropriate tools and returning the outputs.
* @param {OpenAIClient} client - OpenAI or StreamRunManager Client.
Expand Down Expand Up @@ -367,7 +369,13 @@ async function processRequiredActions(client, requiredActions) {
* @param {AbortSignal} params.signal
* @param {Pick<Agent, 'id' | 'provider' | 'model' | 'tools'} params.agent - The agent to load tools for.
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
* @returns {Promise<{ tools?: StructuredTool[]; userMCPAuthMap?: Record<string, Record<string, string>> }>} The agent tools.
* @returns {Promise<{
* tools?: StructuredTool[];
* toolContextMap?: Record<string, unknown>;
* userMCPAuthMap?: Record<string, Record<string, string>>;
* toolRegistry?: Map<string, import('~/utils/toolClassification').LCTool>;
* hasDeferredTools?: boolean;
* }>} The agent tools and registry.
*/
async function loadAgentTools({
req,
Expand Down Expand Up @@ -510,11 +518,23 @@ async function loadAgentTools({
return map;
}, {});

/** Build tool registry from MCP tools and create PTC/tool search tools if configured */
const { toolRegistry, additionalTools, hasDeferredTools } = await buildToolClassification({
loadedTools,
userId: req.user.id,
agentId: agent.id,
agentToolOptions: agent.tool_options,
loadAuthValues,
});
agentTools.push(...additionalTools);

if (!checkCapability(AgentCapabilities.actions)) {
return {
tools: agentTools,
userMCPAuthMap,
toolContextMap,
toolRegistry,
hasDeferredTools,
};
}

Expand All @@ -527,6 +547,8 @@ async function loadAgentTools({
tools: agentTools,
userMCPAuthMap,
toolContextMap,
toolRegistry,
hasDeferredTools,
};
}

Expand Down Expand Up @@ -654,6 +676,8 @@ async function loadAgentTools({
tools: agentTools,
toolContextMap,
userMCPAuthMap,
toolRegistry,
hasDeferredTools,
};
}

Expand Down
3 changes: 3 additions & 0 deletions client/src/common/agents-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type {
AgentModelParameters,
AgentToolOptions,
SupportContact,
AgentProvider,
GraphEdge,
Expand Down Expand Up @@ -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[];
Expand Down
6 changes: 5 additions & 1 deletion client/src/components/Chat/Messages/Content/Part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ const Part = memo(

const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
if (
isToolCall &&
(toolCall.name === Tools.execute_code ||
toolCall.name === Constants.PROGRAMMATIC_TOOL_CALLING)
) {
return (
<ExecuteCode
attachments={attachments}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function ExecuteCode({
const [contentHeight, setContentHeight] = useState<number | undefined>(0);

const prevShowCodeRef = useRef<boolean>(showCode);
const { lang, code } = useParseArgs(args) ?? ({} as ParsedArgs);
const { lang = 'py', code } = useParseArgs(args) ?? ({} as ParsedArgs);
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value assignment for lang uses object destructuring with a default, but the condition ?? ({} as ParsedArgs) means that if useParseArgs returns null, the destructuring will fail before the default value can be applied. The correct approach is to use optional chaining and nullish coalescing before destructuring, or handle the null case explicitly.

Suggested change
const { lang = 'py', code } = useParseArgs(args) ?? ({} as ParsedArgs);
const parsedArgs = useParseArgs(args);
const lang = parsedArgs?.lang ?? 'py';
const code = parsedArgs?.code;

Copilot uses AI. Check for mistakes.
const progress = useProgress(initialProgress);

useEffect(() => {
Expand Down
9 changes: 6 additions & 3 deletions client/src/components/SidePanel/Agents/AgentPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -36,8 +37,8 @@ import ModelPanel from './ModelPanel';
function getUpdateToastMessage(
noVersionChange: boolean,
avatarActionState: AgentForm['avatar_action'],
name: string | undefined,
localize: (key: string, vars?: Record<string, unknown> | Array<string | number>) => string,
name: string | null | undefined,
localize: (key: TranslationKeys, vars?: Record<string, unknown>) => string,
): string | null {
// If only avatar upload is pending (separate endpoint), suppress the no-changes toast.
if (noVersionChange && avatarActionState === 'upload') {
Expand Down Expand Up @@ -72,6 +73,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
recursion_limit,
category,
support_contact,
tool_options,
avatar_action: avatarActionState,
} = data;

Expand All @@ -97,6 +99,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
recursion_limit,
category,
support_contact,
tool_options,
...(shouldResetAvatar ? { avatar: null } : {}),
},
provider,
Expand Down Expand Up @@ -545,7 +548,7 @@ export default function AgentPanel() {
<AgentFooter
createMutation={create}
updateMutation={update}
isAvatarUploading={isAvatarUploadInFlight || uploadAvatarMutation.isPending}
isAvatarUploading={isAvatarUploadInFlight || uploadAvatarMutation.isLoading}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from uploadAvatarMutation.isPending to uploadAvatarMutation.isLoading appears to be inconsistent with TanStack Query v4+ API. Modern versions of React Query (TanStack Query) use isPending instead of the deprecated isLoading. This change might break functionality or cause type errors if the project is using a newer version of React Query. Please verify which version is being used and use the appropriate property.

Suggested change
isAvatarUploading={isAvatarUploadInFlight || uploadAvatarMutation.isLoading}
isAvatarUploading={isAvatarUploadInFlight || uploadAvatarMutation.isPending}

Copilot uses AI. Check for mistakes.
activePanel={activePanel}
setActivePanel={setActivePanel}
setCurrentAgentId={setCurrentAgentId}
Expand Down
5 changes: 5 additions & 0 deletions client/src/components/SidePanel/Agents/AgentSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ export default function AgentSelect({
return;
}

if (name === 'tool_options' && typeof value === 'object' && value !== null) {
formValues[name] = value;
return;
}

if (!keys.has(name)) {
return;
}
Expand Down
Loading