@librechat/agents 3.8.1 → 3.8.2

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 (65) 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/llm/anthropic/utils/message_inputs.cjs +1 -1
  6. package/dist/cjs/main.cjs +3 -3
  7. package/dist/cjs/messages/core.cjs +1 -1
  8. package/dist/cjs/messages/format.cjs +1 -1
  9. package/dist/cjs/messages/prune.cjs +2 -2
  10. package/dist/cjs/run.cjs +17 -6
  11. package/dist/cjs/run.cjs.map +1 -1
  12. package/dist/cjs/session/AgentSession.cjs +1 -1
  13. package/dist/cjs/stream.cjs +2 -2
  14. package/dist/cjs/summarization/node.cjs +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +256 -84
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +24 -9
  18. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/subagent/SubagentReplay.cjs +2 -0
  20. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  21. package/dist/cjs/tools/toolBatchReplay.cjs +187 -0
  22. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -0
  23. package/dist/cjs/tools/toolOutputReferences.cjs +12 -0
  24. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  25. package/dist/cjs/types/hitl.cjs +4 -0
  26. package/dist/cjs/types/hitl.cjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +2 -2
  28. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  29. package/dist/esm/hitl/approvalReview.mjs +83 -0
  30. package/dist/esm/hitl/approvalReview.mjs.map +1 -0
  31. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -1
  32. package/dist/esm/main.mjs +3 -3
  33. package/dist/esm/messages/core.mjs +1 -1
  34. package/dist/esm/messages/format.mjs +1 -1
  35. package/dist/esm/messages/prune.mjs +2 -2
  36. package/dist/esm/run.mjs +18 -7
  37. package/dist/esm/run.mjs.map +1 -1
  38. package/dist/esm/session/AgentSession.mjs +1 -1
  39. package/dist/esm/stream.mjs +2 -2
  40. package/dist/esm/summarization/node.mjs +1 -1
  41. package/dist/esm/tools/ToolNode.mjs +256 -84
  42. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  43. package/dist/esm/tools/subagent/SubagentExecutor.mjs +24 -9
  44. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  45. package/dist/esm/tools/subagent/SubagentReplay.mjs +1 -1
  46. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  47. package/dist/esm/tools/toolBatchReplay.mjs +176 -0
  48. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -0
  49. package/dist/esm/tools/toolOutputReferences.mjs +12 -0
  50. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  51. package/dist/esm/types/hitl.mjs +4 -1
  52. package/dist/esm/types/hitl.mjs.map +1 -1
  53. package/dist/types/hitl/approvalReview.d.ts +32 -0
  54. package/dist/types/tools/ToolNode.d.ts +12 -6
  55. package/dist/types/tools/subagent/SubagentReplay.d.ts +2 -0
  56. package/dist/types/tools/toolBatchReplay.d.ts +54 -0
  57. package/dist/types/tools/toolOutputReferences.d.ts +2 -0
  58. package/package.json +1 -1
  59. package/src/hitl/approvalReview.ts +209 -0
  60. package/src/run.ts +34 -20
  61. package/src/tools/ToolNode.ts +518 -184
  62. package/src/tools/subagent/SubagentExecutor.ts +35 -6
  63. package/src/tools/subagent/SubagentReplay.ts +2 -2
  64. package/src/tools/toolBatchReplay.ts +395 -0
  65. 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,395 @@
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
+ records: ToolBatchReplayRecord[];
53
+ wrappedPayload?: boolean;
54
+ }
55
+
56
+ /** Use the same message/Command codec as LangGraph checkpoints. */
57
+ const serializer = new MemorySaver().serde;
58
+
59
+ function isRecord(value: unknown): value is Record<string, unknown> {
60
+ return value != null && typeof value === 'object' && !Array.isArray(value);
61
+ }
62
+
63
+ /** The only host-to-runtime entry point for checkpoint-owned tool replay state. */
64
+ export function restoreToolReplayConfig(
65
+ configurable: Record<string, unknown>,
66
+ interruptId: string | undefined,
67
+ payload: unknown
68
+ ): void {
69
+ delete configurable[TOOL_BATCH_REPLAY_KEY];
70
+ delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
71
+ const state = getToolBatchReplayState(payload);
72
+ const publicPayload = getPublicToolInterruptPayload(payload);
73
+ const review = createToolApprovalReviewEvidence(
74
+ interruptId,
75
+ publicPayload,
76
+ state?.approvalOwner
77
+ );
78
+ if (isToolApprovalInterrupt(publicPayload) &&
79
+ (review == null || (state != null && state.approvalOwner == null))) {
80
+ throw new Error('Invalid tool approval checkpoint');
81
+ }
82
+ if (state != null) {
83
+ configurable[TOOL_BATCH_REPLAY_KEY] = state;
84
+ }
85
+ if (review != null) {
86
+ configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = review;
87
+ }
88
+ }
89
+
90
+ export function getToolBatchReplayOwner(
91
+ config: RunnableConfig,
92
+ agentId: string
93
+ ): string {
94
+ return JSON.stringify([
95
+ getToolBatchReplayScope(config),
96
+ config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] == null
97
+ ? (config.configurable?.checkpoint_ns ?? '')
98
+ : '',
99
+ agentId,
100
+ ]);
101
+ }
102
+
103
+ export function getToolBatchReplayScope(
104
+ config: RunnableConfig
105
+ ): string | undefined {
106
+ const scope =
107
+ config.configurable?.[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] ??
108
+ config.configurable?.thread_id;
109
+ return typeof scope === 'string' && scope.length > 0 ? scope : undefined;
110
+ }
111
+
112
+ /** Foreign approval evidence may only transit a checkpoint-proven child path. */
113
+ export function isChildToolApprovalOwner(
114
+ config: RunnableConfig,
115
+ owner: string,
116
+ trustedParentCallIds: ReadonlySet<string>
117
+ ): boolean {
118
+ const manifest = requireValidSubagentResumeManifest(config.configurable);
119
+ if (manifest == null || trustedParentCallIds.size === 0) return false;
120
+ const identity: unknown = JSON.parse(owner);
121
+ if (!Array.isArray(identity) || identity.length !== 3 || identity[1] !== '') return false;
122
+ const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
123
+ while (pending.length > 0) {
124
+ const execution = pending.pop()!;
125
+ if (execution.approvalExecutionScope === identity[0]) return true;
126
+ pending.push(...(execution.descendant?.executions ?? []));
127
+ }
128
+ return false;
129
+ }
130
+
131
+ /** Rebind only the checkpoint-proven source execution when a child fork is created. */
132
+ export function rebindToolBatchReplayScope(
133
+ configurable: Record<string, unknown>,
134
+ sourceScope: string,
135
+ destinationScope: string
136
+ ): void {
137
+ const rebind = (key: string): string => {
138
+ const parts: unknown = JSON.parse(key);
139
+ if (!Array.isArray(parts) || parts[0] !== sourceScope) {
140
+ return key;
141
+ }
142
+ return JSON.stringify([destinationScope, ...parts.slice(1)]);
143
+ };
144
+ const config = { configurable };
145
+ const review = getToolApprovalReviewEvidence(config);
146
+ if (review?.owner != null) {
147
+ configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY] = {
148
+ ...review,
149
+ owner: rebind(review.owner),
150
+ };
151
+ }
152
+ const state = getToolBatchReplayState(configurable);
153
+ if (state != null) {
154
+ configurable[TOOL_BATCH_REPLAY_KEY] = {
155
+ ...state,
156
+ approvalOwner:
157
+ state.approvalOwner == null ? undefined : rebind(state.approvalOwner),
158
+ records: state.records.map((record) => ({
159
+ ...record,
160
+ owner: rebind(record.owner),
161
+ batch: rebind(record.batch),
162
+ })),
163
+ };
164
+ }
165
+ }
166
+
167
+ /** A fork may be paused again before it consumes its pending interrupt. */
168
+ export function rebindToolBatchReplayPayload(
169
+ payload: unknown,
170
+ sourceScope: string,
171
+ destinationScope: string
172
+ ): unknown {
173
+ const state = getToolBatchReplayState(payload);
174
+ const value = stripRunStepResumeState(payload);
175
+ if (state == null || !isRecord(value)) {
176
+ return payload;
177
+ }
178
+ const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
179
+ rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
180
+ const rebound = {
181
+ ...value,
182
+ [TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],
183
+ };
184
+ const runStepState = getRunStepResumeState(payload);
185
+ return runStepState == null
186
+ ? rebound
187
+ : attachRunStepResumeState(rebound, runStepState);
188
+ }
189
+
190
+ export function getToolBatchReplayState(
191
+ payload: unknown
192
+ ): ToolBatchReplayState | undefined {
193
+ const value = stripRunStepResumeState(payload);
194
+ if (
195
+ value == null ||
196
+ typeof value !== 'object' ||
197
+ !(TOOL_BATCH_REPLAY_KEY in value)
198
+ ) {
199
+ return undefined;
200
+ }
201
+ const candidate: unknown = value[TOOL_BATCH_REPLAY_KEY];
202
+ if (candidate === undefined) {
203
+ return undefined;
204
+ }
205
+ const state = candidate as Partial<ToolBatchReplayState> | null;
206
+ if (
207
+ state?.version !== 1 ||
208
+ (state.wrappedPayload != null &&
209
+ typeof state.wrappedPayload !== 'boolean') ||
210
+ (state.approvalOwner != null && typeof state.approvalOwner !== 'string') ||
211
+ !Array.isArray(state.records) ||
212
+ state.records.some((value: unknown) => {
213
+ if (value == null || typeof value !== 'object') {
214
+ return true;
215
+ }
216
+ const record = value as Partial<ToolBatchReplayRecord>;
217
+ return (
218
+ typeof record.owner !== 'string' ||
219
+ typeof record.batch !== 'string' ||
220
+ typeof record.encoding !== 'string' ||
221
+ typeof record.data !== 'string' ||
222
+ (record.turnState != null &&
223
+ !isToolNodeResumeState(record.turnState)) ||
224
+ (record.referenceState != null &&
225
+ !isToolOutputReferenceState(record.referenceState))
226
+ );
227
+ })
228
+ ) {
229
+ throw new Error('Invalid tool batch replay checkpoint');
230
+ }
231
+ return structuredClone(state as ToolBatchReplayState);
232
+ }
233
+
234
+ export function stripToolBatchReplayState(payload: unknown): unknown {
235
+ if (
236
+ payload == null ||
237
+ typeof payload !== 'object' ||
238
+ !(TOOL_BATCH_REPLAY_KEY in payload)
239
+ ) {
240
+ return payload;
241
+ }
242
+ const state = getToolBatchReplayState(payload);
243
+ const { [TOOL_BATCH_REPLAY_KEY]: _state, ...publicPayload } = payload;
244
+ if (
245
+ state?.wrappedPayload === true &&
246
+ TOOL_BATCH_PAYLOAD_KEY in publicPayload
247
+ ) {
248
+ return publicPayload[TOOL_BATCH_PAYLOAD_KEY];
249
+ }
250
+ return publicPayload;
251
+ }
252
+
253
+ /** Decode outer execution metadata before shape-sensitive child wrappers. */
254
+ export function getPublicToolInterruptPayload(payload: unknown): unknown {
255
+ return stripSubagentResumeManifest(
256
+ stripToolBatchReplayState(stripRunStepResumeState(payload))
257
+ );
258
+ }
259
+
260
+ export async function attachToolBatchReplayState(
261
+ payload: unknown,
262
+ owner: string,
263
+ batches: ReadonlyMap<string, ReadonlyMap<string, object>>,
264
+ turnState?: SubagentToolNodeResumeState,
265
+ referenceState?: ToolOutputReferenceState
266
+ ): Promise<unknown> {
267
+ const publicPayload = stripRunStepResumeState(payload);
268
+ const previous = getToolBatchReplayState(publicPayload);
269
+ const records =
270
+ previous?.records.filter((record) => record.owner !== owner) ?? [];
271
+ for (const [batch, results] of batches) {
272
+ const [encoding, bytes] = await serializer.dumpsTyped([...results]);
273
+ records.push({
274
+ owner,
275
+ batch,
276
+ encoding,
277
+ data: Buffer.from(bytes).toString('base64'),
278
+ ...(turnState == null ? {} : { turnState }),
279
+ ...(referenceState == null ? {} : { referenceState }),
280
+ });
281
+ }
282
+ const approvalOwner =
283
+ previous?.approvalOwner ??
284
+ (isToolApprovalInterrupt(publicPayload) ? owner : undefined);
285
+ if (records.length === 0 && approvalOwner == null) {
286
+ return payload;
287
+ }
288
+ const result = {
289
+ ...(isRecord(publicPayload)
290
+ ? publicPayload
291
+ : {
292
+ [TOOL_BATCH_WRAPPER_KEY]: 1,
293
+ [TOOL_BATCH_PAYLOAD_KEY]: publicPayload,
294
+ }),
295
+ [TOOL_BATCH_REPLAY_KEY]: {
296
+ version: 1,
297
+ approvalOwner,
298
+ records,
299
+ wrappedPayload: previous?.wrappedPayload ?? !isRecord(publicPayload),
300
+ } satisfies ToolBatchReplayState,
301
+ };
302
+ const runStepState = getRunStepResumeState(payload);
303
+ return runStepState == null
304
+ ? result
305
+ : attachRunStepResumeState(result, runStepState);
306
+ }
307
+
308
+ export async function restoreToolBatchReplayState(
309
+ config: RunnableConfig,
310
+ owner: string
311
+ ): Promise<
312
+ Array<{
313
+ batch: string;
314
+ results: Array<[string, SettledToolBatchResult]>;
315
+ turnState?: SubagentToolNodeResumeState;
316
+ referenceState?: ToolOutputReferenceState;
317
+ }>
318
+ > {
319
+ const state = getToolBatchReplayState(config.configurable);
320
+ if (state == null) {
321
+ return [];
322
+ }
323
+ return Promise.all(
324
+ state.records
325
+ .filter((record) => record.owner === owner)
326
+ .map(async (record) => {
327
+ const values: unknown = await serializer.loadsTyped(
328
+ record.encoding,
329
+ Buffer.from(record.data, 'base64')
330
+ );
331
+ if (!Array.isArray(values)) {
332
+ throw new Error('Invalid settled tool batch results');
333
+ }
334
+ const seen = new Set<string>();
335
+ const results = values.map(
336
+ (entry: unknown): [string, SettledToolBatchResult] => {
337
+ if (
338
+ !Array.isArray(entry) ||
339
+ entry.length !== 2 ||
340
+ typeof entry[0] !== 'string' ||
341
+ seen.has(entry[0])
342
+ ) {
343
+ throw new Error('Invalid settled tool batch results');
344
+ }
345
+ const result = entry[1] as Partial<SettledToolBatchResult> | null;
346
+ if (
347
+ result == null ||
348
+ (!isBaseMessage(result.output) && !isCommand(result.output)) ||
349
+ !Array.isArray(result.additionalContexts) ||
350
+ result.additionalContexts.some(
351
+ (context) => typeof context !== 'string'
352
+ ) ||
353
+ (result.completionHandled != null &&
354
+ typeof result.completionHandled !== 'boolean') ||
355
+ (result.referenceContent != null &&
356
+ typeof result.referenceContent !== 'string') ||
357
+ (result.turn != null &&
358
+ (!Number.isSafeInteger(result.turn) || result.turn < 0)) ||
359
+ !isRecord(result.proposal) ||
360
+ typeof result.proposal.name !== 'string' ||
361
+ result.proposal.name.length === 0 ||
362
+ !isRecord(result.proposal.args) ||
363
+ (result.resolvedArgs != null && !isRecord(result.resolvedArgs))
364
+ ) {
365
+ throw new Error('Invalid settled tool batch results');
366
+ }
367
+ seen.add(entry[0]);
368
+ const output = isCommand(result.output)
369
+ ? new Command({
370
+ update: result.output.update,
371
+ goto: result.output.goto,
372
+ graph: result.output.graph,
373
+ resume: result.output.resume,
374
+ })
375
+ : result.output;
376
+ return [
377
+ entry[0],
378
+ {
379
+ ...result,
380
+ proposal: result.proposal,
381
+ output,
382
+ additionalContexts: result.additionalContexts,
383
+ },
384
+ ];
385
+ }
386
+ );
387
+ return {
388
+ batch: record.batch,
389
+ results,
390
+ turnState: record.turnState,
391
+ referenceState: record.referenceState,
392
+ };
393
+ })
394
+ );
395
+ }
@@ -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,