@librechat/agents 3.2.58 → 3.2.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cjs/main.cjs +7 -0
  2. package/dist/cjs/stream.cjs +2 -1
  3. package/dist/cjs/stream.cjs.map +1 -1
  4. package/dist/cjs/tools/BashExecutor.cjs +58 -9
  5. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  6. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
  7. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  8. package/dist/cjs/tools/CodeExecutor.cjs +57 -7
  9. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  10. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
  11. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/ToolNode.cjs +19 -1
  13. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  14. package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
  15. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  16. package/dist/esm/main.mjs +3 -3
  17. package/dist/esm/stream.mjs +2 -1
  18. package/dist/esm/stream.mjs.map +1 -1
  19. package/dist/esm/tools/BashExecutor.mjs +56 -10
  20. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  21. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
  22. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  23. package/dist/esm/tools/CodeExecutor.mjs +54 -8
  24. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  25. package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
  26. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  27. package/dist/esm/tools/ToolNode.mjs +20 -2
  28. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  29. package/dist/esm/tools/eagerEventExecution.mjs +18 -2
  30. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  31. package/dist/types/tools/BashExecutor.d.ts +13 -0
  32. package/dist/types/tools/CodeExecutor.d.ts +14 -0
  33. package/dist/types/tools/ToolNode.d.ts +3 -0
  34. package/dist/types/tools/eagerEventExecution.d.ts +8 -0
  35. package/dist/types/types/tools.d.ts +58 -0
  36. package/package.json +1 -1
  37. package/src/stream.ts +17 -1
  38. package/src/tools/BashExecutor.ts +107 -14
  39. package/src/tools/BashProgrammaticToolCalling.ts +20 -1
  40. package/src/tools/CodeExecutor.ts +113 -9
  41. package/src/tools/ProgrammaticToolCalling.ts +27 -1
  42. package/src/tools/ToolNode.ts +36 -4
  43. package/src/tools/__tests__/BashExecutor.test.ts +39 -0
  44. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
  45. package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
  46. package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
  47. package/src/tools/eagerEventExecution.ts +32 -5
  48. package/src/types/tools.ts +65 -0
@@ -22,6 +22,19 @@ function recordArgsEqual(left, right) {
22
22
  function normalizeError(error) {
23
23
  return error instanceof Error ? error : new Error(String(error));
24
24
  }
25
+ /**
26
+ * Stateful runtime session hint for the remote sandbox: only when
27
+ * `toolExecution.sandbox.statefulSessions` is on; explicit host hint else the
28
+ * conversation `thread_id`. Undefined disables the wire field. Shared by the
29
+ * direct ToolNode path and both event-driven planners so they stay in lockstep.
30
+ */
31
+ function resolveRuntimeSessionHint(toolExecution, threadId) {
32
+ const sandbox = toolExecution?.sandbox;
33
+ if (sandbox?.statefulSessions !== true) return;
34
+ const explicit = sandbox.runtimeSessionHint;
35
+ if (explicit != null && explicit !== "") return explicit;
36
+ return threadId != null && threadId !== "" ? threadId : void 0;
37
+ }
25
38
  function buildToolExecutionRequestPlan(args) {
26
39
  const invalidArgsBehavior = args.invalidArgsBehavior ?? "abort";
27
40
  const prepared = [];
@@ -36,6 +49,7 @@ function buildToolExecutionRequestPlan(args) {
36
49
  args: {},
37
50
  stepId: toolCall.stepId,
38
51
  codeSessionContext: toolCall.codeSessionContext,
52
+ runtimeSessionHint: toolCall.runtimeSessionHint,
39
53
  rejectedErrorMessage: "Invalid tool call arguments: expected a JSON object."
40
54
  });
41
55
  continue;
@@ -45,7 +59,8 @@ function buildToolExecutionRequestPlan(args) {
45
59
  name: toolCall.name,
46
60
  args: coercedArgs,
47
61
  stepId: toolCall.stepId,
48
- codeSessionContext: toolCall.codeSessionContext
62
+ codeSessionContext: toolCall.codeSessionContext,
63
+ runtimeSessionHint: toolCall.runtimeSessionHint
49
64
  });
50
65
  }
51
66
  const nextUsageCount = new Map(args.usageCount);
@@ -60,6 +75,7 @@ function buildToolExecutionRequestPlan(args) {
60
75
  turn
61
76
  };
62
77
  if (toolCall.codeSessionContext != null) request.codeSessionContext = toolCall.codeSessionContext;
78
+ if (toolCall.runtimeSessionHint != null && toolCall.runtimeSessionHint !== "") request.runtimeSessionHint = toolCall.runtimeSessionHint;
63
79
  return request;
64
80
  });
65
81
  const requests = allRequests.filter((_, index) => prepared[index].rejectedErrorMessage == null);
@@ -81,6 +97,6 @@ function buildToolExecutionRequestPlan(args) {
81
97
  };
82
98
  }
83
99
  //#endregion
84
- export { buildToolExecutionRequestPlan, coerceRecordArgs, normalizeError, recordArgsEqual };
100
+ export { buildToolExecutionRequestPlan, coerceRecordArgs, normalizeError, recordArgsEqual, resolveRuntimeSessionHint };
85
101
 
86
102
  //# sourceMappingURL=eagerEventExecution.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"eagerEventExecution.mjs","names":[],"sources":["../../../src/tools/eagerEventExecution.ts"],"sourcesContent":["import type * as t from '@/types';\n\nexport function coerceRecordArgs(\n args: unknown\n): Record<string, unknown> | undefined {\n if (typeof args === 'string') {\n try {\n const parsed = JSON.parse(args) as unknown;\n return coerceRecordArgs(parsed);\n } catch {\n return undefined;\n }\n }\n\n if (args === null || typeof args !== 'object' || Array.isArray(args)) {\n return undefined;\n }\n\n return args as Record<string, unknown>;\n}\n\nexport function stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item)).join(',')}]`;\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n return `{${keys\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(',')}}`;\n }\n\n return JSON.stringify(value);\n}\n\nexport function recordArgsEqual(\n left: Record<string, unknown>,\n right: Record<string, unknown>\n): boolean {\n return stableStringify(left) === stableStringify(right);\n}\n\nexport function normalizeError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nexport type ToolExecutionPlanCall = {\n id?: string;\n name: string;\n args: unknown;\n stepId?: string;\n codeSessionContext?: t.ToolCallRequest['codeSessionContext'];\n};\n\nexport type ToolExecutionRequestPlan = {\n allRequests: t.ToolCallRequest[];\n requests: t.ToolCallRequest[];\n rejectedResults: t.ToolExecuteResult[];\n};\n\nexport function buildToolExecutionRequestPlan(args: {\n toolCalls: ToolExecutionPlanCall[];\n usageCount: Map<string, number>;\n invalidArgsBehavior?: 'abort' | 'error-result';\n recordTurn?: (toolName: string, turn: number, callId: string) => void;\n}): ToolExecutionRequestPlan | undefined {\n const invalidArgsBehavior = args.invalidArgsBehavior ?? 'abort';\n const prepared: Array<{\n id: string;\n name: string;\n args: Record<string, unknown>;\n stepId?: string;\n codeSessionContext?: t.ToolCallRequest['codeSessionContext'];\n rejectedErrorMessage?: string;\n }> = [];\n\n for (const toolCall of args.toolCalls) {\n if (\n toolCall.id == null ||\n toolCall.id === '' ||\n toolCall.name === ''\n ) {\n return undefined;\n }\n const coercedArgs = coerceRecordArgs(toolCall.args);\n if (coercedArgs == null) {\n if (invalidArgsBehavior === 'abort') {\n return undefined;\n }\n prepared.push({\n id: toolCall.id,\n name: toolCall.name,\n args: {},\n stepId: toolCall.stepId,\n codeSessionContext: toolCall.codeSessionContext,\n rejectedErrorMessage:\n 'Invalid tool call arguments: expected a JSON object.',\n });\n continue;\n }\n prepared.push({\n id: toolCall.id,\n name: toolCall.name,\n args: coercedArgs,\n stepId: toolCall.stepId,\n codeSessionContext: toolCall.codeSessionContext,\n });\n }\n\n const nextUsageCount = new Map(args.usageCount);\n const allRequests = prepared.map((toolCall): t.ToolCallRequest => {\n const turn = nextUsageCount.get(toolCall.name) ?? 0;\n nextUsageCount.set(toolCall.name, turn + 1);\n const request: t.ToolCallRequest = {\n id: toolCall.id,\n name: toolCall.name,\n args: toolCall.args,\n stepId: toolCall.stepId,\n turn,\n };\n if (toolCall.codeSessionContext != null) {\n request.codeSessionContext = toolCall.codeSessionContext;\n }\n return request;\n });\n const requests = allRequests.filter(\n (_, index) => prepared[index].rejectedErrorMessage == null\n );\n const rejectedResults = prepared.flatMap((toolCall) => {\n if (toolCall.rejectedErrorMessage == null) {\n return [];\n }\n return [\n {\n toolCallId: toolCall.id,\n status: 'error' as const,\n content: '',\n errorMessage: toolCall.rejectedErrorMessage,\n },\n ];\n });\n\n for (const [toolName, count] of nextUsageCount) {\n args.usageCount.set(toolName, count);\n }\n for (const request of allRequests) {\n args.recordTurn?.(request.name, request.turn ?? 0, request.id);\n }\n\n return { allRequests, requests, rejectedResults };\n}\n"],"mappings":";AAEA,SAAgB,iBACd,MACqC;CACrC,IAAI,OAAO,SAAS,UAClB,IAAI;EAEF,OAAO,iBADQ,KAAK,MAAM,IACG,CAAC;CAChC,QAAQ;EACN;CACF;CAGF,IAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GACjE;CAGF,OAAO;AACT;AAEA,SAAgB,gBAAgB,OAAwB;CACtD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,IAAI,MAAM,KAAK,SAAS,gBAAgB,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;CAGlE,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,SAAS;EAEf,OAAO,IADM,OAAO,KAAK,MAAM,CAAC,CAAC,KACnB,CAAC,CACZ,KAAK,QAAQ,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG,gBAAgB,OAAO,IAAI,GAAG,CAAC,CACtE,KAAK,GAAG,EAAE;CACf;CAEA,OAAO,KAAK,UAAU,KAAK;AAC7B;AAEA,SAAgB,gBACd,MACA,OACS;CACT,OAAO,gBAAgB,IAAI,MAAM,gBAAgB,KAAK;AACxD;AAEA,SAAgB,eAAe,OAAuB;CACpD,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACjE;AAgBA,SAAgB,8BAA8B,MAKL;CACvC,MAAM,sBAAsB,KAAK,uBAAuB;CACxD,MAAM,WAOD,CAAC;CAEN,KAAK,MAAM,YAAY,KAAK,WAAW;EACrC,IACE,SAAS,MAAM,QACf,SAAS,OAAO,MAChB,SAAS,SAAS,IAElB;EAEF,MAAM,cAAc,iBAAiB,SAAS,IAAI;EAClD,IAAI,eAAe,MAAM;GACvB,IAAI,wBAAwB,SAC1B;GAEF,SAAS,KAAK;IACZ,IAAI,SAAS;IACb,MAAM,SAAS;IACf,MAAM,CAAC;IACP,QAAQ,SAAS;IACjB,oBAAoB,SAAS;IAC7B,sBACE;GACJ,CAAC;GACD;EACF;EACA,SAAS,KAAK;GACZ,IAAI,SAAS;GACb,MAAM,SAAS;GACf,MAAM;GACN,QAAQ,SAAS;GACjB,oBAAoB,SAAS;EAC/B,CAAC;CACH;CAEA,MAAM,iBAAiB,IAAI,IAAI,KAAK,UAAU;CAC9C,MAAM,cAAc,SAAS,KAAK,aAAgC;EAChE,MAAM,OAAO,eAAe,IAAI,SAAS,IAAI,KAAK;EAClD,eAAe,IAAI,SAAS,MAAM,OAAO,CAAC;EAC1C,MAAM,UAA6B;GACjC,IAAI,SAAS;GACb,MAAM,SAAS;GACf,MAAM,SAAS;GACf,QAAQ,SAAS;GACjB;EACF;EACA,IAAI,SAAS,sBAAsB,MACjC,QAAQ,qBAAqB,SAAS;EAExC,OAAO;CACT,CAAC;CACD,MAAM,WAAW,YAAY,QAC1B,GAAG,UAAU,SAAS,MAAM,CAAC,wBAAwB,IACxD;CACA,MAAM,kBAAkB,SAAS,SAAS,aAAa;EACrD,IAAI,SAAS,wBAAwB,MACnC,OAAO,CAAC;EAEV,OAAO,CACL;GACE,YAAY,SAAS;GACrB,QAAQ;GACR,SAAS;GACT,cAAc,SAAS;EACzB,CACF;CACF,CAAC;CAED,KAAK,MAAM,CAAC,UAAU,UAAU,gBAC9B,KAAK,WAAW,IAAI,UAAU,KAAK;CAErC,KAAK,MAAM,WAAW,aACpB,KAAK,aAAa,QAAQ,MAAM,QAAQ,QAAQ,GAAG,QAAQ,EAAE;CAG/D,OAAO;EAAE;EAAa;EAAU;CAAgB;AAClD"}
1
+ {"version":3,"file":"eagerEventExecution.mjs","names":[],"sources":["../../../src/tools/eagerEventExecution.ts"],"sourcesContent":["import type * as t from '@/types';\n\nexport function coerceRecordArgs(\n args: unknown\n): Record<string, unknown> | undefined {\n if (typeof args === 'string') {\n try {\n const parsed = JSON.parse(args) as unknown;\n return coerceRecordArgs(parsed);\n } catch {\n return undefined;\n }\n }\n\n if (args === null || typeof args !== 'object' || Array.isArray(args)) {\n return undefined;\n }\n\n return args as Record<string, unknown>;\n}\n\nexport function stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item)).join(',')}]`;\n }\n\n if (value !== null && typeof value === 'object') {\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n return `{${keys\n .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)\n .join(',')}}`;\n }\n\n return JSON.stringify(value);\n}\n\nexport function recordArgsEqual(\n left: Record<string, unknown>,\n right: Record<string, unknown>\n): boolean {\n return stableStringify(left) === stableStringify(right);\n}\n\nexport function normalizeError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\nexport type ToolExecutionPlanCall = {\n id?: string;\n name: string;\n args: unknown;\n stepId?: string;\n codeSessionContext?: t.ToolCallRequest['codeSessionContext'];\n runtimeSessionHint?: string;\n};\n\n/**\n * Stateful runtime session hint for the remote sandbox: only when\n * `toolExecution.sandbox.statefulSessions` is on; explicit host hint else the\n * conversation `thread_id`. Undefined disables the wire field. Shared by the\n * direct ToolNode path and both event-driven planners so they stay in lockstep.\n */\nexport function resolveRuntimeSessionHint(\n toolExecution: t.ToolExecutionConfig | undefined,\n threadId: string | undefined\n): string | undefined {\n const sandbox = toolExecution?.sandbox;\n if (sandbox?.statefulSessions !== true) {\n return undefined;\n }\n const explicit = sandbox.runtimeSessionHint;\n if (explicit != null && explicit !== '') {\n return explicit;\n }\n return threadId != null && threadId !== '' ? threadId : undefined;\n}\n\nexport type ToolExecutionRequestPlan = {\n allRequests: t.ToolCallRequest[];\n requests: t.ToolCallRequest[];\n rejectedResults: t.ToolExecuteResult[];\n};\n\nexport function buildToolExecutionRequestPlan(args: {\n toolCalls: ToolExecutionPlanCall[];\n usageCount: Map<string, number>;\n invalidArgsBehavior?: 'abort' | 'error-result';\n recordTurn?: (toolName: string, turn: number, callId: string) => void;\n}): ToolExecutionRequestPlan | undefined {\n const invalidArgsBehavior = args.invalidArgsBehavior ?? 'abort';\n const prepared: Array<{\n id: string;\n name: string;\n args: Record<string, unknown>;\n stepId?: string;\n codeSessionContext?: t.ToolCallRequest['codeSessionContext'];\n runtimeSessionHint?: string;\n rejectedErrorMessage?: string;\n }> = [];\n\n for (const toolCall of args.toolCalls) {\n if (toolCall.id == null || toolCall.id === '' || toolCall.name === '') {\n return undefined;\n }\n const coercedArgs = coerceRecordArgs(toolCall.args);\n if (coercedArgs == null) {\n if (invalidArgsBehavior === 'abort') {\n return undefined;\n }\n prepared.push({\n id: toolCall.id,\n name: toolCall.name,\n args: {},\n stepId: toolCall.stepId,\n codeSessionContext: toolCall.codeSessionContext,\n runtimeSessionHint: toolCall.runtimeSessionHint,\n rejectedErrorMessage:\n 'Invalid tool call arguments: expected a JSON object.',\n });\n continue;\n }\n prepared.push({\n id: toolCall.id,\n name: toolCall.name,\n args: coercedArgs,\n stepId: toolCall.stepId,\n codeSessionContext: toolCall.codeSessionContext,\n runtimeSessionHint: toolCall.runtimeSessionHint,\n });\n }\n\n const nextUsageCount = new Map(args.usageCount);\n const allRequests = prepared.map((toolCall): t.ToolCallRequest => {\n const turn = nextUsageCount.get(toolCall.name) ?? 0;\n nextUsageCount.set(toolCall.name, turn + 1);\n const request: t.ToolCallRequest = {\n id: toolCall.id,\n name: toolCall.name,\n args: toolCall.args,\n stepId: toolCall.stepId,\n turn,\n };\n if (toolCall.codeSessionContext != null) {\n request.codeSessionContext = toolCall.codeSessionContext;\n }\n if (\n toolCall.runtimeSessionHint != null &&\n toolCall.runtimeSessionHint !== ''\n ) {\n request.runtimeSessionHint = toolCall.runtimeSessionHint;\n }\n return request;\n });\n const requests = allRequests.filter(\n (_, index) => prepared[index].rejectedErrorMessage == null\n );\n const rejectedResults = prepared.flatMap((toolCall) => {\n if (toolCall.rejectedErrorMessage == null) {\n return [];\n }\n return [\n {\n toolCallId: toolCall.id,\n status: 'error' as const,\n content: '',\n errorMessage: toolCall.rejectedErrorMessage,\n },\n ];\n });\n\n for (const [toolName, count] of nextUsageCount) {\n args.usageCount.set(toolName, count);\n }\n for (const request of allRequests) {\n args.recordTurn?.(request.name, request.turn ?? 0, request.id);\n }\n\n return { allRequests, requests, rejectedResults };\n}\n"],"mappings":"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"}
@@ -19,6 +19,15 @@ export declare const BashExecutionToolSchema: {
19
19
  readonly required: readonly ["command"];
20
20
  };
21
21
  export declare const BashExecutionToolDescription: string;
22
+ /**
23
+ * Bash statefulness is filesystem-tier: on a warm session the machine (files
24
+ * including /tmp, installed packages, background processes) persists between
25
+ * calls, but each call may start a fresh shell — so shell variables and cwd
26
+ * are NOT reliable, and the machine can be reset at any time. Only /mnt/data
27
+ * is durable.
28
+ */
29
+ export declare const STATEFUL_BASH_NOTE = "Session state (best-effort): commands in this conversation usually run on the same machine, so files (including /tmp), installed packages, and running background processes from earlier calls typically persist. Each call may still start a fresh shell \u2014 do not rely on shell variables or the working directory carrying over \u2014 and the machine may be reset at any time. Only /mnt/data is durable.";
30
+ export declare const StatefulBashExecutionToolDescription: string;
22
31
  /**
23
32
  * Supplemental prompt documenting the tool-output reference feature.
24
33
  *
@@ -39,7 +48,11 @@ export declare const BashToolOutputReferencesGuide: string;
39
48
  */
40
49
  export declare function buildBashExecutionToolDescription(options?: {
41
50
  enableToolOutputReferences?: boolean;
51
+ statefulSessions?: boolean;
42
52
  }): string;
53
+ export declare function buildBashExecutionToolSchema(opts?: {
54
+ statefulSessions?: boolean;
55
+ }): typeof BashExecutionToolSchema;
43
56
  export declare const BashExecutionToolName = Constants.BASH_TOOL;
44
57
  /**
45
58
  * Default bash tool definition using the base description.
@@ -38,6 +38,20 @@ export declare function buildCodeApiHttpErrorMessage(method: string, endpoint: s
38
38
  text: () => Promise<string>;
39
39
  }): Promise<string>;
40
40
  export declare const CodeExecutionToolDescription: string;
41
+ /**
42
+ * Best-effort statefulness note. Deliberately hedged: warm reuse is an
43
+ * optimization, not a guarantee (the runtime may be reset on idle timeout,
44
+ * eviction, or the 8h VM lifetime), so the model must never depend on carried
45
+ * state for correctness and must persist anything durable to /mnt/data.
46
+ */
47
+ export declare const STATEFUL_ENV_NOTE = "Session state (best-effort): consecutive executions in this conversation usually share one runtime, so variables, imports, and in-memory data from earlier successful calls are typically still available. The runtime may be reset at any time, so treat carried-over state as an optimization, never a guarantee. Anything that must survive MUST be written to /mnt/data. If a NameError/ImportError signals lost state, re-run the needed setup and continue.";
48
+ export declare const StatefulCodeExecutionToolDescription: string;
49
+ export declare function buildCodeExecutionToolDescription(opts?: {
50
+ statefulSessions?: boolean;
51
+ }): string;
52
+ export declare function buildCodeExecutionToolSchema(opts?: {
53
+ statefulSessions?: boolean;
54
+ }): typeof CodeExecutionToolSchema;
41
55
  export declare const CodeExecutionToolName = Constants.EXECUTE_CODE;
42
56
  export declare const CodeExecutionToolDefinition: {
43
57
  readonly name: Constants.EXECUTE_CODE;
@@ -314,6 +314,9 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
314
314
  * opt-in so only host-declared tools can influence the shared session.
315
315
  */
316
316
  private participatesInCodeSession;
317
+ /** Delegates to the shared resolver so the direct and event-driven planning
318
+ * paths derive the runtime session hint identically. */
319
+ private resolveRuntimeSessionHint;
317
320
  private storeCodeSessionFromResults;
318
321
  /**
319
322
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
@@ -9,7 +9,15 @@ export type ToolExecutionPlanCall = {
9
9
  args: unknown;
10
10
  stepId?: string;
11
11
  codeSessionContext?: t.ToolCallRequest['codeSessionContext'];
12
+ runtimeSessionHint?: string;
12
13
  };
14
+ /**
15
+ * Stateful runtime session hint for the remote sandbox: only when
16
+ * `toolExecution.sandbox.statefulSessions` is on; explicit host hint else the
17
+ * conversation `thread_id`. Undefined disables the wire field. Shared by the
18
+ * direct ToolNode path and both event-driven planners so they stay in lockstep.
19
+ */
20
+ export declare function resolveRuntimeSessionHint(toolExecution: t.ToolExecutionConfig | undefined, threadId: string | undefined): string | undefined;
13
21
  export type ToolExecutionRequestPlan = {
14
22
  allRequests: t.ToolCallRequest[];
15
23
  requests: t.ToolCallRequest[];
@@ -278,6 +278,15 @@ export type CodeExecutionToolParams = undefined | {
278
278
  files?: CodeEnvFile[];
279
279
  /** Optional host-supplied Code API auth headers. */
280
280
  authHeaders?: CodeApiAuthHeaders;
281
+ /**
282
+ * Advertise best-effort stateful sessions in the tool description
283
+ * (variables/files may persist between calls, may reset). Prompt text
284
+ * only, and it must be set here because the description is bound to the
285
+ * LLM at construction time. Pair it with the run-scoped
286
+ * `toolExecution.sandbox.statefulSessions` gate, which drives the wire
287
+ * hint — set both from one flag so the prompt and the backend agree.
288
+ */
289
+ statefulSessions?: boolean;
281
290
  };
282
291
  export type CodeApiAuthHeaderMap = Record<string, string>;
283
292
  export type CodeApiAuthHeaders = CodeApiAuthHeaderMap | (() => CodeApiAuthHeaderMap | Promise<CodeApiAuthHeaderMap>);
@@ -326,6 +335,13 @@ export type ExecuteResult = {
326
335
  stdout: string;
327
336
  stderr: string;
328
337
  files?: FileRefs;
338
+ /**
339
+ * Durable runtime session id echoed by a stateful Code API backend
340
+ * (hash of tenant+user+hint). Additive; absent on stateless servers.
341
+ */
342
+ runtime_session_id?: string;
343
+ /** Whether this execution reused a warm runtime session or started fresh. */
344
+ runtime_status?: 'new' | 'reused';
329
345
  };
330
346
  /** JSON Schema type definition for tool parameters */
331
347
  export type JsonSchemaType = {
@@ -381,6 +397,12 @@ export type ToolCallRequest = {
381
397
  session_id: string;
382
398
  files?: CodeEnvFile[];
383
399
  };
400
+ /**
401
+ * Stable runtime session hint for stateful sandbox sessions. Orthogonal to
402
+ * `codeSessionContext` (which threads the transient exec-session for file
403
+ * continuity): the hint identifies the durable server-side runtime session.
404
+ */
405
+ runtimeSessionHint?: string;
384
406
  };
385
407
  /** Batch request containing ALL tool calls for a graph step */
386
408
  export type ToolExecuteBatchRequest = {
@@ -896,6 +918,31 @@ export type CloudflareSandboxExecutionConfig = {
896
918
  /** Run a fast per-file syntax check after successful edits/writes. */
897
919
  postEditSyntaxCheck?: LocalExecutionConfig['postEditSyntaxCheck'];
898
920
  };
921
+ export type SandboxExecutionConfig = {
922
+ /**
923
+ * Opt into best-effort stateful runtime sessions on the remote Code API
924
+ * (its warm per-session MicroVM backend). This gate is run-scoped: it only
925
+ * controls the wire behavior (ToolNode injecting the session hint on
926
+ * execute_code/bash calls). The transport is otherwise unchanged.
927
+ *
928
+ * It does NOT change the model-facing tool description. Tool descriptions are
929
+ * bound to the LLM at construction time (`createCodeExecutionTool` /
930
+ * `createBashExecutionTool`), before this run config is applied inside the
931
+ * graph, so they can only be adjusted via the tools' own `statefulSessions`
932
+ * factory param. Set BOTH from one flag (as LibreChat does): with this on but
933
+ * the factory param off, the backend runs statefully while the model is still
934
+ * told the environment is stateless (non-corrupting — the model just won't
935
+ * exploit persistence).
936
+ */
937
+ statefulSessions?: boolean;
938
+ /**
939
+ * Stable identity for the runtime session (e.g. the conversation id). The
940
+ * server derives the real session id as hash(tenant, user, hint), so this
941
+ * is never a security boundary. Falls back to `configurable.thread_id` when
942
+ * omitted.
943
+ */
944
+ runtimeSessionHint?: string;
945
+ };
899
946
  export type ToolExecutionConfig = {
900
947
  /** `sandbox` preserves the remote Code API behavior and is the default. */
901
948
  engine?: ToolExecutionEngine;
@@ -903,6 +950,8 @@ export type ToolExecutionConfig = {
903
950
  local?: LocalExecutionConfig;
904
951
  /** Cloudflare Sandbox execution settings used when `engine` is `cloudflare-sandbox`. */
905
952
  cloudflare?: CloudflareSandboxExecutionConfig;
953
+ /** Remote sandbox settings; applies when `engine` is `sandbox` or omitted. */
954
+ sandbox?: SandboxExecutionConfig;
906
955
  };
907
956
  export type ProgrammaticCache = {
908
957
  toolMap: ToolMap;
@@ -1005,6 +1054,9 @@ export type ProgrammaticExecutionResponse = {
1005
1054
  stdout?: string;
1006
1055
  stderr?: string;
1007
1056
  files?: FileRefs;
1057
+ /** Durable runtime session echo from a stateful backend (additive). */
1058
+ runtime_session_id?: string;
1059
+ runtime_status?: 'new' | 'reused';
1008
1060
  /** Present when status='error' */
1009
1061
  error?: string;
1010
1062
  };
@@ -1015,6 +1067,9 @@ export type ProgrammaticExecutionArtifact = {
1015
1067
  /** Execution session — see `CodeSessionContext.session_id`. */
1016
1068
  session_id?: string;
1017
1069
  files?: FileRefs;
1070
+ /** Durable runtime session echo from a stateful backend (additive). */
1071
+ runtime_session_id?: string;
1072
+ runtime_status?: 'new' | 'reused';
1018
1073
  };
1019
1074
  /** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
1020
1075
  export type BashExecutionToolParams = CodeExecutionToolParams;
@@ -1062,6 +1117,9 @@ export type CodeExecutionArtifact = {
1062
1117
  /** Execution session — see `CodeSessionContext.session_id`. */
1063
1118
  session_id?: string;
1064
1119
  files?: FileRefs;
1120
+ /** Durable runtime session echo from a stateful backend (additive). */
1121
+ runtime_session_id?: string;
1122
+ runtime_status?: 'new' | 'reused';
1065
1123
  };
1066
1124
  /**
1067
1125
  * Generic session context union type for different tool types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.58",
3
+ "version": "3.2.59",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
package/src/stream.ts CHANGED
@@ -142,7 +142,18 @@ function isEagerExecutionExcludedTool(
142
142
  // side-effecting: never prestart it speculatively (a revised/superseded turn
143
143
  // would leave the write applied). Implies exclusion without the host having
144
144
  // to also list the name in excludeToolNames.
145
- return graph.codeSessionToolNames?.includes(name) === true;
145
+ if (graph.codeSessionToolNames?.includes(name) === true) {
146
+ return true;
147
+ }
148
+ // With stateful sessions on, execute_code/bash run against a DURABLE warm
149
+ // runtime. Speculative prestart there is unsafe: if the model revises the
150
+ // args, ToolNode discards the eager result but the mutation has already
151
+ // landed in the session workspace, corrupting later runs. Stateless mode
152
+ // uses a throwaway VM per call, so eager prestart stays safe there.
153
+ return (
154
+ graph.toolExecution?.sandbox?.statefulSessions === true &&
155
+ CODE_EXECUTION_TOOLS.has(name)
156
+ );
146
157
  }
147
158
 
148
159
  function isDirectGraphTool(
@@ -668,6 +679,11 @@ function createEagerToolExecutionPlan(args: {
668
679
  return undefined;
669
680
  }
670
681
 
682
+ /* No runtimeSessionHint here on purpose: the eager path is speculative, and
683
+ * code-session tools (the only ones that carry a hint) are excluded from
684
+ * eager prestart entirely when stateful sessions are on — see
685
+ * isEagerExecutionExcludedTool. The durable runtime is only ever touched by
686
+ * the committed ToolNode path. */
671
687
  const plan = buildToolExecutionRequestPlan({
672
688
  toolCalls: candidateToolCalls.map((toolCall) => ({
673
689
  id: toolCall.id,
@@ -57,6 +57,29 @@ Usage:
57
57
  - NEVER use this tool to execute malicious commands.
58
58
  `.trim();
59
59
 
60
+ /**
61
+ * Bash statefulness is filesystem-tier: on a warm session the machine (files
62
+ * including /tmp, installed packages, background processes) persists between
63
+ * calls, but each call may start a fresh shell — so shell variables and cwd
64
+ * are NOT reliable, and the machine can be reset at any time. Only /mnt/data
65
+ * is durable.
66
+ */
67
+ export const STATEFUL_BASH_NOTE =
68
+ 'Session state (best-effort): commands in this conversation usually run on the same machine, so files (including /tmp), installed packages, and running background processes from earlier calls typically persist. Each call may still start a fresh shell — do not rely on shell variables or the working directory carrying over — and the machine may be reset at any time. Only /mnt/data is durable.';
69
+
70
+ export const StatefulBashExecutionToolDescription = `
71
+ Runs bash commands and returns stdout/stderr output from a session-based execution environment, similar to a long-running machine.
72
+
73
+ ${STATEFUL_BASH_NOTE}
74
+
75
+ Usage:
76
+ - No network access available.
77
+ - Generated files are automatically delivered; **DO NOT** provide download links.
78
+ - ${CODE_ARTIFACT_PATH_GUIDANCE}
79
+ - ${BASH_SHELL_GUIDANCE}
80
+ - NEVER use this tool to execute malicious commands.
81
+ `.trim();
82
+
60
83
  /**
61
84
  * Supplemental prompt documenting the tool-output reference feature.
62
85
  *
@@ -87,11 +110,45 @@ Referencing previous tool outputs:
87
110
  */
88
111
  export function buildBashExecutionToolDescription(options?: {
89
112
  enableToolOutputReferences?: boolean;
113
+ statefulSessions?: boolean;
90
114
  }): string {
115
+ const base =
116
+ options?.statefulSessions === true
117
+ ? StatefulBashExecutionToolDescription
118
+ : BashExecutionToolDescription;
91
119
  if (options?.enableToolOutputReferences === true) {
92
- return `${BashExecutionToolDescription}\n\n${BashToolOutputReferencesGuide}`;
120
+ return `${base}\n\n${BashToolOutputReferencesGuide}`;
93
121
  }
94
- return BashExecutionToolDescription;
122
+ return base;
123
+ }
124
+
125
+ const STATELESS_BASH_PARAM_NOTE =
126
+ 'The environment is stateless; variables and state don\'t persist between executions.';
127
+ const STATEFUL_BASH_PARAM_NOTE =
128
+ 'Files, installed packages, and background processes usually persist between calls, but each call may start a fresh shell (do not rely on shell variables or cwd) and the machine may reset. Only /mnt/data is durable.';
129
+
130
+ export function buildBashExecutionToolSchema(opts?: {
131
+ statefulSessions?: boolean;
132
+ }): typeof BashExecutionToolSchema {
133
+ const note =
134
+ opts?.statefulSessions === true
135
+ ? STATEFUL_BASH_PARAM_NOTE
136
+ : STATELESS_BASH_PARAM_NOTE;
137
+ const commandDescription =
138
+ BashExecutionToolSchema.properties.command.description.replace(
139
+ STATELESS_BASH_PARAM_NOTE,
140
+ note
141
+ );
142
+ return {
143
+ ...BashExecutionToolSchema,
144
+ properties: {
145
+ ...BashExecutionToolSchema.properties,
146
+ command: {
147
+ ...BashExecutionToolSchema.properties.command,
148
+ description: commandDescription,
149
+ },
150
+ },
151
+ } as typeof BashExecutionToolSchema;
95
152
  }
96
153
 
97
154
  export const BashExecutionToolName = Constants.BASH_TOOL;
@@ -117,15 +174,32 @@ function createBashExecutionTool(
117
174
  ): DynamicStructuredTool {
118
175
  return tool(
119
176
  async (rawInput, config) => {
120
- const { authHeaders, ...executionParams } = params ?? {};
121
- const { command, ...rest } = rawInput as {
177
+ /* `statefulSessions` is prompt-only keep it out of the wire body. */
178
+ const {
179
+ authHeaders,
180
+ statefulSessions: _statefulSessions,
181
+ ...executionParams
182
+ } = params ?? {};
183
+ void _statefulSessions;
184
+ /* Drop any model-supplied `runtime_session_hint` from the raw args: the
185
+ * hint must only come from ToolNode's injected `_runtime_session_hint`
186
+ * (below), never from the tool call itself. */
187
+ const {
188
+ command,
189
+ runtime_session_hint: _ignoredModelHint,
190
+ ...rest
191
+ } = rawInput as {
122
192
  command: string;
193
+ runtime_session_hint?: unknown;
123
194
  args?: string[];
124
195
  };
125
- const { session_id, _injected_files } = (config.toolCall ?? {}) as {
126
- session_id?: string;
127
- _injected_files?: t.CodeEnvFile[];
128
- };
196
+ void _ignoredModelHint;
197
+ const { session_id, _injected_files, _runtime_session_hint } =
198
+ (config.toolCall ?? {}) as {
199
+ session_id?: string;
200
+ _injected_files?: t.CodeEnvFile[];
201
+ _runtime_session_hint?: string;
202
+ };
129
203
 
130
204
  const postData: Record<string, unknown> = {
131
205
  lang: 'bash',
@@ -134,6 +208,13 @@ function createBashExecutionTool(
134
208
  ...executionParams,
135
209
  };
136
210
 
211
+ if (
212
+ typeof _runtime_session_hint === 'string' &&
213
+ _runtime_session_hint !== ''
214
+ ) {
215
+ postData.runtime_session_hint = _runtime_session_hint;
216
+ }
217
+
137
218
  /* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`
138
219
  * HTTP fallback was removed because codeapi's sessionAuth requires
139
220
  * kind/id query params unavailable at this point. */
@@ -187,12 +268,24 @@ function createBashExecutionTool(
187
268
  command
188
269
  );
189
270
  const hasFiles = result.files != null && result.files.length > 0;
271
+ const runtimeEcho =
272
+ result.runtime_session_id != null
273
+ ? {
274
+ runtime_session_id: result.runtime_session_id,
275
+ runtime_status: result.runtime_status,
276
+ }
277
+ : {};
190
278
  return [
191
279
  appendCodeSessionFileSummary(outputWithReminder, result.files),
192
280
  (hasFiles
193
- ? { session_id: result.session_id, files: result.files }
281
+ ? {
282
+ session_id: result.session_id,
283
+ files: result.files,
284
+ ...runtimeEcho,
285
+ }
194
286
  : {
195
287
  session_id: result.session_id,
288
+ ...runtimeEcho,
196
289
  }) satisfies t.CodeExecutionArtifact,
197
290
  ];
198
291
  } catch (error) {
@@ -200,15 +293,15 @@ function createBashExecutionTool(
200
293
  (error as Error | undefined)?.message ?? '',
201
294
  command
202
295
  );
203
- throw new Error(
204
- `Execution error:\n\n${messageWithReminder}`
205
- );
296
+ throw new Error(`Execution error:\n\n${messageWithReminder}`);
206
297
  }
207
298
  },
208
299
  {
209
300
  name: BashExecutionToolName,
210
- description: BashExecutionToolDescription,
211
- schema: BashExecutionToolSchema,
301
+ description: buildBashExecutionToolDescription({
302
+ statefulSessions: params?.statefulSessions,
303
+ }),
304
+ schema: buildBashExecutionToolSchema(params ?? undefined),
212
305
  responseFormat: Constants.CONTENT_AND_ARTIFACT,
213
306
  }
214
307
  );
@@ -304,8 +304,15 @@ export function createBashProgrammaticToolCallingTool(
304
304
  Partial<t.ProgrammaticCache> & {
305
305
  session_id?: string;
306
306
  _injected_files?: t.CodeEnvFile[];
307
+ _runtime_session_hint?: string;
307
308
  };
308
- const { toolMap, toolDefs, session_id, _injected_files } = toolCall;
309
+ const {
310
+ toolMap,
311
+ toolDefs,
312
+ session_id,
313
+ _injected_files,
314
+ _runtime_session_hint,
315
+ } = toolCall;
309
316
 
310
317
  if (toolMap == null || toolMap.size === 0) {
311
318
  throw new Error(
@@ -351,6 +358,15 @@ export function createBashProgrammaticToolCallingTool(
351
358
  );
352
359
  }
353
360
 
361
+ /* Stateful sessions: hint on the INITIAL request only (continuations
362
+ * bind via continuation_token). Wire-only in v1 — BashPTC keeps its
363
+ * stateless prompt. */
364
+ const runtimeSessionHint =
365
+ typeof _runtime_session_hint === 'string' &&
366
+ _runtime_session_hint !== ''
367
+ ? _runtime_session_hint
368
+ : undefined;
369
+
354
370
  let response = await makeRequest(
355
371
  EXEC_ENDPOINT,
356
372
  {
@@ -360,6 +376,9 @@ export function createBashProgrammaticToolCallingTool(
360
376
  session_id,
361
377
  timeout,
362
378
  ...(files && files.length > 0 ? { files } : {}),
379
+ ...(runtimeSessionHint != null
380
+ ? { runtime_session_hint: runtimeSessionHint }
381
+ : {}),
363
382
  },
364
383
  proxy,
365
384
  initParams.authHeaders