@librechat/agents 3.8.1 → 3.8.3

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 (96) hide show
  1. package/dist/cjs/graphs/Graph.cjs +2 -2
  2. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  3. package/dist/cjs/hitl/approvalReview.cjs +89 -0
  4. package/dist/cjs/hitl/approvalReview.cjs.map +1 -0
  5. package/dist/cjs/hooks/index.cjs +2 -0
  6. package/dist/cjs/hooks/index.cjs.map +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -1
  9. package/dist/cjs/main.cjs +5 -3
  10. package/dist/cjs/messages/core.cjs +1 -1
  11. package/dist/cjs/messages/format.cjs +1 -1
  12. package/dist/cjs/messages/prune.cjs +2 -2
  13. package/dist/cjs/run.cjs +17 -6
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/session/AgentSession.cjs +1 -1
  16. package/dist/cjs/stream.cjs +2 -2
  17. package/dist/cjs/summarization/node.cjs +1 -1
  18. package/dist/cjs/tools/BashExecutor.cjs +2 -3
  19. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  20. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +3 -2
  21. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  22. package/dist/cjs/tools/CodeExecutor.cjs +63 -11
  23. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +8 -7
  25. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  26. package/dist/cjs/tools/ToolNode.cjs +271 -84
  27. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  30. package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  32. package/dist/cjs/tools/toolBatchReplay.cjs +198 -0
  33. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
  34. package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
  35. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  36. package/dist/cjs/types/hitl.cjs +4 -0
  37. package/dist/cjs/types/hitl.cjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +2 -2
  39. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  40. package/dist/esm/hitl/approvalReview.mjs +83 -0
  41. package/dist/esm/hitl/approvalReview.mjs.map +1 -0
  42. package/dist/esm/hooks/index.mjs +2 -1
  43. package/dist/esm/hooks/index.mjs.map +1 -1
  44. package/dist/esm/hooks/types.mjs.map +1 -1
  45. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
  46. package/dist/esm/main.mjs +6 -6
  47. package/dist/esm/messages/core.mjs +1 -1
  48. package/dist/esm/messages/format.mjs +1 -1
  49. package/dist/esm/messages/prune.mjs +2 -2
  50. package/dist/esm/run.mjs +18 -7
  51. package/dist/esm/run.mjs.map +1 -1
  52. package/dist/esm/session/AgentSession.mjs +1 -1
  53. package/dist/esm/stream.mjs +2 -2
  54. package/dist/esm/summarization/node.mjs +1 -1
  55. package/dist/esm/tools/BashExecutor.mjs +2 -3
  56. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  57. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -2
  58. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  59. package/dist/esm/tools/CodeExecutor.mjs +63 -12
  60. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  61. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -7
  62. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  63. package/dist/esm/tools/ToolNode.mjs +271 -84
  64. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  65. package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
  66. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  67. package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
  68. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  69. package/dist/esm/tools/toolBatchReplay.mjs +186 -0
  70. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
  71. package/dist/esm/tools/toolOutputReferences.mjs +12 -0
  72. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  73. package/dist/esm/types/hitl.mjs +4 -1
  74. package/dist/esm/types/hitl.mjs.map +1 -1
  75. package/dist/types/hitl/approvalReview.d.ts +32 -0
  76. package/dist/types/hooks/index.d.ts +2 -0
  77. package/dist/types/hooks/types.d.ts +6 -0
  78. package/dist/types/tools/CodeExecutor.d.ts +2 -0
  79. package/dist/types/tools/ToolNode.d.ts +12 -6
  80. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
  81. package/dist/types/tools/toolBatchReplay.d.ts +63 -0
  82. package/dist/types/tools/toolOutputReferences.d.ts +2 -0
  83. package/package.json +1 -1
  84. package/src/hitl/approvalReview.ts +209 -0
  85. package/src/hooks/index.ts +3 -0
  86. package/src/hooks/types.ts +6 -0
  87. package/src/run.ts +34 -20
  88. package/src/tools/BashExecutor.ts +2 -2
  89. package/src/tools/BashProgrammaticToolCalling.ts +5 -3
  90. package/src/tools/CodeExecutor.ts +111 -17
  91. package/src/tools/ProgrammaticToolCalling.ts +10 -8
  92. package/src/tools/ToolNode.ts +536 -184
  93. package/src/tools/subagent/SubagentExecutor.ts +35 -6
  94. package/src/tools/subagent/SubagentReplay.ts +2 -2
  95. package/src/tools/toolBatchReplay.ts +423 -0
  96. package/src/tools/toolOutputReferences.ts +16 -0
@@ -0,0 +1,209 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type {
3
+ ToolApprovalInterruptPayload,
4
+ ToolApprovalRequest,
5
+ ToolApprovalReviewConfig,
6
+ } from '@/types/hitl';
7
+ import { stableStringify } from '@/tools/eagerEventExecution';
8
+ import { isToolApprovalInterrupt } from '@/types/hitl';
9
+
10
+ /**
11
+ * Private resume-only config entry populated from the checkpointed interrupt.
12
+ * Hosts must never need to construct or inspect this value.
13
+ */
14
+ export const TOOL_APPROVAL_REVIEW_CONFIG_KEY =
15
+ '__librechat_tool_approval_review';
16
+
17
+ export interface ToolApprovalReviewEvidence {
18
+ interruptId: string;
19
+ payload: ToolApprovalInterruptPayload;
20
+ owner?: string;
21
+ }
22
+
23
+ export interface ReviewedToolApproval {
24
+ request: ToolApprovalRequest;
25
+ reviewConfig: ToolApprovalReviewConfig;
26
+ }
27
+
28
+ const APPROVAL_DECISIONS = new Set(['approve', 'reject', 'edit', 'respond']);
29
+
30
+ /** Detach approval payloads from host- or transport-owned object graphs. */
31
+ export function cloneToolApprovalInterruptPayload<T>(payload: T): T {
32
+ return isToolApprovalInterrupt(payload) ? structuredClone(payload) : payload;
33
+ }
34
+
35
+ function isRecord(value: unknown): value is Record<string, unknown> {
36
+ return value != null && typeof value === 'object' && !Array.isArray(value);
37
+ }
38
+
39
+ function hasValidApprovalShape(payload: ToolApprovalInterruptPayload): boolean {
40
+ if (
41
+ !Array.isArray(payload.action_requests) ||
42
+ !Array.isArray(payload.review_configs) ||
43
+ payload.action_requests.length !== payload.review_configs.length
44
+ ) {
45
+ return false;
46
+ }
47
+
48
+ const toolCallIds = new Set<string>();
49
+ return payload.action_requests.every((request, index) => {
50
+ const reviewConfig = payload.review_configs[index];
51
+ if (!isRecord(request) || !isRecord(reviewConfig)) {
52
+ return false;
53
+ }
54
+ const toolCallId = request.tool_call_id;
55
+ const toolName = request.name;
56
+ const allowedDecisions = reviewConfig.allowed_decisions;
57
+ if (
58
+ typeof toolCallId !== 'string' ||
59
+ toolCallId.length === 0 ||
60
+ toolCallIds.has(toolCallId) ||
61
+ typeof toolName !== 'string' ||
62
+ toolName.length === 0 ||
63
+ !isRecord(request.arguments) ||
64
+ (request.description != null &&
65
+ typeof request.description !== 'string') ||
66
+ reviewConfig.tool_call_id !== toolCallId ||
67
+ reviewConfig.action_name !== toolName ||
68
+ !Array.isArray(allowedDecisions) ||
69
+ !allowedDecisions.every(
70
+ (decision) =>
71
+ typeof decision === 'string' && APPROVAL_DECISIONS.has(decision)
72
+ )
73
+ ) {
74
+ return false;
75
+ }
76
+ toolCallIds.add(toolCallId);
77
+ return true;
78
+ });
79
+ }
80
+
81
+ /** Build trusted review evidence from the interrupt restored by `Run`. */
82
+ export function createToolApprovalReviewEvidence(
83
+ interruptId: string | undefined,
84
+ payload: unknown,
85
+ owner?: string
86
+ ): ToolApprovalReviewEvidence | undefined {
87
+ if (
88
+ typeof interruptId !== 'string' ||
89
+ interruptId.length === 0 ||
90
+ !isToolApprovalInterrupt(payload) ||
91
+ !hasValidApprovalShape(payload)
92
+ ) {
93
+ return undefined;
94
+ }
95
+ return {
96
+ interruptId,
97
+ payload: cloneToolApprovalInterruptPayload(payload),
98
+ ...(owner == null ? {} : { owner }),
99
+ };
100
+ }
101
+
102
+ /** Read only well-shaped evidence from a ToolNode's runnable config. */
103
+ export function getToolApprovalReviewEvidence(
104
+ config: RunnableConfig,
105
+ owner?: string
106
+ ): ToolApprovalReviewEvidence | undefined {
107
+ const candidate = config.configurable?.[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
108
+ if (candidate == null || typeof candidate !== 'object') {
109
+ return undefined;
110
+ }
111
+ const {
112
+ interruptId,
113
+ payload,
114
+ owner: evidenceOwner,
115
+ } = candidate as {
116
+ interruptId?: unknown;
117
+ payload?: unknown;
118
+ owner?: unknown;
119
+ };
120
+ if (
121
+ evidenceOwner != null &&
122
+ (typeof evidenceOwner !== 'string' ||
123
+ (owner != null && owner !== evidenceOwner))
124
+ ) {
125
+ return undefined;
126
+ }
127
+ return createToolApprovalReviewEvidence(
128
+ typeof interruptId === 'string' ? interruptId : undefined,
129
+ payload,
130
+ typeof evidenceOwner === 'string' ? evidenceOwner : undefined
131
+ );
132
+ }
133
+
134
+ export function getReviewedToolApproval(
135
+ payload: ToolApprovalInterruptPayload | undefined,
136
+ toolCallId: string | undefined
137
+ ): ReviewedToolApproval | undefined {
138
+ if (payload == null || toolCallId == null || toolCallId === '') {
139
+ return undefined;
140
+ }
141
+ const request = payload.action_requests.find(
142
+ (candidate) => candidate.tool_call_id === toolCallId
143
+ );
144
+ const reviewConfig = payload.review_configs.find(
145
+ (candidate) => candidate.tool_call_id === toolCallId
146
+ );
147
+ if (request == null || reviewConfig == null) {
148
+ return undefined;
149
+ }
150
+ return { request, reviewConfig };
151
+ }
152
+
153
+ function decisionsEqual(
154
+ left: ReadonlyArray<string>,
155
+ right: ReadonlyArray<string>
156
+ ): boolean {
157
+ if (left.length !== right.length) {
158
+ return false;
159
+ }
160
+ const sortedLeft = [...left].sort();
161
+ const sortedRight = [...right].sort();
162
+ return sortedLeft.every((value, index) => value === sortedRight[index]);
163
+ }
164
+
165
+ /**
166
+ * Bind a resume decision to the exact proposal the reviewer saw. Description
167
+ * text is deliberately excluded: it explains policy but cannot change the
168
+ * side effect. Tool identity, normalized arguments and available decisions
169
+ * are execution-authoritative.
170
+ */
171
+ export function toolApprovalProposalMatches(
172
+ current: ReviewedToolApproval,
173
+ reviewed: ReviewedToolApproval
174
+ ): boolean {
175
+ return (
176
+ current.request.tool_call_id === reviewed.request.tool_call_id &&
177
+ current.request.name === reviewed.request.name &&
178
+ current.reviewConfig.tool_call_id === reviewed.reviewConfig.tool_call_id &&
179
+ current.reviewConfig.action_name === reviewed.reviewConfig.action_name &&
180
+ stableStringify(current.request.arguments) ===
181
+ stableStringify(reviewed.request.arguments) &&
182
+ decisionsEqual(
183
+ current.reviewConfig.allowed_decisions,
184
+ reviewed.reviewConfig.allowed_decisions
185
+ )
186
+ );
187
+ }
188
+
189
+ /** Preserve batch order as part of the decision-to-request binding. */
190
+ export function toolApprovalPayloadMatches(
191
+ current: ToolApprovalInterruptPayload,
192
+ reviewed: ToolApprovalInterruptPayload
193
+ ): boolean {
194
+ if (
195
+ current.action_requests.length !== reviewed.action_requests.length ||
196
+ current.review_configs.length !== reviewed.review_configs.length
197
+ ) {
198
+ return false;
199
+ }
200
+ return current.action_requests.every((request, index) => {
201
+ const currentReviewConfig = current.review_configs[index];
202
+ const reviewedRequest = reviewed.action_requests[index];
203
+ const reviewedReviewConfig = reviewed.review_configs[index];
204
+ return toolApprovalProposalMatches(
205
+ { request, reviewConfig: currentReviewConfig },
206
+ { request: reviewedRequest, reviewConfig: reviewedReviewConfig }
207
+ );
208
+ });
209
+ }
@@ -116,3 +116,6 @@ export type {
116
116
  PostCompactHookOutput,
117
117
  } from './types';
118
118
  export type { ExecuteHooksOptions } from './executeHooks';
119
+
120
+ /** Hosts may opt into stable generation scopes across every ToolNode interrupt type. */
121
+ export const TOOL_APPROVAL_EXECUTION_SCOPE_CAPABLE = true;
@@ -28,6 +28,12 @@ export const HOOK_EVENTS = [
28
28
  'PostCompact',
29
29
  ] as const;
30
30
 
31
+ /**
32
+ * Host-owned generation identity, stable across every resume and rebuilt Run.
33
+ * A new generation must use a new scope, even when it reuses a thread or response
34
+ * id. Explicit scoping keeps LangGraph task namespaces out of approval owners;
35
+ * the executing agent id remains part of the owner and cannot change on resume.
36
+ */
31
37
  export const TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY =
32
38
  '__librechat_tool_approval_execution_scope';
33
39
 
package/src/run.ts CHANGED
@@ -22,7 +22,6 @@ import type {
22
22
  } from '@langchain/core/messages';
23
23
  import type { StringPromptValue } from '@langchain/core/prompt_values';
24
24
  import type { RunnableConfig } from '@langchain/core/runnables';
25
- import { isFadingTier } from '@/messages/fading';
26
25
  import type { AggregatedHookResult, HookRegistry } from '@/hooks';
27
26
  import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
28
27
  import type { StandardGraph } from '@/graphs/Graph';
@@ -40,7 +39,6 @@ import {
40
39
  } from '@/common';
41
40
  import {
42
41
  requireValidSubagentResumeManifest,
43
- stripSubagentResumeManifest,
44
42
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
45
43
  SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
46
44
  } from '@/tools/subagent/SubagentReplay';
@@ -70,6 +68,16 @@ import {
70
68
  resolveLangfuseConfig,
71
69
  resolveToolOutputTracingConfig,
72
70
  } from '@/langfuseConfig';
71
+ import {
72
+ cloneToolApprovalInterruptPayload,
73
+ TOOL_APPROVAL_REVIEW_CONFIG_KEY,
74
+ } from '@/hitl/approvalReview';
75
+ import {
76
+ TOOL_BATCH_REPLAY_KEY,
77
+ restoreToolReplayConfig,
78
+ stripToolBatchReplayState,
79
+ getPublicToolInterruptPayload,
80
+ } from '@/tools/toolBatchReplay';
73
81
  import {
74
82
  resolveLangfuseDestinationKey,
75
83
  resolveLangfuseTraceAnchorParent,
@@ -104,6 +112,7 @@ import { seedRunInitialSessions } from '@/utils/toolSessions';
104
112
  import { getTraceIdSeed } from '@/langfuseRuntimeContext';
105
113
  import { resolveClientOptionsModel } from '@/llm/request';
106
114
  import { createGraph } from '@/graphs/createGraph';
115
+ import { isFadingTier } from '@/messages/fading';
107
116
  import { resolveMaxSeals } from '@/llm/preempt';
108
117
  import { isBuiltRuntime } from '@/lazyRequire';
109
118
  import { initializeModel } from '@/llm/init';
@@ -169,9 +178,7 @@ function materializeStopContinuation(
169
178
  return messages;
170
179
  }
171
180
 
172
- function advanceCheckpointCursor(
173
- config: t.RunStreamConfig
174
- ): t.RunStreamConfig {
181
+ function advanceCheckpointCursor(config: t.RunStreamConfig): t.RunStreamConfig {
175
182
  const configurable = { ...config.configurable };
176
183
  delete configurable.checkpoint_id;
177
184
  delete configurable.checkpoint_map;
@@ -186,9 +193,7 @@ function assertFinalAdmissionSucceeded(result: AggregatedHookResult): void {
186
193
  result.errors.length > 0
187
194
  ? result.errors.join('; ')
188
195
  : 'one or more internal finalizers failed';
189
- throw new Error(
190
- `StopFinalize terminal admission failed: ${detail}`
191
- );
196
+ throw new Error(`StopFinalize terminal admission failed: ${detail}`);
192
197
  }
193
198
 
194
199
  const CUSTOM_GRAPH_EVENTS = new Set<string>([
@@ -268,9 +273,7 @@ function isLangGraphResumeMapForInterrupt(
268
273
  }
269
274
 
270
275
  function getInterruptHookSessionId(payload: unknown): string | undefined {
271
- const publicPayload = stripSubagentResumeManifest(
272
- stripRunStepResumeState(payload)
273
- );
276
+ const publicPayload = getPublicToolInterruptPayload(payload);
274
277
  if (
275
278
  publicPayload == null ||
276
279
  typeof publicPayload !== 'object' ||
@@ -423,7 +426,7 @@ function overwriteResumeRunStepState(
423
426
  const update = Array.isArray(command.update)
424
427
  ? [
425
428
  ...command.update.filter(([key]) => key !== 'runStepState'),
426
- ['runStepState', overwrite] as [string, unknown],
429
+ ['runStepState', overwrite] as [string, unknown],
427
430
  ]
428
431
  : { ...(command.update ?? {}), runStepState: overwrite };
429
432
  const resumed = new Command({
@@ -1266,6 +1269,8 @@ export class Run<_T extends t.BaseGraphState> {
1266
1269
  recursionLimit,
1267
1270
  configurable: { ...callerConfig.configurable },
1268
1271
  };
1272
+ delete config.configurable?.[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
1273
+ delete config.configurable?.[TOOL_BATCH_REPLAY_KEY];
1269
1274
  if (!isResume) {
1270
1275
  delete config.configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
1271
1276
  delete config.configurable?.[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
@@ -1276,6 +1281,10 @@ export class Run<_T extends t.BaseGraphState> {
1276
1281
  config,
1277
1282
  (inputs as Command).update
1278
1283
  );
1284
+ const publicPayload = stripRunStepResumeState(this._interrupt?.payload);
1285
+ if (config.configurable != null) {
1286
+ restoreToolReplayConfig(config.configurable, this._interrupt?.interruptId, publicPayload);
1287
+ }
1279
1288
  if (graph.getStopContinuationExecutionId() === '') {
1280
1289
  graph.startStopContinuationExecution(nanoid());
1281
1290
  overwriteLegacyResumeState = this.hasCheckpointer;
@@ -1514,7 +1523,7 @@ export class Run<_T extends t.BaseGraphState> {
1514
1523
  this._interrupt = {
1515
1524
  interruptId: first.id ?? '',
1516
1525
  threadId,
1517
- payload: first.value,
1526
+ payload: cloneToolApprovalInterruptPayload(first.value),
1518
1527
  };
1519
1528
  }
1520
1529
  }
@@ -1558,7 +1567,8 @@ export class Run<_T extends t.BaseGraphState> {
1558
1567
  stopReason = OUTPUT_TRUNCATED_HALT_REASON;
1559
1568
  }
1560
1569
 
1561
- const stopMessages = graph.getRunMessages() ?? stateInputs?.messages ?? [];
1570
+ const stopMessages =
1571
+ graph.getRunMessages() ?? stateInputs?.messages ?? [];
1562
1572
  const stopContinuationCount = graph.getStopContinuationCount();
1563
1573
  const continuationBudgetRemaining = Math.max(
1564
1574
  0,
@@ -1881,8 +1891,8 @@ export class Run<_T extends t.BaseGraphState> {
1881
1891
  }
1882
1892
  return {
1883
1893
  ...this._interrupt,
1884
- payload: stripSubagentResumeManifest(
1885
- stripRunStepResumeState(this._interrupt.payload)
1894
+ payload: cloneToolApprovalInterruptPayload(
1895
+ getPublicToolInterruptPayload(this._interrupt.payload)
1886
1896
  ),
1887
1897
  } as t.RunInterruptResult<TPayload>;
1888
1898
  }
@@ -2004,16 +2014,20 @@ export class Run<_T extends t.BaseGraphState> {
2004
2014
  ): Promise<t.RunStreamConfig> {
2005
2015
  await this.restoreInterruptFromCheckpoint(callerConfig, resumeUpdate);
2006
2016
  const interrupt = this._interrupt;
2007
- const resumeManifest = requireValidSubagentResumeManifest(
2008
- stripRunStepResumeState(interrupt?.payload)
2009
- );
2017
+ const replayPayload = stripRunStepResumeState(interrupt?.payload);
2018
+ const resumeManifest = requireValidSubagentResumeManifest(replayPayload) ??
2019
+ requireValidSubagentResumeManifest(stripToolBatchReplayState(replayPayload));
2010
2020
  const resumeConfigurable = { ...callerConfig.configurable };
2021
+ delete resumeConfigurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
2022
+ delete resumeConfigurable[TOOL_BATCH_REPLAY_KEY];
2011
2023
  delete resumeConfigurable[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
2012
2024
  delete resumeConfigurable[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY];
2013
2025
  resumeConfigurable[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY] = nanoid();
2014
2026
  if (resumeManifest != null) {
2015
2027
  resumeConfigurable[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY] = resumeManifest;
2016
2028
  }
2029
+ const publicPayload = stripRunStepResumeState(interrupt?.payload);
2030
+ restoreToolReplayConfig(resumeConfigurable, interrupt?.interruptId, publicPayload);
2017
2031
  const manifestConfig = {
2018
2032
  ...callerConfig,
2019
2033
  configurable: resumeConfigurable,
@@ -2130,7 +2144,7 @@ export class Run<_T extends t.BaseGraphState> {
2130
2144
  const threadId = callerConfig.configurable?.thread_id;
2131
2145
  this._interrupt = {
2132
2146
  interruptId: persistedInterrupt.id,
2133
- payload: persistedInterrupt.value,
2147
+ payload: cloneToolApprovalInterruptPayload(persistedInterrupt.value),
2134
2148
  ...(typeof threadId === 'string' ? { threadId } : {}),
2135
2149
  ...(typeof checkpointId === 'string' ? { checkpointId } : {}),
2136
2150
  ...(typeof checkpointNs === 'string' ? { checkpointNs } : {}),
@@ -279,7 +279,7 @@ function createBashExecutionTool(
279
279
  };
280
280
 
281
281
  const proxyAgent = resolveFetchProxyAgent(execEndpoint);
282
- if (proxyAgent) {
282
+ if (proxyAgent != null) {
283
283
  fetchOptions.agent = proxyAgent;
284
284
  }
285
285
  const response = await fetch(execEndpoint, fetchOptions);
@@ -343,7 +343,7 @@ function createBashExecutionTool(
343
343
  normalizeCodeApiRequestError(error).message,
344
344
  command
345
345
  );
346
- throw new Error(`Execution error:\n\n${messageWithReminder}`);
346
+ throw new CodeApiRequestError(`Execution error:\n\n${messageWithReminder}`);
347
347
  }
348
348
  },
349
349
  {
@@ -542,9 +542,11 @@ export function createBashProgrammaticToolCallingTool(
542
542
  (error as Error).message,
543
543
  code
544
544
  );
545
- throw new Error(
546
- `Bash programmatic execution failed: ${messageWithReminder}`
547
- );
545
+ const message = `Bash programmatic execution failed: ${messageWithReminder}`;
546
+ if (error instanceof CodeApiRequestError) {
547
+ throw new CodeApiRequestError(message);
548
+ }
549
+ throw new Error(message, { cause: error });
548
550
  }
549
551
  },
550
552
  {
@@ -70,6 +70,7 @@ export function appendFailedExecutionFileReminder(
70
70
  code: string
71
71
  ): string {
72
72
  if (
73
+ output.includes(CODE_API_CAPABILITY_ERROR_MESSAGE) ||
73
74
  !MNT_DATA_PATH_PATTERN.test(code) ||
74
75
  output.includes(FAILED_EXECUTION_FILE_REMINDER)
75
76
  ) {
@@ -157,10 +158,67 @@ const SAFE_CODE_API_EXECUTION_ERROR_DETAILS: Readonly<
157
158
  'stdout length exceeded': 'Execution output exceeded the size limit.',
158
159
  };
159
160
 
161
+ export const CODE_API_CAPABILITY_ERROR_MESSAGE =
162
+ 'Code execution is not supported by the selected environment.';
163
+ const CODE_API_CAPABILITY_ERROR_GUIDANCE =
164
+ 'This is a permanent capability mismatch. Do not retry this tool in this environment; select a compatible environment or use an available workspace tool.';
165
+
166
+ const CODE_API_CAPABILITY_LIMITATIONS: Readonly<
167
+ Partial<Record<string, string>>
168
+ > = {
169
+ bridge_worker_mismatch:
170
+ 'The selected worker does not support the requested execution capabilities (such as the stateful workspace required by programmatic tool calling).',
171
+ execution_profile_mismatch:
172
+ 'The selected environment does not provide the requested execution profile.',
173
+ capability_mismatch:
174
+ 'The selected environment lacks a required execution capability.',
175
+ unsupported_capability:
176
+ 'The selected environment lacks a required execution capability.',
177
+ stateful_workspace_unsupported:
178
+ 'The selected worker does not provide a stateful workspace.',
179
+ };
180
+
181
+ function getCodeApiCapabilityErrorMessage(
182
+ responseBody: string
183
+ ): string | undefined {
184
+ try {
185
+ const parsed = JSON.parse(responseBody) as {
186
+ error?: string;
187
+ code?: string;
188
+ message?: string;
189
+ } | null;
190
+ const code = parsed?.code ?? parsed?.error;
191
+ if (typeof code !== 'string') return undefined;
192
+ const normalizedCode = code.toLowerCase();
193
+ if (!Object.hasOwn(CODE_API_CAPABILITY_LIMITATIONS, normalizedCode))
194
+ return undefined;
195
+ let limitation = CODE_API_CAPABILITY_LIMITATIONS[normalizedCode];
196
+ if (limitation == null) return undefined;
197
+ if (
198
+ normalizedCode === 'bridge_worker_mismatch' &&
199
+ typeof parsed?.message === 'string' &&
200
+ /^Bridge worker [A-Za-z0-9._:-]+ does not provide a stateful workspace$/.test(
201
+ parsed.message
202
+ )
203
+ ) {
204
+ limitation =
205
+ CODE_API_CAPABILITY_LIMITATIONS.stateful_workspace_unsupported;
206
+ }
207
+ return `${CODE_API_CAPABILITY_ERROR_MESSAGE} ${limitation} ${CODE_API_CAPABILITY_ERROR_GUIDANCE}`;
208
+ } catch {
209
+ return undefined;
210
+ }
211
+ }
212
+
160
213
  export class CodeApiRequestError extends Error {
214
+ readonly retryable: boolean;
215
+
161
216
  constructor(message = CODE_API_UNAVAILABLE_ERROR_MESSAGE) {
162
217
  super(message);
163
218
  this.name = 'CodeApiRequestError';
219
+ this.retryable =
220
+ message.includes(CODE_API_UNAVAILABLE_ERROR_MESSAGE) ||
221
+ message.includes('Code execution is temporarily rate-limited.');
164
222
  }
165
223
  }
166
224
 
@@ -277,11 +335,7 @@ type CodeApiErrorResponse = {
277
335
  body?: NodeJS.ReadableStream | null;
278
336
  };
279
337
 
280
- /**
281
- * Only the 429 branch reads the body, and node-fetch keeps the stream and its
282
- * socket alive until something does. Repeated backend failures would otherwise
283
- * accumulate connections holding payloads this module deliberately discards.
284
- */
338
+ /** Releases unread or incomplete error responses after bounded classification. */
285
339
  function discardResponseBody(response: CodeApiErrorResponse): void {
286
340
  const body = response.body;
287
341
  if (body == null || !('destroy' in body)) {
@@ -298,6 +352,48 @@ function discardResponseBody(response: CodeApiErrorResponse): void {
298
352
  }
299
353
  }
300
354
 
355
+ const MAX_CODE_API_ERROR_BODY_BYTES = 64 * 1024;
356
+ const CODE_API_ERROR_BODY_TIMEOUT_MS = 1_000;
357
+
358
+ /** Reads only small error envelopes, with a deadline for stalled upstreams. */
359
+ async function readCodeApiErrorBody(
360
+ response: CodeApiErrorResponse
361
+ ): Promise<string> {
362
+ let timer: ReturnType<typeof setTimeout> | undefined;
363
+ try {
364
+ const read = async (): Promise<string> => {
365
+ if (response.body != null && Symbol.asyncIterator in response.body) {
366
+ let bytes = 0;
367
+ const chunks: Buffer[] = [];
368
+ for await (const chunk of response.body as AsyncIterable<
369
+ Buffer | string
370
+ >) {
371
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
372
+ bytes += buffer.length;
373
+ if (bytes > MAX_CODE_API_ERROR_BODY_BYTES) return '';
374
+ chunks.push(buffer);
375
+ }
376
+ return Buffer.concat(chunks).toString('utf8');
377
+ }
378
+ const body = await response.text();
379
+ return Buffer.byteLength(body) <= MAX_CODE_API_ERROR_BODY_BYTES
380
+ ? body
381
+ : '';
382
+ };
383
+ return await Promise.race([
384
+ read(),
385
+ new Promise<string>((resolve) => {
386
+ timer = setTimeout(() => resolve(''), CODE_API_ERROR_BODY_TIMEOUT_MS);
387
+ }),
388
+ ]);
389
+ } catch {
390
+ return '';
391
+ } finally {
392
+ clearTimeout(timer);
393
+ discardResponseBody(response);
394
+ }
395
+ }
396
+
301
397
  export async function buildCodeApiHttpErrorMessage(
302
398
  method: CodeApiMethod,
303
399
  _endpoint: string,
@@ -305,8 +401,8 @@ export async function buildCodeApiHttpErrorMessage(
305
401
  options?: { recoverable?: boolean; profile?: t.CodeApiExecutionProfile }
306
402
  ): Promise<string> {
307
403
  /* Logged before the body is touched. A non-OK response can leave a chunked
308
- body open, and there is no read timeout here, so draining first would
309
- withhold the diagnostic indefinitely for exactly the backend it identifies.
404
+ body open, so logging first preserves the diagnostic even if the bounded
405
+ classification read times out.
310
406
  The body itself is never logged — it is upstream free text that can echo
311
407
  the header that was sent — and the endpoint is host-configured, so the
312
408
  backend is named by the profile this module chose rather than by its
@@ -325,16 +421,12 @@ export async function buildCodeApiHttpErrorMessage(
325
421
  }
326
422
  );
327
423
  }
328
- if (response.status !== 429) {
329
- discardResponseBody(response);
424
+ const responseBody = await readCodeApiErrorBody(response);
425
+ const capabilityError = getCodeApiCapabilityErrorMessage(responseBody);
426
+ if (capabilityError != null) {
427
+ return capabilityError;
330
428
  }
331
429
  if (response.status === 429) {
332
- let responseBody = '';
333
- try {
334
- responseBody = await response.text();
335
- } catch {
336
- responseBody = '';
337
- }
338
430
  const retryAfterSeconds = getRetryAfterSeconds(responseBody);
339
431
  return retryAfterSeconds != null
340
432
  ? `Code execution is temporarily rate-limited. Retry after ${retryAfterSeconds} seconds.`
@@ -543,7 +635,7 @@ function createCodeExecutionTool(
543
635
  };
544
636
 
545
637
  const proxyAgent = resolveFetchProxyAgent(execEndpoint);
546
- if (proxyAgent) {
638
+ if (proxyAgent != null) {
547
639
  fetchOptions.agent = proxyAgent;
548
640
  }
549
641
  const response = await fetch(execEndpoint, fetchOptions);
@@ -610,7 +702,9 @@ function createCodeExecutionTool(
610
702
  normalizeCodeApiRequestError(error).message,
611
703
  code
612
704
  );
613
- throw new Error(`Execution error:\n\n${messageWithReminder}`);
705
+ throw new CodeApiRequestError(
706
+ `Execution error:\n\n${messageWithReminder}`
707
+ );
614
708
  }
615
709
  },
616
710
  {
@@ -469,7 +469,7 @@ export async function fetchSessionFiles(
469
469
  };
470
470
 
471
471
  const proxyAgent = resolveFetchProxyAgent(filesEndpoint, proxy);
472
- if (proxyAgent) {
472
+ if (proxyAgent != null) {
473
473
  fetchOptions.agent = proxyAgent;
474
474
  }
475
475
 
@@ -531,7 +531,7 @@ export async function makeRequest(
531
531
  };
532
532
 
533
533
  const proxyAgent = resolveFetchProxyAgent(endpoint, proxy);
534
- if (proxyAgent) {
534
+ if (proxyAgent != null) {
535
535
  fetchOptions.agent = proxyAgent;
536
536
  }
537
537
 
@@ -689,7 +689,7 @@ type ToolInputSchemaKind = {
689
689
  function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
690
690
  const kind: ToolInputSchemaKind = { object: false, string: false };
691
691
 
692
- if (!schema || typeof schema !== 'object') {
692
+ if (schema == null || typeof schema !== 'object') {
693
693
  return kind;
694
694
  }
695
695
 
@@ -704,7 +704,7 @@ function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
704
704
  }
705
705
 
706
706
  const zodDef = (schema as { _def?: unknown })._def;
707
- if (!zodDef || typeof zodDef !== 'object') {
707
+ if (zodDef == null || typeof zodDef !== 'object') {
708
708
  return kind;
709
709
  }
710
710
 
@@ -726,7 +726,7 @@ function detectSchemaKind(schema: unknown): ToolInputSchemaKind {
726
726
  type?: unknown;
727
727
  }
728
728
  ).innerType ?? (zodDef as { schema?: unknown }).schema;
729
- if (innerSchema) {
729
+ if (innerSchema != null) {
730
730
  const innerKind = detectSchemaKind(innerSchema);
731
731
  kind.object ||= innerKind.object;
732
732
  kind.string ||= innerKind.string;
@@ -1221,9 +1221,11 @@ export function createProgrammaticToolCallingTool(
1221
1221
  (error as Error).message,
1222
1222
  code
1223
1223
  );
1224
- throw new Error(
1225
- `Programmatic execution failed: ${messageWithReminder}`
1226
- );
1224
+ const message = `Programmatic execution failed: ${messageWithReminder}`;
1225
+ if (error instanceof CodeApiRequestError) {
1226
+ throw new CodeApiRequestError(message);
1227
+ }
1228
+ throw new Error(message, { cause: error });
1227
1229
  }
1228
1230
  },
1229
1231
  {