@librechat/agents 3.2.58 → 3.2.60

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 (67) hide show
  1. package/dist/cjs/graphs/Graph.cjs +31 -7
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +7 -0
  4. package/dist/cjs/run.cjs +4 -0
  5. package/dist/cjs/run.cjs.map +1 -1
  6. package/dist/cjs/stream.cjs +2 -1
  7. package/dist/cjs/stream.cjs.map +1 -1
  8. package/dist/cjs/tools/BashExecutor.cjs +58 -9
  9. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/CodeExecutor.cjs +57 -7
  13. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
  15. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  16. package/dist/cjs/tools/ToolNode.cjs +114 -11
  17. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  18. package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
  19. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +31 -7
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/main.mjs +3 -3
  23. package/dist/esm/run.mjs +4 -0
  24. package/dist/esm/run.mjs.map +1 -1
  25. package/dist/esm/stream.mjs +2 -1
  26. package/dist/esm/stream.mjs.map +1 -1
  27. package/dist/esm/tools/BashExecutor.mjs +56 -10
  28. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  29. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
  30. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  31. package/dist/esm/tools/CodeExecutor.mjs +54 -8
  32. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  33. package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
  34. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  35. package/dist/esm/tools/ToolNode.mjs +115 -12
  36. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  37. package/dist/esm/tools/eagerEventExecution.mjs +18 -2
  38. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  39. package/dist/types/graphs/Graph.d.ts +21 -3
  40. package/dist/types/run.d.ts +1 -0
  41. package/dist/types/tools/BashExecutor.d.ts +13 -0
  42. package/dist/types/tools/CodeExecutor.d.ts +14 -0
  43. package/dist/types/tools/ToolNode.d.ts +60 -1
  44. package/dist/types/tools/eagerEventExecution.d.ts +8 -0
  45. package/dist/types/types/hitl.d.ts +49 -3
  46. package/dist/types/types/run.d.ts +21 -0
  47. package/dist/types/types/tools.d.ts +95 -1
  48. package/package.json +1 -1
  49. package/src/graphs/Graph.ts +74 -28
  50. package/src/run.ts +4 -0
  51. package/src/specs/ask-user-question-batch.test.ts +289 -0
  52. package/src/specs/tool-error-resume.test.ts +194 -0
  53. package/src/stream.ts +17 -1
  54. package/src/tools/BashExecutor.ts +107 -14
  55. package/src/tools/BashProgrammaticToolCalling.ts +20 -1
  56. package/src/tools/CodeExecutor.ts +113 -9
  57. package/src/tools/ProgrammaticToolCalling.ts +27 -1
  58. package/src/tools/ToolNode.ts +208 -31
  59. package/src/tools/__tests__/BashExecutor.test.ts +39 -0
  60. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
  61. package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
  62. package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
  63. package/src/tools/__tests__/hitl.test.ts +48 -0
  64. package/src/tools/eagerEventExecution.ts +32 -5
  65. package/src/types/hitl.ts +49 -3
  66. package/src/types/run.ts +21 -0
  67. package/src/types/tools.ts +102 -1
@@ -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":";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;;;;;;;AAiBA,SAAgB,0BACd,eACA,UACoB;CACpB,MAAM,UAAU,eAAe;CAC/B,IAAI,SAAS,qBAAqB,MAChC;CAEF,MAAM,WAAW,QAAQ;CACzB,IAAI,YAAY,QAAQ,aAAa,IACnC,OAAO;CAET,OAAO,YAAY,QAAQ,aAAa,KAAK,WAAW,KAAA;AAC1D;AAQA,SAAgB,8BAA8B,MAKL;CACvC,MAAM,sBAAsB,KAAK,uBAAuB;CACxD,MAAM,WAQD,CAAC;CAEN,KAAK,MAAM,YAAY,KAAK,WAAW;EACrC,IAAI,SAAS,MAAM,QAAQ,SAAS,OAAO,MAAM,SAAS,SAAS,IACjE;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,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;GAC7B,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,IACE,SAAS,sBAAsB,QAC/B,SAAS,uBAAuB,IAEhC,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"}
@@ -82,6 +82,14 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
82
82
  */
83
83
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
84
84
  codeSessionToolNames: string[] | undefined;
85
+ /**
86
+ * Run-scoped names of tools whose in-process body may raise a LangGraph
87
+ * `interrupt()` (e.g. `ask_user_question`). Threaded from
88
+ * `RunConfig.interruptingToolNames` into every ToolNode this graph
89
+ * compiles so a mid-batch interrupt cannot double-execute non-idempotent
90
+ * siblings on resume. See {@link t.ToolNodeOptions.interruptingToolNames}.
91
+ */
92
+ interruptingToolNames: string[] | undefined;
85
93
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution>;
86
94
  eagerEventToolUsageCount: Map<string, number>;
87
95
  private eagerEventToolUsageCountsByAgentId;
@@ -253,14 +261,24 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
253
261
  dispatchRunStep(stepKey: string, stepDetails: t.StepDetails, metadata?: Record<string, unknown>): Promise<string>;
254
262
  /**
255
263
  * Static version of handleToolCallError to avoid creating strong references
256
- * that prevent garbage collection
264
+ * that prevent garbage collection.
265
+ *
266
+ * Returns whether the error completion event was actually dispatched. A
267
+ * tool can error before this graph instance has a run step for the call —
268
+ * on a resume pass the interrupted batch re-executes IMMEDIATELY on graph
269
+ * re-entry, before any step replay has registered `toolCallStepIds` (a
270
+ * fast-failing tool, e.g. a schema-validation reject, loses that race).
271
+ * That is a caller-recoverable condition, not an invariant violation: the
272
+ * ToolNode falls back to its normal completion dispatch for the error
273
+ * ToolMessage when this returns `false`, so throwing here would only
274
+ * replace a recoverable miss with a lost completion event and a scary log.
257
275
  */
258
- static handleToolCallErrorStatic(graph: StandardGraph, data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<void>;
276
+ static handleToolCallErrorStatic(graph: StandardGraph, data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<boolean>;
259
277
  /**
260
278
  * Instance method that delegates to the static method
261
279
  * Kept for backward compatibility
262
280
  */
263
- handleToolCallError(data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<void>;
281
+ handleToolCallError(data: t.ToolErrorData, metadata?: Record<string, unknown>): Promise<boolean>;
264
282
  dispatchRunStepDelta(id: string, delta: t.ToolCallDelta, metadata?: Record<string, unknown>): Promise<void>;
265
283
  dispatchMessageDelta(id: string, delta: t.MessageDelta, metadata?: Record<string, unknown>): Promise<void>;
266
284
  dispatchReasoningDelta: (stepId: string, delta: t.ReasoningDelta, metadata?: Record<string, unknown>) => Promise<void>;
@@ -14,6 +14,7 @@ export declare class Run<_T extends t.BaseGraphState> {
14
14
  private toolOutputReferences?;
15
15
  private eagerEventToolExecution?;
16
16
  private codeSessionToolNames?;
17
+ private interruptingToolNames?;
17
18
  private toolExecution?;
18
19
  private subagentUsageSink?;
19
20
  private indexTokenCountMap?;
@@ -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;
@@ -67,6 +67,13 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
67
67
  private agentLangfuse?;
68
68
  toolCallStepIds?: Map<string, string>;
69
69
  errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
70
+ /**
71
+ * Tool call ids whose `errorHandler` did NOT dispatch the error completion
72
+ * event (it returned `false` or threw). The output loop must dispatch the
73
+ * completion for these itself — skipping them there would strand the
74
+ * client's tool-call part without a terminal event.
75
+ */
76
+ private undispatchedToolErrors;
70
77
  private toolUsageCount;
71
78
  /** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
72
79
  private toolCallTurns;
@@ -108,6 +115,21 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
108
115
  private executingAgentId?;
109
116
  /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
110
117
  private directToolNames?;
118
+ /**
119
+ * Tool names whose in-process body may raise a LangGraph `interrupt()`
120
+ * mid-execution (e.g. `ask_user_question`). Used only to REORDER within
121
+ * the direct group: a tool named here that is *already* direct (a real
122
+ * in-process graphTool — the only kind whose body can reach
123
+ * `interrupt()`) is scheduled ahead of its non-interrupting direct
124
+ * siblings, so a mid-body interrupt unwinds the ToolNode before a
125
+ * non-idempotent sibling executes and LangGraph's resume-time batch
126
+ * re-execution can't double it. This set is deliberately NOT folded
127
+ * into direct classification: a name that resolves to a schema-only
128
+ * event stub (an inherited `toolDefinition` with no executable
129
+ * instance) stays event-dispatched — forcing it direct would invoke
130
+ * the stub, which throws. See {@link t.ToolNodeOptions.interruptingToolNames}.
131
+ */
132
+ private interruptingToolNames?;
111
133
  /**
112
134
  * File checkpointer extracted from the local coding tool bundle when
113
135
  * `toolExecution.local.fileCheckpointing === true`. Exposed via
@@ -147,7 +169,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
147
169
  * other's in-flight state.
148
170
  */
149
171
  private anonBatchCounter;
150
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
172
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
151
173
  invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
152
174
  /**
153
175
  * Returns the run-scoped tool output registry, or `undefined` when
@@ -314,6 +336,9 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
314
336
  * opt-in so only host-declared tools can influence the shared session.
315
337
  */
316
338
  private participatesInCodeSession;
339
+ /** Delegates to the shared resolver so the direct and event-driven planning
340
+ * paths derive the runtime session hint identically. */
341
+ private resolveRuntimeSessionHint;
317
342
  private storeCodeSessionFromResults;
318
343
  /**
319
344
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
@@ -382,6 +407,40 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
382
407
  * The original role is preserved in additional_kwargs for downstream consumers.
383
408
  */
384
409
  private convertInjectedMessages;
410
+ /**
411
+ * Execute a group of direct (in-process) tool calls with interrupt-safe
412
+ * ordering, returning outputs aligned 1:1 with `directCalls`.
413
+ *
414
+ * Fast path (the common case): when no call in the group is in
415
+ * `interruptingToolNames`, this is a single `Promise.all` — byte-for-byte
416
+ * the prior behavior, so ordinary batches are unaffected.
417
+ *
418
+ * Interrupt-safe path: when the group contains an interrupting tool (e.g.
419
+ * `ask_user_question`, whose body raises a LangGraph `interrupt()` to
420
+ * collect a human answer), the interrupting calls run as their own awaited
421
+ * group **first**; only after they all settle without interrupting do the
422
+ * remaining (potentially non-idempotent) siblings run. If an interrupting
423
+ * call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
424
+ * whole ToolNode *before* any non-interrupting sibling has started — so a
425
+ * sibling with real side effects (send_email, billing) never executes on
426
+ * the first pass. On the resume pass LangGraph re-runs the batch from the
427
+ * top; the interrupting tool resolves with the host's answer instead of
428
+ * throwing, and the siblings execute for the FIRST time, exactly once.
429
+ *
430
+ * Without this ordering, a flat `Promise.all` starts every sibling
431
+ * concurrently, so a non-idempotent sibling can complete its side effect
432
+ * before the interrupt unwinds and then run a SECOND time on resume — the
433
+ * duplicate side effect this method exists to prevent. Interrupting tools
434
+ * are expected to be side-effect-free (they only suspend), so running them
435
+ * as a group and re-running them on resume is harmless.
436
+ *
437
+ * `batchIndices[i]` is `directCalls[i]`'s position within the parent
438
+ * ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
439
+ * preserved regardless of execution order. `baseContext` carries the
440
+ * batch-scoped fields every call shares; `batchIndex` is filled in
441
+ * per-call here.
442
+ */
443
+ private runDirectBatchInterruptSafe;
385
444
  /**
386
445
  * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
387
446
  * Injected messages are placed AFTER ToolMessages to respect provider
@@ -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[];
@@ -134,6 +134,13 @@ export interface AskUserQuestionRequest {
134
134
  * UI allows it. Omit to require a free-form answer.
135
135
  */
136
136
  options?: AskUserQuestionOption[];
137
+ /**
138
+ * When `true`, the host UI may let the user pick several options; the
139
+ * resulting `AskUserQuestionResolution.answer` is the selected option
140
+ * values joined by `", "`. When omitted or `false`, hosts render a
141
+ * single-select picker. Only meaningful alongside `options`.
142
+ */
143
+ multiSelect?: boolean;
137
144
  }
138
145
  /**
139
146
  * Structured payload the SDK passes to `interrupt()` when an agent (or
@@ -157,9 +164,10 @@ export type HumanInterruptPayload = ToolApprovalInterruptPayload | AskUserQuesti
157
164
  export interface AskUserQuestionResolution {
158
165
  /**
159
166
  * The human's answer. Free-form text, or — when `options` were
160
- * provided — one of the option `value`s. Hosts may also send any
161
- * structured object their custom UI defines; see the host docs for
162
- * what your downstream consumer expects.
167
+ * provided — one of the option `value`s (or, when the request set
168
+ * `multiSelect`, several option `value`s joined by `", "`). Hosts may
169
+ * also send any structured object their custom UI defines; see the
170
+ * host docs for what your downstream consumer expects.
163
171
  */
164
172
  answer: string;
165
173
  }
@@ -257,6 +265,44 @@ export declare function isAskUserQuestionInterrupt(payload: unknown): payload is
257
265
  * an interrupt. This applies equally to direct tools (handoffs,
258
266
  * subagents) and to event tools.
259
267
  *
268
+ * ### Guarding non-idempotent siblings via `interruptingToolNames`
269
+ *
270
+ * The "must be idempotent" rule above is unavoidable in the general
271
+ * case, but the SDK can protect siblings against the one interrupt
272
+ * shape it can predict: a tool whose *body* raises `interrupt()`
273
+ * mid-execution — the `ask_user_question` shape, where the tool
274
+ * suspends the run to collect a human answer. Declare such tools in
275
+ * `RunConfig.interruptingToolNames`
276
+ * ({@link ToolNodeOptions.interruptingToolNames}) and the ToolNode
277
+ * schedules them, within each batch, **ahead of** their
278
+ * non-interrupting direct siblings. When one interrupts, the batch
279
+ * unwinds before any declared-safe sibling has run, so the sibling
280
+ * executes exactly once (on resume) instead of twice. Empirically:
281
+ *
282
+ * - A **direct** sibling sharing the interrupter's in-process
283
+ * `Promise.all` is the only shape that double-executes; declaring
284
+ * the interrupter closes it.
285
+ * - An **event-dispatched** sibling is already safe without any
286
+ * config: the ToolNode awaits the whole direct group (where the
287
+ * body interrupt unwinds) before it dispatches event tools, so a
288
+ * dispatched sibling never runs on the first pass.
289
+ *
290
+ * This is a *scheduling* guard, not full resume idempotency: it only
291
+ * covers tools that interrupt from their own body and only protects
292
+ * siblings scheduled after them. It does not retroactively make a
293
+ * `PreToolUse` `'ask'` gate on tool B stop tool A (already executed)
294
+ * from re-running — unless B is itself declared interrupting, so it
295
+ * runs first. Tools with side effects should still be written
296
+ * idempotent as defense in depth.
297
+ *
298
+ * The guard only REORDERS the direct group — declaring a name does not
299
+ * force it onto the direct path. The interrupting tool must already be a
300
+ * real in-process graphTool (the only kind whose body can reach
301
+ * `interrupt()`). A name that resolves to a schema-only event stub (an
302
+ * inherited `toolDefinition` with no executable instance, e.g. in a
303
+ * self-spawned child that scrubs `graphTools`) stays event-dispatched
304
+ * and the ordering is a no-op for it.
305
+ *
260
306
  * ## Note on idempotency
261
307
  *
262
308
  * Same root cause as the resume re-execution above: LangGraph
@@ -179,6 +179,27 @@ export type RunConfig = {
179
179
  * the SDK stays name-agnostic.
180
180
  */
181
181
  codeSessionToolNames?: string[];
182
+ /**
183
+ * Names of host tools whose in-process body may raise a LangGraph
184
+ * `interrupt()` mid-execution — the canonical example is an
185
+ * `ask_user_question` tool that suspends the run to collect a human
186
+ * answer. Within a single tool-call batch, a named tool that is a real
187
+ * in-process graphTool (the only kind whose body can reach `interrupt()`;
188
+ * graphTools are auto-marked direct) is scheduled **ahead of** its
189
+ * non-interrupting direct siblings. That ordering guarantees a mid-body
190
+ * interrupt unwinds the tool batch before a non-idempotent sibling
191
+ * (send_email, billing) executes, so the sibling cannot run once on the
192
+ * first pass and AGAIN when LangGraph re-runs the interrupted batch on
193
+ * resume.
194
+ *
195
+ * This only reorders the direct group — it does NOT force a name onto
196
+ * the direct path. A name that is only an inherited event `toolDefinition`
197
+ * (schema-only stub, e.g. in a self-spawned child) stays event-dispatched;
198
+ * the guard applies only to tools that are independently direct.
199
+ * Host-declared so the SDK stays name-agnostic; omit to keep the prior
200
+ * (unguarded) behavior.
201
+ */
202
+ interruptingToolNames?: string[];
182
203
  /**
183
204
  * Selects the execution backend for built-in code tools. Omit this to keep
184
205
  * the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
@@ -82,7 +82,15 @@ export type ToolNodeOptions = {
82
82
  handleToolErrors?: boolean;
83
83
  loadRuntimeTools?: ToolRefGenerator;
84
84
  toolCallStepIds?: Map<string, string>;
85
- errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => Promise<void>;
85
+ /**
86
+ * Dispatches the error completion event for a failed tool call. Returns
87
+ * whether the event was actually dispatched — `false` (e.g. no run step is
88
+ * registered for the call yet, which happens when a tool fails fast on a
89
+ * resume pass) tells the ToolNode to fall back to its own completion
90
+ * dispatch for the error ToolMessage. A `void` resolution is treated as
91
+ * dispatched for backward compatibility.
92
+ */
93
+ errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => Promise<boolean | void>;
86
94
  /** Tool registry for lazy computation of programmatic tools and tool search */
87
95
  toolRegistry?: LCToolRegistry;
88
96
  /** Reference to Graph's sessions map for automatic session injection */
@@ -98,6 +106,34 @@ export type ToolNodeOptions = {
98
106
  executingAgentId?: string;
99
107
  /** Tool names that must be executed directly (via runTool) even in event-driven mode (e.g., graph-managed handoff tools) */
100
108
  directToolNames?: Set<string>;
109
+ /**
110
+ * Tool names whose in-process body may raise a LangGraph `interrupt()`
111
+ * mid-execution — e.g. an `ask_user_question` tool that suspends the
112
+ * run to collect a human answer.
113
+ *
114
+ * Within a single tool-call batch, a named tool that is *already*
115
+ * direct (a real in-process graphTool — the only kind whose body can
116
+ * reach `interrupt()`; graphTools are auto-marked direct by the graph)
117
+ * is executed as its own awaited group **before** its non-interrupting
118
+ * direct siblings. If it interrupts, the ToolNode unwinds before any
119
+ * sibling runs, so a non-idempotent sibling (send_email, billing)
120
+ * cannot execute once on the first pass and AGAIN when LangGraph re-runs
121
+ * the interrupted batch on resume.
122
+ *
123
+ * This set only REORDERS the direct group; it does NOT promote a name
124
+ * into it. A name that resolves to a schema-only event stub (an
125
+ * inherited `toolDefinition` with no executable instance — e.g. in a
126
+ * self-spawned child that scrubs `graphTools`) stays event-dispatched.
127
+ * Forcing such a name direct would invoke the stub, which throws
128
+ * "should not be invoked directly in event-driven mode". For the guard
129
+ * to apply, the interrupting tool must independently be direct.
130
+ *
131
+ * Opt-in and empty by default: when unset (or when no direct batch call
132
+ * matches), direct-batch execution is byte-for-byte unchanged. See the
133
+ * "Resume re-execution" section of {@link HumanInTheLoopConfig} for the
134
+ * batch re-execution contract this guards against.
135
+ */
136
+ interruptingToolNames?: Set<string>;
101
137
  /** Opt-in eager execution for event-driven tool calls. */
102
138
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
103
139
  /**
@@ -278,6 +314,15 @@ export type CodeExecutionToolParams = undefined | {
278
314
  files?: CodeEnvFile[];
279
315
  /** Optional host-supplied Code API auth headers. */
280
316
  authHeaders?: CodeApiAuthHeaders;
317
+ /**
318
+ * Advertise best-effort stateful sessions in the tool description
319
+ * (variables/files may persist between calls, may reset). Prompt text
320
+ * only, and it must be set here because the description is bound to the
321
+ * LLM at construction time. Pair it with the run-scoped
322
+ * `toolExecution.sandbox.statefulSessions` gate, which drives the wire
323
+ * hint — set both from one flag so the prompt and the backend agree.
324
+ */
325
+ statefulSessions?: boolean;
281
326
  };
282
327
  export type CodeApiAuthHeaderMap = Record<string, string>;
283
328
  export type CodeApiAuthHeaders = CodeApiAuthHeaderMap | (() => CodeApiAuthHeaderMap | Promise<CodeApiAuthHeaderMap>);
@@ -326,6 +371,13 @@ export type ExecuteResult = {
326
371
  stdout: string;
327
372
  stderr: string;
328
373
  files?: FileRefs;
374
+ /**
375
+ * Durable runtime session id echoed by a stateful Code API backend
376
+ * (hash of tenant+user+hint). Additive; absent on stateless servers.
377
+ */
378
+ runtime_session_id?: string;
379
+ /** Whether this execution reused a warm runtime session or started fresh. */
380
+ runtime_status?: 'new' | 'reused';
329
381
  };
330
382
  /** JSON Schema type definition for tool parameters */
331
383
  export type JsonSchemaType = {
@@ -381,6 +433,12 @@ export type ToolCallRequest = {
381
433
  session_id: string;
382
434
  files?: CodeEnvFile[];
383
435
  };
436
+ /**
437
+ * Stable runtime session hint for stateful sandbox sessions. Orthogonal to
438
+ * `codeSessionContext` (which threads the transient exec-session for file
439
+ * continuity): the hint identifies the durable server-side runtime session.
440
+ */
441
+ runtimeSessionHint?: string;
384
442
  };
385
443
  /** Batch request containing ALL tool calls for a graph step */
386
444
  export type ToolExecuteBatchRequest = {
@@ -896,6 +954,31 @@ export type CloudflareSandboxExecutionConfig = {
896
954
  /** Run a fast per-file syntax check after successful edits/writes. */
897
955
  postEditSyntaxCheck?: LocalExecutionConfig['postEditSyntaxCheck'];
898
956
  };
957
+ export type SandboxExecutionConfig = {
958
+ /**
959
+ * Opt into best-effort stateful runtime sessions on the remote Code API
960
+ * (its warm per-session MicroVM backend). This gate is run-scoped: it only
961
+ * controls the wire behavior (ToolNode injecting the session hint on
962
+ * execute_code/bash calls). The transport is otherwise unchanged.
963
+ *
964
+ * It does NOT change the model-facing tool description. Tool descriptions are
965
+ * bound to the LLM at construction time (`createCodeExecutionTool` /
966
+ * `createBashExecutionTool`), before this run config is applied inside the
967
+ * graph, so they can only be adjusted via the tools' own `statefulSessions`
968
+ * factory param. Set BOTH from one flag (as LibreChat does): with this on but
969
+ * the factory param off, the backend runs statefully while the model is still
970
+ * told the environment is stateless (non-corrupting — the model just won't
971
+ * exploit persistence).
972
+ */
973
+ statefulSessions?: boolean;
974
+ /**
975
+ * Stable identity for the runtime session (e.g. the conversation id). The
976
+ * server derives the real session id as hash(tenant, user, hint), so this
977
+ * is never a security boundary. Falls back to `configurable.thread_id` when
978
+ * omitted.
979
+ */
980
+ runtimeSessionHint?: string;
981
+ };
899
982
  export type ToolExecutionConfig = {
900
983
  /** `sandbox` preserves the remote Code API behavior and is the default. */
901
984
  engine?: ToolExecutionEngine;
@@ -903,6 +986,8 @@ export type ToolExecutionConfig = {
903
986
  local?: LocalExecutionConfig;
904
987
  /** Cloudflare Sandbox execution settings used when `engine` is `cloudflare-sandbox`. */
905
988
  cloudflare?: CloudflareSandboxExecutionConfig;
989
+ /** Remote sandbox settings; applies when `engine` is `sandbox` or omitted. */
990
+ sandbox?: SandboxExecutionConfig;
906
991
  };
907
992
  export type ProgrammaticCache = {
908
993
  toolMap: ToolMap;
@@ -1005,6 +1090,9 @@ export type ProgrammaticExecutionResponse = {
1005
1090
  stdout?: string;
1006
1091
  stderr?: string;
1007
1092
  files?: FileRefs;
1093
+ /** Durable runtime session echo from a stateful backend (additive). */
1094
+ runtime_session_id?: string;
1095
+ runtime_status?: 'new' | 'reused';
1008
1096
  /** Present when status='error' */
1009
1097
  error?: string;
1010
1098
  };
@@ -1015,6 +1103,9 @@ export type ProgrammaticExecutionArtifact = {
1015
1103
  /** Execution session — see `CodeSessionContext.session_id`. */
1016
1104
  session_id?: string;
1017
1105
  files?: FileRefs;
1106
+ /** Durable runtime session echo from a stateful backend (additive). */
1107
+ runtime_session_id?: string;
1108
+ runtime_status?: 'new' | 'reused';
1018
1109
  };
1019
1110
  /** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
1020
1111
  export type BashExecutionToolParams = CodeExecutionToolParams;
@@ -1062,6 +1153,9 @@ export type CodeExecutionArtifact = {
1062
1153
  /** Execution session — see `CodeSessionContext.session_id`. */
1063
1154
  session_id?: string;
1064
1155
  files?: FileRefs;
1156
+ /** Durable runtime session echo from a stateful backend (additive). */
1157
+ runtime_session_id?: string;
1158
+ runtime_status?: 'new' | 'reused';
1065
1159
  };
1066
1160
  /**
1067
1161
  * 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.60",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",