Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 13 additions & 23 deletions api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const {
buildWebSearchDynamicContext,
codeExecutionAuthHeaders,
getCodeFileLocation,
resolveCodeExecutionContext,
resolveCodeExecutionWorkspaceSelections,
withRequestCodeInputs,
resolveAgentCodeExecution,
resolveMCPClientCapabilityProfile,
} = require('@librechat/api');
const {
Expand Down Expand Up @@ -386,30 +386,20 @@ const loadTools = async ({

if (tool === Tools.execute_code) {
requestedTools[tool] = async () => {
const statefulSessions =
agent?.stateful_code_sessions === true &&
(await checkCapability(options.req, AgentCapabilities.stateful_code_sessions));
const codeExecutionContext =
options.codeExecutionContext ??
resolveCodeExecutionContext({
statefulSessions,
environment: agent?.stateful_code_environment,
environmentId: agent?.code_environment_id,
environmentIds: agent?.code_environment_ids,
allowEnvironmentSelection:
options.req?.config?.endpoints?.agents?.statefulCodeSessions
?.allowEnvironmentSelection,
workspaceSelections: resolveCodeExecutionWorkspaceSelections({
conversation: options.req?.resolvedConversation,
request: options.req?.body,
resolveAgentCodeExecution(
withRequestCodeInputs({
req: options.req ?? {},
agent,
codeExecutionAvailable: true,
statefulSessionsAvailable:
agent?.stateful_code_sessions === true &&
(await checkCapability(options.req, AgentCapabilities.stateful_code_sessions)),
userId: user,
conversationId: options.req?.body?.conversationId,
}),
inheritedEnvironments: options.req?.codeWorkspaceInheritance,
environments:
options.req?.config?.endpoints?.agents?.statefulCodeSessions?.environments,
userId: user,
agentId: agent?.id,
conversationId: options.req?.body?.conversationId,
});
).context;
const { files, toolContext } = await primeCodeFiles({
...options,
signal,
Expand Down
2 changes: 2 additions & 0 deletions api/server/controllers/agents/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function createToolLoader({ req, res, signal, definitionsOnly = true }) {
tool_resources,
requestBody,
codeExecutionContext,
attachedEnvironmentOptOut,
accessibleMcpServerNames,
}) {
const agent = { id: agentId, tools, provider, model, tool_options };
Expand All @@ -147,6 +148,7 @@ function createToolLoader({ req, res, signal, definitionsOnly = true }) {
requestBody,
tool_resources,
codeExecutionContext,
attachedEnvironmentOptOut,
agentResourceType: ResourceType.REMOTE_AGENT,
definitionsOnly,
accessibleMcpServerNames,
Expand Down
2 changes: 2 additions & 0 deletions api/server/controllers/agents/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ function createToolLoader({ req, res, signal, definitionsOnly = true }) {
tool_resources,
requestBody,
codeExecutionContext,
attachedEnvironmentOptOut,
accessibleMcpServerNames,
}) {
const agent = { id: agentId, tools, provider, model, tool_options };
Expand All @@ -196,6 +197,7 @@ function createToolLoader({ req, res, signal, definitionsOnly = true }) {
requestBody,
tool_resources,
codeExecutionContext,
attachedEnvironmentOptOut,
agentResourceType: ResourceType.REMOTE_AGENT,
definitionsOnly,
accessibleMcpServerNames,
Expand Down
86 changes: 27 additions & 59 deletions api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ const {
getLazySubagentConfigId,
createRoutedGraphMemberLoader,
createViewableSubagentLoader,
resolveCodeExecutionContext,
resolveAgentCodeFlags,
withRequestCodeInputs,
resolveAgentCodeExecution,
resolveCodeExecutionWorkspaceSelections,
optsOutOfAttachedCodeEnvironment,
isImplicitStatefulCodeRouteAvailable,
resolveCodeExecutionWorkspaceContext,
resolveSubagentCodeWorkspaceInheritance,
resolveSubagentCodeAvailability,
guardRoutableSubagent,
createStatefulCodeEnvironmentPolicyError,
buildSubagentThreadTaskConfig,
backgroundCompletionWakeupsEnabled,
createLazyAgentHistoryResolver,
Expand Down Expand Up @@ -169,6 +168,7 @@ function createToolLoader(
tool_resources,
requestBody,
codeExecutionContext,
attachedEnvironmentOptOut,
accessibleMcpServerNames,
}) {
const agent = { id: agentId, tools, provider, model, tool_options };
Expand All @@ -183,6 +183,7 @@ function createToolLoader(
requestBody,
tool_resources,
codeExecutionContext,
attachedEnvironmentOptOut,
definitionsOnly,
accessibleMcpServerNames,
upstreamTokenProvider,
Expand Down Expand Up @@ -1151,6 +1152,7 @@ const initializeClientWithProvider = async ({
});

subagentCodeRouting = createSubagentCodeRouting({
codeEnvironmentMode: runtimeRequestBody?.codeEnvironmentMode,
allowEnvironmentSelection:
appConfig.endpoints?.agents?.statefulCodeSessions?.allowEnvironmentSelection,
persistedSelections: admittedConversation?.codeWorkspaces,
Expand All @@ -1166,77 +1168,43 @@ const initializeClientWithProvider = async ({
}),
});

/** Inputs the shared per-agent code rule reads for a subagent in this request. */
const getSubagentCodeParams = (agent) =>
withRequestCodeInputs({
req,
agent,
requestBody: runtimeRequestBody,
codeExecutionAvailable: codeEnvAvailable === true,
statefulSessionsAvailable: statefulSessionsAvailable === true,
allowedStatefulCodeEnvironments,
conversationId,
});

/** The code flags a lazy subagent runs with in this request. */
const getSubagentCodeFlags = (agent) => {
const configuredCodeEnvironments =
appConfig?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments;
const attachedEnvironmentOptOut = optsOutOfAttachedCodeEnvironment(
agent,
runtimeRequestBody,
configuredCodeEnvironments,
isImplicitStatefulCodeRouteAvailable(
process.env.CODE_ENVIRONMENT_DECISION_VERSION,
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL,
),
);
const lazyCodeEnvAvailable =
codeEnvAvailable === true &&
agent.tools?.includes(Tools.execute_code) === true &&
!attachedEnvironmentOptOut;
const statefulCodeSessions =
statefulSessionsAvailable === true &&
lazyCodeEnvAvailable &&
agent.stateful_code_sessions === true &&
agent.tools?.includes(Tools.execute_code) === true;
const statefulCodeEnvironment = agent.stateful_code_environment ?? 'user';
if (
statefulCodeSessions &&
!allowedStatefulCodeEnvironments.includes(statefulCodeEnvironment)
) {
throw createStatefulCodeEnvironmentPolicyError(statefulCodeEnvironment);
}
const flags = resolveAgentCodeFlags(getSubagentCodeParams(agent));
return {
configuredCodeEnvironments,
lazyCodeEnvAvailable,
statefulCodeSessions,
statefulCodeEnvironment,
lazyCodeEnvAvailable: flags.codeEnvAvailable,
statefulCodeSessions: flags.statefulSessions,
statefulCodeEnvironment: flags.statefulCodeEnvironment,
};
};

const toLazySubagentMetadata = async (agent) => {
const {
configuredCodeEnvironments,
lazyCodeEnvAvailable,
statefulCodeSessions,
statefulCodeEnvironment,
} = getSubagentCodeFlags(agent);
const { lazyCodeEnvAvailable, statefulCodeSessions, statefulCodeEnvironment } =
getSubagentCodeFlags(agent);
const codeAvailability = await resolveSubagentCodeAvailability({
agentId: agent.id,
codeEnvAvailable: lazyCodeEnvAvailable,
statefulCodeSessions,
resolveContext: async () => {
if (!lazyCodeEnvAvailable) return undefined;
const params = getSubagentCodeParams(agent);
return resolveCodeExecutionWorkspaceContext({
context: resolveCodeExecutionContext({
statefulSessions: statefulCodeSessions,
environment: statefulCodeEnvironment,
environmentId: agent.code_environment_id,
environmentIds: agent.code_environment_ids,
allowEnvironmentSelection:
appConfig.endpoints?.agents?.statefulCodeSessions?.allowEnvironmentSelection,
workspaceSelections: resolveCodeExecutionWorkspaceSelections({
conversation: admittedConversation,
request: runtimeRequestBody,
}),
inheritedEnvironments: req.codeWorkspaceInheritance,
environments: configuredCodeEnvironments,
userId,
agentId: agent.id,
conversationId,
}),
context: resolveAgentCodeExecution(params).context,
requestedSelections: runtimeRequestBody?.codeWorkspaces,
persistedSelections: admittedConversation?.codeWorkspaces,
environments: configuredCodeEnvironments,
environments: params.environments,
getAppConfig,
});
},
Expand Down
113 changes: 113 additions & 0 deletions api/server/services/Endpoints/agents/initialize.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,119 @@ describe('initializeClient — subagent loading', () => {
}
});

it('forwards the initializer code opt-out through the tool loader', async () => {
const codeExecutionContext = {
baseUrl: 'https://api.librechat.ai/v1',
codeSessionKey: 'execute_code',
executionProfile: 'default',
statefulSessions: false,
};
mockInitializeAgent.mockImplementationOnce(async (params) => {
await params.loadTools({
agentId: PRIMARY_ID,
tools: ['execute_code'],
codeExecutionContext,
attachedEnvironmentOptOut: true,
});
return makePrimaryConfig({});
});

await initializeClient({
req: makeSubagentReq(),
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
});

expect(loadAgentTools).toHaveBeenCalledWith(
expect.objectContaining({ codeExecutionContext, attachedEnvironmentOptOut: true }),
);
});

it('keeps a lazy subagent off its attached machine in a conversation that chose no workspace', async () => {
const subAgent = await createAgent({
id: SUBAGENT_ID,
name: 'Attached Default Subagent',
provider: 'openai',
model: 'gpt-4',
author: new mongoose.Types.ObjectId(),
tools: ['execute_code'],
stateful_code_sessions: true,
stateful_code_environment: 'agent-user',
code_environment_id: 'attached-vm',
code_environment_ids: ['other-vm'],
});
await grantView(subAgent);
mockInitializeAgent.mockResolvedValue(
makePrimaryConfig({
subagents: { enabled: true, allowSelf: false, agent_ids: [SUBAGENT_ID] },
}),
);
const attachedVm = (id) => ({
id,
name: id,
type: 'attached',
owner: 'deployment',
workerId: `${id}-worker`,
baseURL: 'https://bridge.example.com/v1/',
});
const req = makeSubagentReq();
req.body.codeEnvironmentMode = 'without_attached';
req.config.endpoints.agents.capabilities.push('execute_code', 'stateful_code_sessions');
req.config.endpoints.agents.statefulCodeSessions = {
allowedEnvironments: ['agent-user'],
allowEnvironmentSelection: true,
environments: [attachedVm('attached-vm'), attachedVm('other-vm')],
};
const fetchSpy = jest.spyOn(globalThis, 'fetch');

try {
await initializeClient({
req,
res: {},
signal: new AbortController().signal,
endpointOption: makeEndpointOption(),
});

const descriptor = agentClientArgs.agent.lazySubagentConfigs[0];
expect(descriptor).toEqual(
expect.objectContaining({
codeEnvAvailable: false,
statefulCodeSessions: false,
codeExecutionContext: undefined,
subagentHostArgs: undefined,
codeExecutionChoices: undefined,
}),
);
expect(descriptor.description ?? '').not.toContain('Unavailable in this conversation');

const initCalls = mockInitializeAgent.mock.calls.length;
await expect(
descriptor.resolve({
signal: new AbortController().signal,
hostArgs: { machine: 'other-vm' },
}),
).rejects.toThrow(/rejected/);
expect(mockInitializeAgent).toHaveBeenCalledTimes(initCalls);
mockInitializeAgent.mockResolvedValueOnce(makeSubagentConfig(SUBAGENT_ID));
await descriptor.resolve({ signal: new AbortController().signal });

const childInit = mockInitializeAgent.mock.calls.at(-1)[0];
expect(childInit.agent.code_environment_id).toBe('attached-vm');
expect(childInit.requestBody).toEqual(
expect.objectContaining({ codeEnvironmentMode: 'without_attached' }),
);
expect(
mockInitializeAgent.mock.calls.some(
([params]) => params.agent?.code_environment_id === 'other-vm',
),
).toBe(false);
expect(fetchSpy).not.toHaveBeenCalled();
} finally {
fetchSpy.mockRestore();
}
});

it.each([
[true, 'request', false],
[false, 'request', false],
Expand Down
Loading
Loading