From 19d736811ea55c64abe92fbfcb8a49a12604ad7f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 7 Jul 2026 07:38:06 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20stateful=5Fcode=5Fsessions?= =?UTF-8?q?=20capability=20for=20warm=20Code=20API=20sandbox=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the @librechat/agents stateful sandbox sub-config behind a new, off-by-default stateful_code_sessions agent capability. createRun sets toolExecution.sandbox.statefulSessions when code execution is active in the run AND the capability is enabled; execute_code and bash_tool factories get the param so their descriptions hedge toward persistence. Rides the existing variable-not-literal runConfig pattern, so it no-ops until @librechat/agents is bumped to the version shipping the sandbox sub-config. --- api/app/clients/tools/util/handleTools.js | 12 ++++++- api/server/services/ToolService.js | 11 ++++++ packages/api/src/agents/run.ts | 36 +++++++++++++++++++ .../src/agents/statefulCodeSessions.spec.ts | 27 ++++++++++++++ packages/data-provider/src/config.ts | 1 + 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/agents/statefulCodeSessions.spec.ts diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index a01d4fcdec..f653c26a08 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -22,6 +22,7 @@ const { Permissions, EToolResources, PermissionTypes, + AgentCapabilities, } = require('librechat-data-provider'); const { availableTools, @@ -51,7 +52,7 @@ const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSe const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { getMCPServerTools } = require('~/server/services/Config'); +const { getMCPServerTools, checkCapability } = require('~/server/services/Config'); const { getMCPServersRegistry } = require('~/config'); const { getRoleByName, setMemory, deleteMemory, getFormattedMemories } = require('~/models'); @@ -301,10 +302,19 @@ const loadTools = async ({ if (files?.length) { primedCodeFiles = files; } + /* Hedge the execute_code description toward persistence only when the + * admin `stateful_code_sessions` capability is on (off by default); the + * matching wire hint is set in the run config. Older @librechat/agents + * ignore the param. */ + const statefulSessions = await checkCapability( + options.req, + AgentCapabilities.stateful_code_sessions, + ); return createCodeExecutionTool({ user_id: user, files, authHeaders: () => getCodeApiAuthHeaders(options.req), + statefulSessions, }); }; continue; diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 0b24714ec6..b5d4e192b8 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1483,6 +1483,16 @@ async function loadToolsForExecution({ enabledCapabilities?.has(AgentCapabilities.execute_code) === true && agent?.tools?.includes(Tools.execute_code) === true; + /** + * Opt bash_tool into the hedged stateful-session description. Gated on code + * execution being enabled AND the admin `stateful_code_sessions` capability; + * off by default. Sets prompt text only (the wire hint is set at run config). + * PTC keeps its stateless prompt in v1. Older @librechat/agents ignore the param. + */ + const statefulCodeSessions = + codeExecutionEnabled && + enabledCapabilities?.has(AgentCapabilities.stateful_code_sessions) === true; + const isPTC = isPTCRequested && enabledCapabilities.has(AgentCapabilities.programmatic_tools) && @@ -1534,6 +1544,7 @@ async function loadToolsForExecution({ try { const bashTool = createBashExecutionTool({ authHeaders: () => getCodeApiAuthHeaders(req), + statefulSessions: statefulCodeSessions, }); allLoadedTools.push(bashTool); } catch (error) { diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 7a76fc62b6..6a191cb721 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -3,6 +3,7 @@ import { Run, Providers, Constants } from '@librechat/agents'; import { KnownEndpoints, EModelEndpoint, + AgentCapabilities, MAX_SUBAGENT_DEPTH, MAX_SUBAGENT_RUN_CONFIGS, extractEnvVariable, @@ -778,6 +779,26 @@ function isAskUserQuestionAdminDisabled(appConfig?: AppConfig): boolean { return appConfig?.filteredTools?.includes(ASK_USER_QUESTION_TOOL_NAME) === true; } +/** + * Whether this run opts its remote sandbox tools into best-effort stateful + * runtime sessions (a warm per-session MicroVM workspace on the Code API). + * Requires BOTH code execution active in the run (`codeEnvActive`, i.e. some + * reachable agent has execute_code/bash) AND the admin `stateful_code_sessions` + * capability — a stateful sandbox session is meaningless without code + * execution. Off by default: the capability is absent from + * `defaultAgentCapabilities`, and the SDK derives the session hint from + * `thread_id` (= conversationId), so this is never a trust boundary. + */ +export function resolveStatefulCodeSessions( + codeEnvActive: boolean, + agentsEndpointConfig: { capabilities?: AgentCapabilities[] } | undefined, +): boolean { + if (!codeEnvActive) { + return false; + } + return new Set(agentsEndpointConfig?.capabilities).has(AgentCapabilities.stateful_code_sessions); +} + /** * Whether any agent reachable in the run — primary, handoff/parallel, or a * nested subagent — opts into cross-turn `reasoning_content` reconstruction. @@ -1282,6 +1303,12 @@ export async function createRun({ * and the resume route). When disabled, nothing attaches and the run is identical * to before this feature shipped. */ + // `enableToolOutputReferences` (bash present anywhere in the run) doubles as + // the "code execution active" signal for the stateful-session gate. + const statefulCodeSessions = resolveStatefulCodeSessions( + enableToolOutputReferences, + agentsEndpointConfig, + ); // Resolve the effective policy through the single seam so per-agent / per-skill // sources can layer in later without touching this call site (see // `resolveToolApprovalPolicy`). Only the endpoint layer is wired today, so this @@ -1378,6 +1405,15 @@ export async function createRun({ ...(enableToolOutputReferences && { toolOutputReferences: { enabled: true }, }), + // Best-effort stateful runtime sessions on the remote Code API. The SDK + // stamps a per-conversation session hint on execute_code/bash requests and + // hedges those tools' descriptions; the transport is otherwise unchanged. + // `engine` is omitted (defaults to `sandbox`) and the hint defaults to + // thread_id. Requires @librechat/agents with `toolExecution.sandbox`; + // older versions ignore the field. + ...(statefulCodeSessions && { + toolExecution: { sandbox: { statefulSessions: true } }, + }), // HITL opt-in: the `humanInTheLoop` switch + the PreToolUse policy hook. Spread // here (not just `compileOptions.checkpointer` above) so an `ask` decision raises // a real interrupt — without these the run would never pause. Absent when disabled. diff --git a/packages/api/src/agents/statefulCodeSessions.spec.ts b/packages/api/src/agents/statefulCodeSessions.spec.ts new file mode 100644 index 0000000000..d83e103074 --- /dev/null +++ b/packages/api/src/agents/statefulCodeSessions.spec.ts @@ -0,0 +1,27 @@ +import { AgentCapabilities } from 'librechat-data-provider'; +import { resolveStatefulCodeSessions } from './run'; + +describe('resolveStatefulCodeSessions', () => { + const withCap = { + capabilities: [AgentCapabilities.execute_code, AgentCapabilities.stateful_code_sessions], + }; + const withoutCap = { capabilities: [AgentCapabilities.execute_code] }; + + it('enables only when code execution is active AND the capability is present', () => { + expect(resolveStatefulCodeSessions(true, withCap)).toBe(true); + }); + + it('stays off when the capability is absent, even with code execution active', () => { + expect(resolveStatefulCodeSessions(true, withoutCap)).toBe(false); + }); + + it('stays off when code execution is inactive, even with the capability present', () => { + expect(resolveStatefulCodeSessions(false, withCap)).toBe(false); + }); + + it('stays off (default) when the endpoint config or capabilities are missing', () => { + expect(resolveStatefulCodeSessions(true, undefined)).toBe(false); + expect(resolveStatefulCodeSessions(true, {})).toBe(false); + expect(resolveStatefulCodeSessions(true, { capabilities: [] })).toBe(false); + }); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 5a753ab1ac..653d112552 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -565,6 +565,7 @@ export enum AgentCapabilities { end_after_tools = 'end_after_tools', deferred_tools = 'deferred_tools', execute_code = 'execute_code', + stateful_code_sessions = 'stateful_code_sessions', file_search = 'file_search', web_search = 'web_search', artifacts = 'artifacts',