@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
@@ -88,6 +88,11 @@ import type {
88
88
  ToolApprovalReplaySnapshot,
89
89
  } from '@/hooks';
90
90
  import type { GraphFactory } from '@/graphs/graphFactory';
91
+ import {
92
+ rebindToolBatchReplayScope,
93
+ rebindToolBatchReplayPayload,
94
+ restoreToolReplayConfig,
95
+ } from '@/tools/toolBatchReplay';
91
96
  import type { StandardGraph } from '@/graphs/Graph';
92
97
  import {
93
98
  getSubagentApprovalExecutionScope,
@@ -1448,7 +1453,8 @@ export class SubagentExecutor {
1448
1453
  await this.prepareCheckpointFork(
1449
1454
  resumeExecution.checkpoints,
1450
1455
  branchChildThreadId,
1451
- lease
1456
+ lease,
1457
+ { source: resumeExecution.approvalExecutionScope, target: approvalExecutionScope }
1452
1458
  );
1453
1459
  let previousApprovals: ToolApprovalReplaySnapshot[] = [];
1454
1460
  let approvalsMutated = false;
@@ -1589,7 +1595,8 @@ export class SubagentExecutor {
1589
1595
  private async prepareCheckpointFork(
1590
1596
  sources: ReadonlyArray<SubagentCheckpointReference>,
1591
1597
  targetThreadId: string,
1592
- lease: SubagentExecutionPreparationLease
1598
+ lease: SubagentExecutionPreparationLease,
1599
+ approvalScope?: { source: string; target: string }
1593
1600
  ): Promise<void> {
1594
1601
  const checkpointer = this.checkpointer;
1595
1602
  try {
@@ -1608,7 +1615,7 @@ export class SubagentExecutor {
1608
1615
  checkpointer.deleteThread(targetThreadId)
1609
1616
  );
1610
1617
  }
1611
- await this.forkCheckpointSnapshot(sources, targetThreadId);
1618
+ await this.forkCheckpointSnapshot(sources, targetThreadId, approvalScope);
1612
1619
  lease.assertOwned();
1613
1620
  } catch (error) {
1614
1621
  return lease.rollback(error);
@@ -1663,7 +1670,8 @@ export class SubagentExecutor {
1663
1670
  /** Copies exact checkpoint lineages, including pending task writes. */
1664
1671
  private async forkCheckpointSnapshot(
1665
1672
  sources: ReadonlyArray<SubagentCheckpointReference>,
1666
- targetThreadId: string
1673
+ targetThreadId: string,
1674
+ approvalScope?: { source: string; target: string }
1667
1675
  ): Promise<void> {
1668
1676
  if (
1669
1677
  this.checkpointer == null ||
@@ -1755,7 +1763,15 @@ export class SubagentExecutor {
1755
1763
  writes = [];
1756
1764
  writesByTask.set(taskId, writes);
1757
1765
  }
1758
- writes.push([channel, value]);
1766
+ /** Repeated unconsumed forks must persist their current owner. */
1767
+ const reboundValue =
1768
+ channel === INTERRUPT && approvalScope != null &&
1769
+ value != null && typeof value === 'object' && 'value' in value
1770
+ ? { ...value, value: rebindToolBatchReplayPayload(
1771
+ value.value, approvalScope.source, approvalScope.target
1772
+ ) }
1773
+ : value;
1774
+ writes.push([channel, reboundValue]);
1759
1775
  }
1760
1776
  for (const [taskId, writes] of writesByTask) {
1761
1777
  await this.checkpointer.putWrites(storedConfig, writes, taskId);
@@ -2647,7 +2663,11 @@ export class SubagentExecutor {
2647
2663
  }
2648
2664
  const persistedInterrupts = getPersistedInterrupts(persistedState);
2649
2665
  if (persistedInterrupts.length > 0) {
2650
- activeChildRun.pendingInterrupts = persistedInterrupts;
2666
+ activeChildRun.pendingInterrupts = resumeExecution == null ? persistedInterrupts :
2667
+ persistedInterrupts.map((pending) => ({
2668
+ ...pending,
2669
+ value: rebindToolBatchReplayPayload(pending.value, resumeExecution.approvalExecutionScope, approvalExecutionScope),
2670
+ }));
2651
2671
  execution.markStarted();
2652
2672
  childAlreadyStarted = true;
2653
2673
  } else if (persistedState.next.length > 0) {
@@ -2682,6 +2702,15 @@ export class SubagentExecutor {
2682
2702
  }
2683
2703
  let childInput: BaseGraphState | Command | null;
2684
2704
  if (childResumeMap != null) {
2705
+ const pending = activeChildRun.pendingInterrupts.find((entry) =>
2706
+ entry.id != null && Object.prototype.hasOwnProperty.call(childResumeMap, entry.id)
2707
+ ) ?? activeChildRun.pendingInterrupts[0];
2708
+ restoreToolReplayConfig(childConfigurable, pending.id, pending.value);
2709
+ rebindToolBatchReplayScope(
2710
+ childConfigurable,
2711
+ resumeExecution?.approvalExecutionScope ?? approvalExecutionScope,
2712
+ approvalExecutionScope
2713
+ );
2685
2714
  childInput = new Command({ resume: childResumeMap });
2686
2715
  } else if (recoveredInProgress) {
2687
2716
  childInput = null;
@@ -201,7 +201,7 @@ function isDirectPathTurnReference(
201
201
  return isString(entry.toolCallId) && isNonnegativeInteger(entry.turn);
202
202
  }
203
203
 
204
- function isToolNodeResumeState(
204
+ export function isToolNodeResumeState(
205
205
  value: unknown
206
206
  ): value is SubagentToolNodeResumeState {
207
207
  if (value == null || typeof value !== 'object') {
@@ -266,7 +266,7 @@ function isToolOutputReferenceEntry(
266
266
  return isString(entry.key) && typeof entry.value === 'string';
267
267
  }
268
268
 
269
- function isToolOutputReferenceState(
269
+ export function isToolOutputReferenceState(
270
270
  value: unknown
271
271
  ): value is ToolOutputReferenceState {
272
272
  if (value == null || typeof value !== 'object') {
@@ -0,0 +1,423 @@
1
+ import { Command, MemorySaver, isCommand } from '@langchain/langgraph';
2
+ import { isBaseMessage } from '@langchain/core/messages';
3
+ import type { BaseMessage } from '@langchain/core/messages';
4
+ import type { RunnableConfig } from '@langchain/core/runnables';
5
+ import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
6
+ import type { ToolOutputReferenceState } from '@/tools/toolOutputReferences';
7
+ import {
8
+ isToolNodeResumeState,
9
+ isToolOutputReferenceState,
10
+ stripSubagentResumeManifest,
11
+ requireValidSubagentResumeManifest,
12
+ } from '@/tools/subagent/SubagentReplay';
13
+ import { isToolApprovalInterrupt } from '@/types/hitl';
14
+ import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from '@/hooks/types';
15
+ import {
16
+ TOOL_APPROVAL_REVIEW_CONFIG_KEY,
17
+ getToolApprovalReviewEvidence,
18
+ createToolApprovalReviewEvidence,
19
+ } from '@/hitl/approvalReview';
20
+ import {
21
+ attachRunStepResumeState,
22
+ getRunStepResumeState,
23
+ stripRunStepResumeState,
24
+ } from '@/tools/runStepResume';
25
+
26
+ export const TOOL_BATCH_REPLAY_KEY = '__librechat_tool_batch_replay';
27
+ const TOOL_BATCH_PAYLOAD_KEY = '__librechat_tool_batch_payload';
28
+ const TOOL_BATCH_WRAPPER_KEY = '__librechat_tool_batch_wrapper';
29
+
30
+ export interface SettledToolBatchResult {
31
+ proposal: { name: string; args: Record<string, unknown> };
32
+ output: BaseMessage | Command;
33
+ additionalContexts: string[];
34
+ resolvedArgs?: Record<string, unknown>;
35
+ completionHandled?: boolean;
36
+ referenceContent?: string;
37
+ turn?: number;
38
+ }
39
+
40
+ export interface ToolBatchReplayRecord {
41
+ owner: string;
42
+ batch: string;
43
+ encoding: string;
44
+ data: string;
45
+ turnState?: SubagentToolNodeResumeState;
46
+ referenceState?: ToolOutputReferenceState;
47
+ }
48
+
49
+ interface ToolBatchReplayState {
50
+ version: 1;
51
+ approvalOwner?: string;
52
+ /** Populated from the pending interrupt by Run, including question pauses. */
53
+ interruptId?: string;
54
+ records: ToolBatchReplayRecord[];
55
+ wrappedPayload?: boolean;
56
+ }
57
+
58
+ /** Use the same message/Command codec as LangGraph checkpoints. */
59
+ const serializer = new MemorySaver().serde;
60
+
61
+ function isRecord(value: unknown): value is Record<string, unknown> {
62
+ return value != null && typeof value === 'object' && !Array.isArray(value);
63
+ }
64
+
65
+ /** The only host-to-runtime entry point for checkpoint-owned tool replay state. */
66
+ export function restoreToolReplayConfig(
67
+ configurable: Record<string, unknown>,
68
+ interruptId: string | undefined,
69
+ payload: unknown
70
+ ): void {
71
+ delete configurable[TOOL_BATCH_REPLAY_KEY];
72
+ delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
73
+ const state = getToolBatchReplayState(payload);
74
+ const publicPayload = getPublicToolInterruptPayload(payload);
75
+ const review = createToolApprovalReviewEvidence(
76
+ interruptId,
77
+ publicPayload,
78
+ state?.approvalOwner
79
+ );
80
+ if (isToolApprovalInterrupt(publicPayload) &&
81
+ (review == null || (state != null && state.approvalOwner == null))) {
82
+ throw new Error('Invalid tool approval checkpoint');
83
+ }
84
+ if (state != null) {
85
+ configurable[TOOL_BATCH_REPLAY_KEY] = { ...state, interruptId };
86
+ }
87
+ if (review != null) {
88
+ configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = review;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Resume maps identify the checkpointed interrupt, while the task scratchpad
94
+ * identifies the node that can consume it. Config survives into later graph
95
+ * steps, so the presence of replay evidence alone does not imply a resume.
96
+ * A parent replaying a checkpointed child has no local resume value.
97
+ */
98
+ export function isToolReplayResume(
99
+ config: RunnableConfig,
100
+ interruptId: string | undefined,
101
+ isChildReplay: boolean
102
+ ): boolean {
103
+ const resumeMap: object | undefined = config.configurable?.__pregel_resume_map;
104
+ if (interruptId != null && resumeMap != null &&
105
+ !Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
106
+ return false;
107
+ }
108
+ if (isChildReplay && resumeMap != null) {
109
+ return true;
110
+ }
111
+ const scratchpad: { resume?: { length: number }; nullResume?: unknown } | undefined =
112
+ config.configurable?.__pregel_scratchpad;
113
+ return scratchpad == null ||
114
+ (scratchpad.resume?.length ?? 0) > 0 || scratchpad.nullResume !== undefined;
115
+ }
116
+
117
+ export function getToolBatchReplayOwner(
118
+ config: RunnableConfig,
119
+ agentId: string
120
+ ): string {
121
+ return JSON.stringify([
122
+ getToolBatchReplayScope(config),
123
+ config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] == null
124
+ ? (config.configurable?.checkpoint_ns ?? '')
125
+ : '',
126
+ agentId,
127
+ ]);
128
+ }
129
+
130
+ export function getToolBatchReplayScope(
131
+ config: RunnableConfig
132
+ ): string | undefined {
133
+ const scope =
134
+ config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] ??
135
+ config.configurable?.thread_id;
136
+ return typeof scope === 'string' && scope.length > 0 ? scope : undefined;
137
+ }
138
+
139
+ /** Foreign replay evidence may only transit a checkpoint-proven child path. */
140
+ export function isChildToolReplayOwner(
141
+ config: RunnableConfig,
142
+ owner: string,
143
+ trustedParentCallIds: ReadonlySet<string>
144
+ ): boolean {
145
+ const manifest = requireValidSubagentResumeManifest(config.configurable);
146
+ if (manifest == null || trustedParentCallIds.size === 0) return false;
147
+ const identity: unknown = JSON.parse(owner);
148
+ if (!Array.isArray(identity) || identity.length !== 3 || identity[1] !== '') return false;
149
+ const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
150
+ while (pending.length > 0) {
151
+ const execution = pending.pop()!;
152
+ if (execution.approvalExecutionScope === identity[0]) return true;
153
+ pending.push(...(execution.descendant?.executions ?? []));
154
+ }
155
+ return false;
156
+ }
157
+
158
+ /** Rebind only the checkpoint-proven source execution when a child fork is created. */
159
+ export function rebindToolBatchReplayScope(
160
+ configurable: Record<string, unknown>,
161
+ sourceScope: string,
162
+ destinationScope: string
163
+ ): void {
164
+ const rebind = (key: string): string => {
165
+ const parts: unknown = JSON.parse(key);
166
+ if (!Array.isArray(parts) || parts[0] !== sourceScope) {
167
+ return key;
168
+ }
169
+ return JSON.stringify([destinationScope, ...parts.slice(1)]);
170
+ };
171
+ const config = { configurable };
172
+ const review = getToolApprovalReviewEvidence(config);
173
+ if (review?.owner != null) {
174
+ configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = {
175
+ ...review,
176
+ owner: rebind(review.owner),
177
+ };
178
+ }
179
+ const state = getToolBatchReplayState(configurable);
180
+ if (state != null) {
181
+ configurable[TOOL_BATCH_REPLAY_KEY] = {
182
+ ...state,
183
+ approvalOwner:
184
+ state.approvalOwner == null ? undefined : rebind(state.approvalOwner),
185
+ records: state.records.map((record) => ({
186
+ ...record,
187
+ owner: rebind(record.owner),
188
+ batch: rebind(record.batch),
189
+ })),
190
+ };
191
+ }
192
+ }
193
+
194
+ /** A fork may be paused again before it consumes its pending interrupt. */
195
+ export function rebindToolBatchReplayPayload(
196
+ payload: unknown,
197
+ sourceScope: string,
198
+ destinationScope: string
199
+ ): unknown {
200
+ const state = getToolBatchReplayState(payload);
201
+ const value = stripRunStepResumeState(payload);
202
+ if (state == null || !isRecord(value)) {
203
+ return payload;
204
+ }
205
+ const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
206
+ rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
207
+ const rebound = {
208
+ ...value,
209
+ [TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],
210
+ };
211
+ const runStepState = getRunStepResumeState(payload);
212
+ return runStepState == null
213
+ ? rebound
214
+ : attachRunStepResumeState(rebound, runStepState);
215
+ }
216
+
217
+ export function getToolBatchReplayState(
218
+ payload: unknown
219
+ ): ToolBatchReplayState | undefined {
220
+ const value = stripRunStepResumeState(payload);
221
+ if (
222
+ value == null ||
223
+ typeof value !== 'object' ||
224
+ !(TOOL_BATCH_REPLAY_KEY in value)
225
+ ) {
226
+ return undefined;
227
+ }
228
+ const candidate: unknown = value[TOOL_BATCH_REPLAY_KEY];
229
+ if (candidate === undefined) {
230
+ return undefined;
231
+ }
232
+ const state = candidate as Partial<ToolBatchReplayState> | null;
233
+ if (
234
+ state?.version !== 1 ||
235
+ (state.wrappedPayload != null &&
236
+ typeof state.wrappedPayload !== 'boolean') ||
237
+ (state.approvalOwner != null && typeof state.approvalOwner !== 'string') ||
238
+ (state.interruptId != null && typeof state.interruptId !== 'string') ||
239
+ !Array.isArray(state.records) ||
240
+ state.records.some((value: unknown) => {
241
+ if (value == null || typeof value !== 'object') {
242
+ return true;
243
+ }
244
+ const record = value as Partial<ToolBatchReplayRecord>;
245
+ return (
246
+ typeof record.owner !== 'string' ||
247
+ typeof record.batch !== 'string' ||
248
+ typeof record.encoding !== 'string' ||
249
+ typeof record.data !== 'string' ||
250
+ (record.turnState != null &&
251
+ !isToolNodeResumeState(record.turnState)) ||
252
+ (record.referenceState != null &&
253
+ !isToolOutputReferenceState(record.referenceState))
254
+ );
255
+ })
256
+ ) {
257
+ throw new Error('Invalid tool batch replay checkpoint');
258
+ }
259
+ return structuredClone(state as ToolBatchReplayState);
260
+ }
261
+
262
+ export function stripToolBatchReplayState(payload: unknown): unknown {
263
+ if (
264
+ payload == null ||
265
+ typeof payload !== 'object' ||
266
+ !(TOOL_BATCH_REPLAY_KEY in payload)
267
+ ) {
268
+ return payload;
269
+ }
270
+ const state = getToolBatchReplayState(payload);
271
+ const { [TOOL_BATCH_REPLAY_KEY]: _state, ...publicPayload } = payload;
272
+ if (
273
+ state?.wrappedPayload === true &&
274
+ TOOL_BATCH_PAYLOAD_KEY in publicPayload
275
+ ) {
276
+ return publicPayload[TOOL_BATCH_PAYLOAD_KEY];
277
+ }
278
+ return publicPayload;
279
+ }
280
+
281
+ /** Decode outer execution metadata before shape-sensitive child wrappers. */
282
+ export function getPublicToolInterruptPayload(payload: unknown): unknown {
283
+ return stripSubagentResumeManifest(
284
+ stripToolBatchReplayState(stripRunStepResumeState(payload))
285
+ );
286
+ }
287
+
288
+ export async function attachToolBatchReplayState(
289
+ payload: unknown,
290
+ owner: string,
291
+ batches: ReadonlyMap<string, ReadonlyMap<string, object>>,
292
+ turnState?: SubagentToolNodeResumeState,
293
+ referenceState?: ToolOutputReferenceState
294
+ ): Promise<unknown> {
295
+ const publicPayload = stripRunStepResumeState(payload);
296
+ const previous = getToolBatchReplayState(publicPayload);
297
+ const records =
298
+ previous?.records.filter((record) => record.owner !== owner) ?? [];
299
+ for (const [batch, results] of batches) {
300
+ const [encoding, bytes] = await serializer.dumpsTyped([...results]);
301
+ records.push({
302
+ owner,
303
+ batch,
304
+ encoding,
305
+ data: Buffer.from(bytes).toString('base64'),
306
+ ...(turnState == null ? {} : { turnState }),
307
+ ...(referenceState == null ? {} : { referenceState }),
308
+ });
309
+ }
310
+ const approvalOwner =
311
+ previous?.approvalOwner ??
312
+ (isToolApprovalInterrupt(publicPayload) ? owner : undefined);
313
+ if (records.length === 0 && approvalOwner == null) {
314
+ return payload;
315
+ }
316
+ const result = {
317
+ ...(isRecord(publicPayload)
318
+ ? publicPayload
319
+ : {
320
+ [TOOL_BATCH_WRAPPER_KEY]: 1,
321
+ [TOOL_BATCH_PAYLOAD_KEY]: publicPayload,
322
+ }),
323
+ [TOOL_BATCH_REPLAY_KEY]: {
324
+ version: 1,
325
+ approvalOwner,
326
+ records,
327
+ wrappedPayload: previous?.wrappedPayload ?? !isRecord(publicPayload),
328
+ } satisfies ToolBatchReplayState,
329
+ };
330
+ const runStepState = getRunStepResumeState(payload);
331
+ return runStepState == null
332
+ ? result
333
+ : attachRunStepResumeState(result, runStepState);
334
+ }
335
+
336
+ export async function restoreToolBatchReplayState(
337
+ config: RunnableConfig,
338
+ owner: string
339
+ ): Promise<
340
+ Array<{
341
+ batch: string;
342
+ results: Array<[string, SettledToolBatchResult]>;
343
+ turnState?: SubagentToolNodeResumeState;
344
+ referenceState?: ToolOutputReferenceState;
345
+ }>
346
+ > {
347
+ const state = getToolBatchReplayState(config.configurable);
348
+ if (state == null) {
349
+ return [];
350
+ }
351
+ return Promise.all(
352
+ state.records
353
+ .filter((record) => record.owner === owner)
354
+ .map(async (record) => {
355
+ const values: unknown = await serializer.loadsTyped(
356
+ record.encoding,
357
+ Buffer.from(record.data, 'base64')
358
+ );
359
+ if (!Array.isArray(values)) {
360
+ throw new Error('Invalid settled tool batch results');
361
+ }
362
+ const seen = new Set<string>();
363
+ const results = values.map(
364
+ (entry: unknown): [string, SettledToolBatchResult] => {
365
+ if (
366
+ !Array.isArray(entry) ||
367
+ entry.length !== 2 ||
368
+ typeof entry[0] !== 'string' ||
369
+ seen.has(entry[0])
370
+ ) {
371
+ throw new Error('Invalid settled tool batch results');
372
+ }
373
+ const result = entry[1] as Partial<SettledToolBatchResult> | null;
374
+ if (
375
+ result == null ||
376
+ (!isBaseMessage(result.output) && !isCommand(result.output)) ||
377
+ !Array.isArray(result.additionalContexts) ||
378
+ result.additionalContexts.some(
379
+ (context) => typeof context !== 'string'
380
+ ) ||
381
+ (result.completionHandled != null &&
382
+ typeof result.completionHandled !== 'boolean') ||
383
+ (result.referenceContent != null &&
384
+ typeof result.referenceContent !== 'string') ||
385
+ (result.turn != null &&
386
+ (!Number.isSafeInteger(result.turn) || result.turn < 0)) ||
387
+ !isRecord(result.proposal) ||
388
+ typeof result.proposal.name !== 'string' ||
389
+ result.proposal.name.length === 0 ||
390
+ !isRecord(result.proposal.args) ||
391
+ (result.resolvedArgs != null && !isRecord(result.resolvedArgs))
392
+ ) {
393
+ throw new Error('Invalid settled tool batch results');
394
+ }
395
+ seen.add(entry[0]);
396
+ const output = isCommand(result.output)
397
+ ? new Command({
398
+ update: result.output.update,
399
+ goto: result.output.goto,
400
+ graph: result.output.graph,
401
+ resume: result.output.resume,
402
+ })
403
+ : result.output;
404
+ return [
405
+ entry[0],
406
+ {
407
+ ...result,
408
+ proposal: result.proposal,
409
+ output,
410
+ additionalContexts: result.additionalContexts,
411
+ },
412
+ ];
413
+ }
414
+ );
415
+ return {
416
+ batch: record.batch,
417
+ results,
418
+ turnState: record.turnState,
419
+ referenceState: record.referenceState,
420
+ };
421
+ })
422
+ );
423
+ }
@@ -420,6 +420,22 @@ export class ToolOutputReferenceRegistry {
420
420
  };
421
421
  }
422
422
 
423
+ /** Restore batch inputs without rolling back graph-owned concurrent outputs. */
424
+ resumeBatch(runId: string, state: ToolOutputReferenceState): ToolOutputResolveView {
425
+ if (!this.runStates.has(runId)) {
426
+ this.restoreState(runId, state);
427
+ }
428
+ const bucket = this.getOrCreate(runId);
429
+ bucket.turnCounter = Math.max(bucket.turnCounter, state.turnCounter + 1);
430
+ const inputs = new ToolOutputReferenceRegistry({
431
+ maxOutputSize: this.maxOutputSize,
432
+ maxTotalSize: this.maxTotalSize,
433
+ maxActiveRuns: 1,
434
+ });
435
+ inputs.restoreState(runId, state);
436
+ return inputs.snapshot(runId);
437
+ }
438
+
423
439
  /** Restores a checkpointed run bucket under the current resume scope. */
424
440
  restoreState(
425
441
  runId: string | undefined,