@librechat/agents 3.8.4 → 3.8.6

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 (81) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +1 -1
  2. package/dist/cjs/common/constants.cjs +2 -0
  3. package/dist/cjs/common/constants.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +18 -9
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/hitl/approvalReview.cjs +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +7 -6
  9. package/dist/cjs/messages/index.cjs +1 -1
  10. package/dist/cjs/messages/prune.cjs +1 -1
  11. package/dist/cjs/run.cjs +7 -3
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +13 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/summarization/node.cjs +3 -1
  16. package/dist/cjs/summarization/node.cjs.map +1 -1
  17. package/dist/cjs/tools/ToolNode.cjs +134 -32
  18. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  19. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +1 -0
  20. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  21. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +0 -1
  22. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +123 -29
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/tools/toolBatchReplay.cjs +65 -13
  26. package/dist/cjs/tools/toolBatchReplay.cjs.map +1 -1
  27. package/dist/cjs/utils/index.cjs +1 -1
  28. package/dist/esm/agents/AgentContext.mjs +1 -1
  29. package/dist/esm/common/constants.mjs +2 -1
  30. package/dist/esm/common/constants.mjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +17 -8
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/hitl/approvalReview.mjs +1 -1
  34. package/dist/esm/hooks/types.mjs.map +1 -1
  35. package/dist/esm/main.mjs +6 -6
  36. package/dist/esm/messages/index.mjs +1 -1
  37. package/dist/esm/messages/prune.mjs +1 -1
  38. package/dist/esm/run.mjs +7 -3
  39. package/dist/esm/run.mjs.map +1 -1
  40. package/dist/esm/stream.mjs +13 -3
  41. package/dist/esm/stream.mjs.map +1 -1
  42. package/dist/esm/summarization/node.mjs +3 -1
  43. package/dist/esm/summarization/node.mjs.map +1 -1
  44. package/dist/esm/tools/ToolNode.mjs +135 -33
  45. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  46. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +1 -0
  47. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  48. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +0 -1
  49. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs +123 -29
  51. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  52. package/dist/esm/tools/toolBatchReplay.mjs +64 -13
  53. package/dist/esm/tools/toolBatchReplay.mjs.map +1 -1
  54. package/dist/esm/utils/index.mjs +1 -1
  55. package/dist/types/common/constants.d.ts +2 -0
  56. package/dist/types/graphs/Graph.d.ts +3 -2
  57. package/dist/types/hooks/types.d.ts +3 -0
  58. package/dist/types/run.d.ts +1 -0
  59. package/dist/types/summarization/node.d.ts +1 -0
  60. package/dist/types/tools/ToolNode.d.ts +4 -2
  61. package/dist/types/tools/subagent/SubagentExecutor.d.ts +7 -4
  62. package/dist/types/tools/toolBatchReplay.d.ts +6 -3
  63. package/dist/types/types/graph.d.ts +28 -0
  64. package/dist/types/types/run.d.ts +2 -0
  65. package/dist/types/types/tools.d.ts +11 -1
  66. package/package.json +1 -1
  67. package/src/common/constants.ts +3 -0
  68. package/src/graphs/Graph.ts +32 -18
  69. package/src/hooks/types.ts +3 -0
  70. package/src/run.ts +30 -15
  71. package/src/stream.ts +27 -5
  72. package/src/summarization/node.ts +3 -0
  73. package/src/tools/ToolNode.ts +528 -149
  74. package/src/tools/local/LocalProgrammaticToolCalling.ts +1 -0
  75. package/src/tools/subagent/SubagentExecutionRegistry.ts +0 -1
  76. package/src/tools/subagent/SubagentExecutor.ts +216 -48
  77. package/src/tools/toolBatchReplay.ts +121 -20
  78. package/src/types/graph.ts +37 -0
  79. package/src/types/run.ts +2 -0
  80. package/src/types/tools.ts +11 -1
  81. package/src/utils/llmConfig.ts +0 -1
@@ -214,6 +214,7 @@ export async function applyPreToolUseHooksForBridge(
214
214
  runId: hookContext.runId,
215
215
  threadId: hookContext.threadId,
216
216
  agentId: hookContext.agentId,
217
+ executionContext: hookContext.executionContext,
217
218
  executingAgentId: hookContext.executingAgentId,
218
219
  toolName,
219
220
  toolInput,
@@ -783,7 +783,6 @@ export class SubagentExecutionRecord<
783
783
  this.transitionTo('completed', ['active']);
784
784
  this.completedValue = true;
785
785
  this.completedResultValue = result;
786
- this.resolvedConfigValue = undefined;
787
786
  }
788
787
 
789
788
  releaseActiveRun(): TActiveRun | undefined {
@@ -52,6 +52,10 @@ import type {
52
52
  ResolvedSubagentConfig,
53
53
  ResolvedSubagentConfigEntry,
54
54
  SubagentExecutionContext,
55
+ SubagentContextAdapter,
56
+ SubagentContextInput,
57
+ SubagentContextResult,
58
+ PreparedSubagentContext,
55
59
  SubagentTaskConfig,
56
60
  SubagentTaskRuntime,
57
61
  SubagentResolveConfigurable,
@@ -88,11 +92,6 @@ import type {
88
92
  ToolApprovalReplaySnapshot,
89
93
  } from '@/hooks';
90
94
  import type { GraphFactory } from '@/graphs/graphFactory';
91
- import {
92
- rebindToolBatchReplayScope,
93
- rebindToolBatchReplayPayload,
94
- restoreToolReplayConfig,
95
- } from '@/tools/toolBatchReplay';
96
95
  import type { StandardGraph } from '@/graphs/Graph';
97
96
  import {
98
97
  getSubagentApprovalExecutionScope,
@@ -108,6 +107,11 @@ import {
108
107
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
109
108
  SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
110
109
  } from './SubagentReplay';
110
+ import {
111
+ rebindToolBatchReplayScope,
112
+ rebindToolBatchReplayPayload,
113
+ restoreToolReplayConfig,
114
+ } from '@/tools/toolBatchReplay';
111
115
  import {
112
116
  executeHooks,
113
117
  HookRegistry,
@@ -138,6 +142,7 @@ import { stableStringify } from '@/tools/eagerEventExecution';
138
142
  import { convertInjectedMessages } from '@/messages/injected';
139
143
  import { resolveClientOptionsModel } from '@/llm/request';
140
144
  import { composeAbortSignals } from '@/utils/misc';
145
+ import { sleep } from '@/utils/run';
141
146
  import { HandlerRegistry } from '@/events';
142
147
 
143
148
  export {
@@ -168,6 +173,31 @@ function seedChildGraphSessions(
168
173
  seedAgentInitialSessions(childGraph.sessions, agents);
169
174
  }
170
175
 
176
+ async function awaitWithAbort<T>(
177
+ promise: Promise<T>,
178
+ signal: AbortSignal
179
+ ): Promise<T> {
180
+ signal.throwIfAborted();
181
+ let onAbort: (() => void) | undefined;
182
+ const aborted = new Promise<never>((_resolve, reject) => {
183
+ onAbort = (): void => {
184
+ reject(
185
+ signal.reason instanceof Error
186
+ ? signal.reason
187
+ : new Error('Detached subagent task cancelled.')
188
+ );
189
+ };
190
+ signal.addEventListener('abort', onAbort, { once: true });
191
+ });
192
+ try {
193
+ return await Promise.race([promise, aborted]);
194
+ } finally {
195
+ if (onAbort != null) {
196
+ signal.removeEventListener('abort', onAbort);
197
+ }
198
+ }
199
+ }
200
+
171
201
  async function dispatchObservationalSubagentUpdate(
172
202
  handler: EventHandler,
173
203
  event: SubagentUpdateEvent
@@ -831,11 +861,11 @@ export type SubagentExecuteParams = {
831
861
  initialMessages?: BaseMessage[];
832
862
  };
833
863
 
834
- export type SubagentExecuteResult = {
835
- content: string;
836
- messages: BaseMessage[];
864
+ export type SubagentExecuteResult = SubagentContextResult & {
837
865
  /** Tagged internal failure; foreground callers retain the legacy content. */
838
866
  error?: string;
867
+ /** Completed child work whose host projection must be retried without re-execution. */
868
+ retryableDelivery?: true;
839
869
  };
840
870
 
841
871
  function createSubagentFailure(
@@ -931,6 +961,7 @@ export type SubagentExecutorOptions = {
931
961
  usageSink?: SubagentUsageSink;
932
962
  /** Host-owned process-local task namespace for detached execution. */
933
963
  taskConfig?: SubagentTaskConfig;
964
+ subagentContext?: SubagentContextAdapter;
934
965
  };
935
966
 
936
967
  type DurableExecutionRecord = SubagentExecutionRecord<
@@ -968,6 +999,7 @@ export class SubagentExecutor {
968
999
  ) => GraphFactory;
969
1000
  private readonly usageSink?: SubagentUsageSink;
970
1001
  private readonly taskConfig?: SubagentTaskConfig;
1002
+ private readonly subagentContext?: SubagentContextAdapter;
971
1003
  private readonly executions: SubagentExecutionRegistry<
972
1004
  SubagentExecuteResult,
973
1005
  ResolvedSubagentConfig,
@@ -1012,6 +1044,7 @@ export class SubagentExecutor {
1012
1044
  options.createDetachedChildGraphFactory;
1013
1045
  this.usageSink = options.usageSink;
1014
1046
  this.taskConfig = options.taskConfig;
1047
+ this.subagentContext = options.subagentContext;
1015
1048
  const rawRegistry = options.parentHandlerRegistry;
1016
1049
  if (typeof rawRegistry === 'function') {
1017
1050
  this.resolveParentHandlerRegistry = rawRegistry;
@@ -1257,6 +1290,7 @@ export class SubagentExecutor {
1257
1290
  langfuse: this.langfuse,
1258
1291
  tokenCounter: this.tokenCounter,
1259
1292
  usageSink: this.usageSink,
1293
+ subagentContext: this.subagentContext,
1260
1294
  streamLimits: this.streamLimits,
1261
1295
  maxDepth: this.maxDepth,
1262
1296
  createChildGraph:
@@ -1268,21 +1302,38 @@ export class SubagentExecutor {
1268
1302
  detachedGraphFactory ?? this.createChildGraphByKind,
1269
1303
  });
1270
1304
  try {
1271
- const result = await detached.execute({
1272
- ...params,
1273
- signal: undefined,
1274
- breaker: undefined,
1275
- hookSessionId: taskHookSessionId,
1276
- taskRuntime: runtime,
1277
- parentConfigurable: {
1278
- ...params.parentConfigurable,
1279
- run_id: taskHookSessionId,
1280
- },
1281
- });
1282
- if (runtime.signal.aborted) {
1283
- throw runtime.signal.reason instanceof Error
1284
- ? runtime.signal.reason
1285
- : new Error('Detached subagent task cancelled.');
1305
+ const executeAttempt = async (): Promise<SubagentExecuteResult> => {
1306
+ const result = await detached.execute({
1307
+ ...params,
1308
+ signal: undefined,
1309
+ breaker: undefined,
1310
+ hookSessionId: taskHookSessionId,
1311
+ taskRuntime: runtime,
1312
+ parentConfigurable: {
1313
+ ...params.parentConfigurable,
1314
+ run_id: taskHookSessionId,
1315
+ },
1316
+ });
1317
+ if (runtime.signal.aborted) {
1318
+ throw runtime.signal.reason instanceof Error
1319
+ ? runtime.signal.reason
1320
+ : new Error('Detached subagent task cancelled.');
1321
+ }
1322
+ return result;
1323
+ };
1324
+ let deliveryAttempts = 1;
1325
+ let result = await executeAttempt();
1326
+ while (
1327
+ result.retryableDelivery === true &&
1328
+ deliveryAttempts < 3
1329
+ ) {
1330
+ if (deliveryAttempts > 1) {
1331
+ await sleep(
1332
+ Math.min(100 * 2 ** Math.min(deliveryAttempts - 2, 6), 5_000)
1333
+ );
1334
+ }
1335
+ deliveryAttempts += 1;
1336
+ result = await executeAttempt();
1286
1337
  }
1287
1338
  if (result.error != null) {
1288
1339
  throw new Error(result.error);
@@ -1454,7 +1505,10 @@ export class SubagentExecutor {
1454
1505
  resumeExecution.checkpoints,
1455
1506
  branchChildThreadId,
1456
1507
  lease,
1457
- { source: resumeExecution.approvalExecutionScope, target: approvalExecutionScope }
1508
+ {
1509
+ source: resumeExecution.approvalExecutionScope,
1510
+ target: approvalExecutionScope,
1511
+ }
1458
1512
  );
1459
1513
  let previousApprovals: ToolApprovalReplaySnapshot[] = [];
1460
1514
  let approvalsMutated = false;
@@ -1765,11 +1819,20 @@ export class SubagentExecutor {
1765
1819
  }
1766
1820
  /** Repeated unconsumed forks must persist their current owner. */
1767
1821
  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
- ) }
1822
+ channel === INTERRUPT &&
1823
+ approvalScope != null &&
1824
+ value != null &&
1825
+ typeof value === 'object' &&
1826
+ 'value' in value
1827
+ ? {
1828
+ ...value,
1829
+ value: rebindToolBatchReplayPayload(
1830
+ value.value,
1831
+ approvalScope.source,
1832
+ approvalScope.target,
1833
+ targetThreadId
1834
+ ),
1835
+ }
1773
1836
  : value;
1774
1837
  writes.push([channel, reboundValue]);
1775
1838
  }
@@ -2114,6 +2177,9 @@ export class SubagentExecutor {
2114
2177
  ) {
2115
2178
  return;
2116
2179
  }
2180
+ if (settled.output.status === 'error' && config.signal?.aborted === true) {
2181
+ return;
2182
+ }
2117
2183
  const parentConfigurable = config.configurable as
2118
2184
  | Record<string, unknown>
2119
2185
  | undefined;
@@ -2123,6 +2189,9 @@ export class SubagentExecutor {
2123
2189
  parentToolCallId,
2124
2190
  parentConfigurable,
2125
2191
  });
2192
+ if (execution.completedResult != null && settled.output.status === 'error') {
2193
+ return;
2194
+ }
2126
2195
  const { resumeExecution } = execution;
2127
2196
  const resolvedSubagentType = getSubagentTypeFromArgs(settled.resolvedArgs);
2128
2197
  const resolvedDescription = getSubagentDescriptionFromArgs(
@@ -2383,11 +2452,6 @@ export class SubagentExecutor {
2383
2452
  if (!bound) {
2384
2453
  return createSubagentFailure(SUBAGENT_CONFIG_CHANGED_MESSAGE);
2385
2454
  }
2386
- const completedChildResult = execution.completedResult;
2387
- if (completedChildResult != null) {
2388
- return completedChildResult;
2389
- }
2390
-
2391
2455
  let config: ResolvedSubagentConfigEntry;
2392
2456
  try {
2393
2457
  config = await this.resolveExecutionConfig(executableConfig, execution, {
@@ -2447,6 +2511,63 @@ export class SubagentExecutor {
2447
2511
  },
2448
2512
  ],
2449
2513
  };
2514
+ const hostContextInput: SubagentContextInput = {
2515
+ executionContext: childExecutionContext,
2516
+ parentThreadId: threadId,
2517
+ memberAgentIds: childPlan.agents.map((agent) => agent.agentId),
2518
+ signal: childSignal,
2519
+ resumed: execution.started || resumeExecution != null,
2520
+ };
2521
+ let preparedContext: PreparedSubagentContext | undefined;
2522
+ try {
2523
+ preparedContext =
2524
+ this.subagentContext?.prepare == null
2525
+ ? undefined
2526
+ : await awaitWithAbort(
2527
+ Promise.resolve(this.subagentContext.prepare(hostContextInput)),
2528
+ childSignal
2529
+ );
2530
+ execution.assertUsable(childSignal);
2531
+ for (const [agentId, sessions] of Object.entries(
2532
+ preparedContext?.agentSessions ?? {}
2533
+ )) {
2534
+ const member = childPlan.memberInputs.get(agentId);
2535
+ if (member == null) throw new Error('Unknown subagent context member.');
2536
+ const activeMember =
2537
+ execution.activeRun?.graph.agentContexts.get(agentId);
2538
+ if (
2539
+ activeMember != null &&
2540
+ activeMember.codeSessionKey !== sessions.codeSessionKey
2541
+ ) {
2542
+ throw new Error(
2543
+ 'Subagent session partition changed during execution.'
2544
+ );
2545
+ }
2546
+ member.codeSessionKey = sessions.codeSessionKey;
2547
+ member.initialSessions = sessions.initialSessions;
2548
+ }
2549
+ } catch (error) {
2550
+ if (childSignal.aborted) {
2551
+ throw childSignal.reason instanceof Error
2552
+ ? childSignal.reason
2553
+ : error;
2554
+ }
2555
+ if (execution.completedResult != null) {
2556
+ return {
2557
+ ...createSubagentFailure(
2558
+ 'Subagent context is unavailable or access was denied.'
2559
+ ),
2560
+ retryableDelivery: true,
2561
+ };
2562
+ }
2563
+ return createSubagentFailure(
2564
+ 'Subagent context is unavailable or access was denied.'
2565
+ );
2566
+ }
2567
+ const completedChildResult = execution.completedResult;
2568
+ if (completedChildResult != null) {
2569
+ return this.completeHostContext(hostContextInput, completedChildResult);
2570
+ }
2450
2571
  const maxTurns = config.maxTurns ?? DEFAULT_SUBAGENT_MAX_TURNS;
2451
2572
  const memberRecursionLimit = maxTurns * SUBAGENT_RECURSION_MULTIPLIER;
2452
2573
  const recursionLimit =
@@ -2473,6 +2594,7 @@ export class SubagentExecutor {
2473
2594
  streamLimits: this.streamLimits,
2474
2595
  subagentScope: true,
2475
2596
  subagentExecutionContext: childExecutionContext,
2597
+ subagentContext: this.subagentContext,
2476
2598
  ...(resumeExecution?.graphState.fadingTier == null
2477
2599
  ? {}
2478
2600
  : { fadingTier: resumeExecution.graphState.fadingTier }),
@@ -2519,9 +2641,7 @@ export class SubagentExecutor {
2519
2641
  if (params.taskRuntime != null) {
2520
2642
  childGraph.hookRegistry = this.hookRegistry;
2521
2643
  }
2522
- if (cachedChildRun == null) {
2523
- seedChildGraphSessions(childGraph, childPlan.agents);
2524
- }
2644
+ seedChildGraphSessions(childGraph, childPlan.agents);
2525
2645
  let forwarding: ForwarderCallback | undefined;
2526
2646
  if (forwardingEnabled) {
2527
2647
  forwarding = this.createForwarderCallback({
@@ -2623,6 +2743,8 @@ export class SubagentExecutor {
2623
2743
  }
2624
2744
  const childConfigurable: Record<string, unknown> = {
2625
2745
  ...inheritedConfigurable,
2746
+ ...sanitizePreparedConfigurable(preparedContext?.configurable),
2747
+ executionContext: childExecutionContext,
2626
2748
  };
2627
2749
  if (this.humanInTheLoop?.enabled === true) {
2628
2750
  childConfigurable[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] =
@@ -2663,11 +2785,18 @@ export class SubagentExecutor {
2663
2785
  }
2664
2786
  const persistedInterrupts = getPersistedInterrupts(persistedState);
2665
2787
  if (persistedInterrupts.length > 0) {
2666
- activeChildRun.pendingInterrupts = resumeExecution == null ? persistedInterrupts :
2667
- persistedInterrupts.map((pending) => ({
2668
- ...pending,
2669
- value: rebindToolBatchReplayPayload(pending.value, resumeExecution.approvalExecutionScope, approvalExecutionScope),
2670
- }));
2788
+ activeChildRun.pendingInterrupts =
2789
+ resumeExecution == null
2790
+ ? persistedInterrupts
2791
+ : persistedInterrupts.map((pending) => ({
2792
+ ...pending,
2793
+ value: rebindToolBatchReplayPayload(
2794
+ pending.value,
2795
+ resumeExecution.approvalExecutionScope,
2796
+ approvalExecutionScope,
2797
+ childThreadId
2798
+ ),
2799
+ }));
2671
2800
  execution.markStarted();
2672
2801
  childAlreadyStarted = true;
2673
2802
  } else if (persistedState.next.length > 0) {
@@ -2702,14 +2831,18 @@ export class SubagentExecutor {
2702
2831
  }
2703
2832
  let childInput: BaseGraphState | Command | null;
2704
2833
  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];
2834
+ const pending =
2835
+ activeChildRun.pendingInterrupts.find(
2836
+ (entry) =>
2837
+ entry.id != null &&
2838
+ Object.prototype.hasOwnProperty.call(childResumeMap, entry.id)
2839
+ ) ?? activeChildRun.pendingInterrupts[0];
2708
2840
  restoreToolReplayConfig(childConfigurable, pending.id, pending.value);
2709
2841
  rebindToolBatchReplayScope(
2710
2842
  childConfigurable,
2711
2843
  resumeExecution?.approvalExecutionScope ?? approvalExecutionScope,
2712
- approvalExecutionScope
2844
+ approvalExecutionScope,
2845
+ childThreadId
2713
2846
  );
2714
2847
  childInput = new Command({ resume: childResumeMap });
2715
2848
  } else if (recoveredInProgress) {
@@ -2725,6 +2858,7 @@ export class SubagentExecutor {
2725
2858
  [SUBAGENT_RUN_ID_KEY]: childRunId,
2726
2859
  },
2727
2860
  }),
2861
+ ...(preparedContext?.messages ?? []),
2728
2862
  ],
2729
2863
  };
2730
2864
  }
@@ -2740,6 +2874,7 @@ export class SubagentExecutor {
2740
2874
  registry: this.hookRegistry,
2741
2875
  input: {
2742
2876
  hook_event_name: 'SubagentStart',
2877
+ executionContext: childExecutionContext,
2743
2878
  runId: currentHookSessionId,
2744
2879
  threadId,
2745
2880
  parentAgentId: this.parentAgentId,
@@ -2946,6 +3081,7 @@ export class SubagentExecutor {
2946
3081
  registry: this.hookRegistry,
2947
3082
  input: {
2948
3083
  hook_event_name: 'SubagentStop',
3084
+ executionContext: childExecutionContext,
2949
3085
  runId: currentHookSessionId,
2950
3086
  threadId,
2951
3087
  agentId: childAgentId,
@@ -2988,12 +3124,33 @@ export class SubagentExecutor {
2988
3124
 
2989
3125
  this.clearChildGraph(childGraph);
2990
3126
 
2991
- const completedResult = {
3127
+ const completedResult: SubagentExecuteResult = {
2992
3128
  content: filteredContent,
2993
3129
  messages: result.messages,
2994
3130
  };
2995
3131
  execution.markCompleted(completedResult);
2996
- return completedResult;
3132
+ return this.completeHostContext(hostContextInput, completedResult);
3133
+ }
3134
+
3135
+ private async completeHostContext(
3136
+ input: SubagentContextInput,
3137
+ result: SubagentExecuteResult
3138
+ ): Promise<SubagentExecuteResult> {
3139
+ if (this.subagentContext?.complete == null) return result;
3140
+ try {
3141
+ input.signal.throwIfAborted();
3142
+ const completed = await awaitWithAbort(
3143
+ this.subagentContext.complete(input, result),
3144
+ input.signal
3145
+ );
3146
+ input.signal.throwIfAborted();
3147
+ return { ...result, content: completed.content };
3148
+ } catch {
3149
+ return {
3150
+ ...createSubagentFailure('Subagent result delivery failed.'),
3151
+ retryableDelivery: true,
3152
+ };
3153
+ }
2997
3154
  }
2998
3155
 
2999
3156
  /**
@@ -3526,6 +3683,17 @@ function sanitizeChildConfigurable(
3526
3683
  );
3527
3684
  }
3528
3685
 
3686
+ function sanitizePreparedConfigurable(
3687
+ configurable: RunnableConfig['configurable']
3688
+ ): Record<string, unknown> {
3689
+ const prepared = sanitizeChildConfigurable(configurable);
3690
+ delete prepared.run_id;
3691
+ delete prepared.parent_run_id;
3692
+ delete prepared.thread_id;
3693
+ delete prepared.executionContext;
3694
+ return prepared;
3695
+ }
3696
+
3529
3697
  function isLangGraphRuntimeConfigKey(key: string): boolean {
3530
3698
  return (
3531
3699
  key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) ||
@@ -46,6 +46,8 @@ export interface ToolBatchReplayRecord {
46
46
  referenceState?: ToolOutputReferenceState;
47
47
  }
48
48
 
49
+ export type ToolReplayResumeStatus = 'active' | 'stale' | 'unverifiable';
50
+
49
51
  interface ToolBatchReplayState {
50
52
  version: 1;
51
53
  approvalOwner?: string;
@@ -62,6 +64,44 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
64
  return value != null && typeof value === 'object' && !Array.isArray(value);
63
65
  }
64
66
 
67
+ function bindLegacyReplayOwner(
68
+ owner: string,
69
+ configurable: Record<string, unknown>
70
+ ): string {
71
+ let identity: unknown;
72
+ try {
73
+ identity = JSON.parse(owner);
74
+ } catch {
75
+ return owner;
76
+ }
77
+ if (!Array.isArray(identity) || identity.length !== 3) {
78
+ return owner;
79
+ }
80
+ return JSON.stringify([
81
+ ...identity,
82
+ typeof configurable.user_id === 'string' ? configurable.user_id : '',
83
+ typeof configurable.thread_id === 'string' ? configurable.thread_id : '',
84
+ ]);
85
+ }
86
+
87
+ function bindLegacyReplayState(
88
+ state: ToolBatchReplayState | undefined,
89
+ configurable: Record<string, unknown>
90
+ ): ToolBatchReplayState | undefined {
91
+ if (state == null) return undefined;
92
+ return {
93
+ ...state,
94
+ approvalOwner:
95
+ state.approvalOwner == null
96
+ ? undefined
97
+ : bindLegacyReplayOwner(state.approvalOwner, configurable),
98
+ records: state.records.map((record) => ({
99
+ ...record,
100
+ owner: bindLegacyReplayOwner(record.owner, configurable),
101
+ })),
102
+ };
103
+ }
104
+
65
105
  /** The only host-to-runtime entry point for checkpoint-owned tool replay state. */
66
106
  export function restoreToolReplayConfig(
67
107
  configurable: Record<string, unknown>,
@@ -70,7 +110,10 @@ export function restoreToolReplayConfig(
70
110
  ): void {
71
111
  delete configurable[TOOL_BATCH_REPLAY_KEY];
72
112
  delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
73
- const state = getToolBatchReplayState(payload);
113
+ const state = bindLegacyReplayState(
114
+ getToolBatchReplayState(payload),
115
+ configurable
116
+ );
74
117
  const publicPayload = getPublicToolInterruptPayload(payload);
75
118
  const review = createToolApprovalReviewEvidence(
76
119
  interruptId,
@@ -95,25 +138,40 @@ export function restoreToolReplayConfig(
95
138
  * steps, so the presence of replay evidence alone does not imply a resume.
96
139
  * A parent replaying a checkpointed child has no local resume value.
97
140
  */
98
- export function isToolReplayResume(
141
+ export function getToolReplayResumeStatus(
99
142
  config: RunnableConfig,
100
143
  interruptId: string | undefined,
101
144
  isChildReplay: boolean
102
- ): boolean {
103
- const resumeMap: object | undefined = config.configurable?.__pregel_resume_map;
104
- if (interruptId != null &&
105
- resumeMap != null &&
106
- !Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
107
- return false;
145
+ ): ToolReplayResumeStatus {
146
+ const resumeMap: unknown = config.configurable?.__pregel_resume_map;
147
+ const scratchpad: unknown = config.configurable?.__pregel_scratchpad;
148
+ if (!isRecord(scratchpad) || !Array.isArray(scratchpad.resume)) {
149
+ return 'unverifiable';
108
150
  }
109
- if (isChildReplay && resumeMap != null) {
110
- return true;
151
+ /**
152
+ * LangGraph supports both an interrupt-id resume map and a task-local
153
+ * `nullResume` value. The latter is already scoped to the checkpoint task,
154
+ * so it remains a supported, verifiable resume without a map entry.
155
+ */
156
+ if (scratchpad.nullResume !== undefined) {
157
+ return 'active';
111
158
  }
112
- const scratchpad: { resume?: { length: number }; nullResume?: unknown } | undefined =
113
- config.configurable?.__pregel_scratchpad;
114
- return scratchpad == null ||
115
- (scratchpad.resume?.length ?? 0) > 0 ||
116
- scratchpad.nullResume !== undefined;
159
+ const hasResume = scratchpad.resume.length > 0;
160
+ if (!isRecord(resumeMap)) {
161
+ return hasResume ? 'unverifiable' : 'stale';
162
+ }
163
+ if (interruptId == null) {
164
+ /** Legacy direct graph callers do not restore the ID into replay state. */
165
+ if (!hasResume) return 'stale';
166
+ return Object.keys(resumeMap).length === 1 ? 'active' : 'unverifiable';
167
+ }
168
+ if (!Object.prototype.hasOwnProperty.call(resumeMap, interruptId)) {
169
+ return hasResume ? 'unverifiable' : 'stale';
170
+ }
171
+ if (isChildReplay) {
172
+ return 'active';
173
+ }
174
+ return hasResume ? 'active' : 'stale';
117
175
  }
118
176
 
119
177
  export function getToolBatchReplayOwner(
@@ -126,9 +184,39 @@ export function getToolBatchReplayOwner(
126
184
  ? (config.configurable?.checkpoint_ns ?? '')
127
185
  : '',
128
186
  agentId,
187
+ typeof config.configurable?.user_id === 'string'
188
+ ? config.configurable.user_id
189
+ : '',
190
+ typeof config.configurable?.thread_id === 'string'
191
+ ? config.configurable.thread_id
192
+ : '',
129
193
  ]);
130
194
  }
131
195
 
196
+ /** Remove stale authorization while retaining only settled results for this batch. */
197
+ export function clearStaleToolApprovalConfig(
198
+ configurable: Record<string, unknown>,
199
+ owner: string,
200
+ batch: string | undefined
201
+ ): void {
202
+ delete configurable[TOOL_APPROVAL_REVIEW_CONFIG_KEY];
203
+ const state = getToolBatchReplayState(configurable);
204
+ const records = batch == null
205
+ ? []
206
+ : (state?.records.filter(
207
+ (record) => record.owner === owner && record.batch === batch
208
+ ) ?? []);
209
+ if (state == null || records.length === 0) {
210
+ delete configurable[TOOL_BATCH_REPLAY_KEY];
211
+ return;
212
+ }
213
+ configurable[TOOL_BATCH_REPLAY_KEY] = {
214
+ version: 1,
215
+ records,
216
+ wrappedPayload: state.wrappedPayload,
217
+ } satisfies ToolBatchReplayState;
218
+ }
219
+
132
220
  export function getToolBatchReplayScope(
133
221
  config: RunnableConfig
134
222
  ): string | undefined {
@@ -147,7 +235,9 @@ export function isChildToolReplayOwner(
147
235
  const manifest = requireValidSubagentResumeManifest(config.configurable);
148
236
  if (manifest == null || trustedParentCallIds.size === 0) return false;
149
237
  const identity: unknown = JSON.parse(owner);
150
- if (!Array.isArray(identity) || identity.length !== 3 || identity[1] !== '') return false;
238
+ if (!Array.isArray(identity) ||
239
+ (identity.length !== 3 && identity.length !== 5) ||
240
+ identity[1] !== '') return false;
151
241
  const pending = manifest.executions.filter((execution) => trustedParentCallIds.has(execution.parentToolCallId));
152
242
  while (pending.length > 0) {
153
243
  const execution = pending.pop()!;
@@ -161,14 +251,19 @@ export function isChildToolReplayOwner(
161
251
  export function rebindToolBatchReplayScope(
162
252
  configurable: Record<string, unknown>,
163
253
  sourceScope: string,
164
- destinationScope: string
254
+ destinationScope: string,
255
+ destinationThreadId?: string
165
256
  ): void {
166
257
  const rebind = (key: string): string => {
167
258
  const parts: unknown = JSON.parse(key);
168
259
  if (!Array.isArray(parts) || parts[0] !== sourceScope) {
169
260
  return key;
170
261
  }
171
- return JSON.stringify([destinationScope, ...parts.slice(1)]);
262
+ const rebound = [destinationScope, ...parts.slice(1)];
263
+ if (rebound.length === 5 && destinationThreadId != null) {
264
+ rebound[4] = destinationThreadId;
265
+ }
266
+ return JSON.stringify(rebound);
172
267
  };
173
268
  const config = { configurable };
174
269
  const review = getToolApprovalReviewEvidence(config);
@@ -197,7 +292,8 @@ export function rebindToolBatchReplayScope(
197
292
  export function rebindToolBatchReplayPayload(
198
293
  payload: unknown,
199
294
  sourceScope: string,
200
- destinationScope: string
295
+ destinationScope: string,
296
+ destinationThreadId?: string
201
297
  ): unknown {
202
298
  const state = getToolBatchReplayState(payload);
203
299
  const value = stripRunStepResumeState(payload);
@@ -205,7 +301,12 @@ export function rebindToolBatchReplayPayload(
205
301
  return payload;
206
302
  }
207
303
  const configurable = { [TOOL_BATCH_REPLAY_KEY]: state };
208
- rebindToolBatchReplayScope(configurable, sourceScope, destinationScope);
304
+ rebindToolBatchReplayScope(
305
+ configurable,
306
+ sourceScope,
307
+ destinationScope,
308
+ destinationThreadId
309
+ );
209
310
  const rebound = {
210
311
  ...value,
211
312
  [TOOL_BATCH_REPLAY_KEY]: configurable[TOOL_BATCH_REPLAY_KEY],