@librechat/agents 3.3.13 → 3.4.1

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 (192) hide show
  1. package/dist/cjs/graphs/Graph.cjs +121 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs.map +1 -1
  8. package/dist/cjs/hooks/types.cjs +2 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/llm/anthropic/index.cjs +35 -206
  11. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  12. package/dist/cjs/llm/bedrock/index.cjs +121 -241
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/llm/google/index.cjs +19 -12
  15. package/dist/cjs/llm/google/index.cjs.map +1 -1
  16. package/dist/cjs/llm/init.cjs +3 -3
  17. package/dist/cjs/llm/invoke.cjs +2 -2
  18. package/dist/cjs/llm/mistral/index.cjs +26 -0
  19. package/dist/cjs/llm/mistral/index.cjs.map +1 -0
  20. package/dist/cjs/llm/openai/index.cjs +82 -80
  21. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/llm/providers.cjs +19 -19
  24. package/dist/cjs/llm/providers.cjs.map +1 -1
  25. package/dist/cjs/llm/stream/chunkAdapters.cjs +198 -0
  26. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -0
  27. package/dist/cjs/llm/stream/smoother.cjs +369 -0
  28. package/dist/cjs/llm/stream/smoother.cjs.map +1 -0
  29. package/dist/cjs/llm/vertexai/index.cjs +13 -1
  30. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  31. package/dist/cjs/main.cjs +25 -16
  32. package/dist/cjs/messages/format.cjs +40 -23
  33. package/dist/cjs/messages/format.cjs.map +1 -1
  34. package/dist/cjs/run.cjs +82 -15
  35. package/dist/cjs/run.cjs.map +1 -1
  36. package/dist/cjs/session/AgentSession.cjs +37 -2
  37. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  38. package/dist/cjs/stream.cjs +1 -1
  39. package/dist/cjs/summarization/node.cjs +6 -3
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  42. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  43. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  44. package/dist/cjs/tools/ToolNode.cjs +316 -94
  45. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  46. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +664 -80
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  49. package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
  50. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  51. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  52. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  53. package/dist/cjs/utils/index.cjs +2 -2
  54. package/dist/cjs/utils/tokens.cjs +76 -29
  55. package/dist/cjs/utils/tokens.cjs.map +1 -1
  56. package/dist/esm/graphs/Graph.mjs +121 -27
  57. package/dist/esm/graphs/Graph.mjs.map +1 -1
  58. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  59. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  60. package/dist/esm/hooks/executeHooks.mjs +60 -14
  61. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  62. package/dist/esm/hooks/index.mjs.map +1 -1
  63. package/dist/esm/hooks/types.mjs +2 -1
  64. package/dist/esm/hooks/types.mjs.map +1 -1
  65. package/dist/esm/llm/anthropic/index.mjs +34 -205
  66. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  67. package/dist/esm/llm/bedrock/index.mjs +120 -240
  68. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  69. package/dist/esm/llm/google/index.mjs +19 -12
  70. package/dist/esm/llm/google/index.mjs.map +1 -1
  71. package/dist/esm/llm/init.mjs +1 -1
  72. package/dist/esm/llm/invoke.mjs +2 -2
  73. package/dist/esm/llm/mistral/index.mjs +26 -0
  74. package/dist/esm/llm/mistral/index.mjs.map +1 -0
  75. package/dist/esm/llm/openai/index.mjs +82 -80
  76. package/dist/esm/llm/openai/index.mjs.map +1 -1
  77. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  78. package/dist/esm/llm/providers.mjs +3 -3
  79. package/dist/esm/llm/providers.mjs.map +1 -1
  80. package/dist/esm/llm/stream/chunkAdapters.mjs +195 -0
  81. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -0
  82. package/dist/esm/llm/stream/smoother.mjs +365 -0
  83. package/dist/esm/llm/stream/smoother.mjs.map +1 -0
  84. package/dist/esm/llm/vertexai/index.mjs +13 -1
  85. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  86. package/dist/esm/main.mjs +13 -11
  87. package/dist/esm/messages/format.mjs +40 -23
  88. package/dist/esm/messages/format.mjs.map +1 -1
  89. package/dist/esm/run.mjs +83 -16
  90. package/dist/esm/run.mjs.map +1 -1
  91. package/dist/esm/session/AgentSession.mjs +37 -2
  92. package/dist/esm/session/AgentSession.mjs.map +1 -1
  93. package/dist/esm/stream.mjs +1 -1
  94. package/dist/esm/summarization/node.mjs +6 -3
  95. package/dist/esm/summarization/node.mjs.map +1 -1
  96. package/dist/esm/tools/BashExecutor.mjs +1 -1
  97. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  98. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  99. package/dist/esm/tools/ToolNode.mjs +317 -95
  100. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  101. package/dist/esm/tools/ToolSearch.mjs +1 -1
  102. package/dist/esm/tools/subagent/SubagentExecutor.mjs +665 -81
  103. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  104. package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
  105. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  106. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  107. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  108. package/dist/esm/utils/index.mjs +2 -2
  109. package/dist/esm/utils/tokens.mjs +76 -30
  110. package/dist/esm/utils/tokens.mjs.map +1 -1
  111. package/dist/types/graphs/Graph.d.ts +19 -1
  112. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  113. package/dist/types/hooks/executeHooks.d.ts +5 -1
  114. package/dist/types/hooks/index.d.ts +2 -2
  115. package/dist/types/hooks/types.d.ts +12 -0
  116. package/dist/types/index.d.ts +3 -0
  117. package/dist/types/llm/google/index.d.ts +2 -0
  118. package/dist/types/llm/mistral/index.d.ts +11 -0
  119. package/dist/types/llm/openai/index.d.ts +20 -4
  120. package/dist/types/llm/openrouter/index.d.ts +4 -1
  121. package/dist/types/llm/stream/chunkAdapters.d.ts +48 -0
  122. package/dist/types/llm/stream/smoother.d.ts +95 -0
  123. package/dist/types/llm/vertexai/index.d.ts +2 -0
  124. package/dist/types/run.d.ts +4 -1
  125. package/dist/types/session/AgentSession.d.ts +1 -0
  126. package/dist/types/tools/ToolNode.d.ts +23 -15
  127. package/dist/types/tools/subagent/SubagentExecutor.d.ts +49 -7
  128. package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
  129. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  130. package/dist/types/types/graph.d.ts +5 -5
  131. package/dist/types/types/hitl.d.ts +15 -0
  132. package/dist/types/types/llm.d.ts +21 -14
  133. package/dist/types/utils/tokens.d.ts +10 -0
  134. package/package.json +6 -2
  135. package/src/graphs/Graph.ts +257 -52
  136. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  137. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  138. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  139. package/src/hooks/HookRegistry.ts +151 -1
  140. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  141. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  142. package/src/hooks/executeHooks.ts +133 -15
  143. package/src/hooks/index.ts +3 -1
  144. package/src/hooks/types.ts +16 -0
  145. package/src/index.ts +8 -0
  146. package/src/llm/anthropic/index.ts +85 -354
  147. package/src/llm/bedrock/index.ts +174 -390
  148. package/src/llm/bedrock/llm.spec.ts +2 -0
  149. package/src/llm/bedrock/streamSealDispatch.test.ts +76 -0
  150. package/src/llm/custom-chat-models.smoke.test.ts +16 -1
  151. package/src/llm/google/index.ts +17 -18
  152. package/src/llm/google/streamSmoothing.test.ts +121 -0
  153. package/src/llm/mistral/index.ts +33 -0
  154. package/src/llm/mistral/streamSmoothing.test.ts +97 -0
  155. package/src/llm/openai/deepseek.test.ts +56 -0
  156. package/src/llm/openai/index.ts +119 -126
  157. package/src/llm/openrouter/index.ts +4 -1
  158. package/src/llm/providers.ts +3 -3
  159. package/src/llm/stream/chunkAdapters.test.ts +202 -0
  160. package/src/llm/stream/chunkAdapters.ts +317 -0
  161. package/src/llm/stream/reassembly.test.ts +241 -0
  162. package/src/llm/stream/smoother.bench.test.ts +155 -0
  163. package/src/llm/stream/smoother.test.ts +519 -0
  164. package/src/llm/stream/smoother.ts +574 -0
  165. package/src/llm/vertexai/index.ts +17 -1
  166. package/src/llm/vertexai/streamSmoothing.test.ts +109 -0
  167. package/src/messages/format.ts +54 -26
  168. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  169. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  170. package/src/messages/formatAgentMessages.test.ts +7 -2
  171. package/src/run.ts +164 -18
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/subagent.test.ts +44 -0
  175. package/src/specs/tokens.test.ts +159 -14
  176. package/src/summarization/__tests__/node.test.ts +32 -14
  177. package/src/summarization/node.ts +15 -11
  178. package/src/tools/ToolNode.ts +631 -157
  179. package/src/tools/__tests__/SubagentExecutor.test.ts +590 -7
  180. package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
  181. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  182. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
  183. package/src/tools/__tests__/hitl.test.ts +234 -5
  184. package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
  185. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  186. package/src/tools/subagent/SubagentExecutor.ts +1438 -136
  187. package/src/tools/subagent/SubagentReplay.ts +575 -0
  188. package/src/tools/toolOutputReferences.ts +40 -1
  189. package/src/types/graph.ts +5 -5
  190. package/src/types/hitl.ts +16 -0
  191. package/src/types/llm.ts +53 -36
  192. package/src/utils/tokens.ts +115 -30
@@ -1,11 +1,40 @@
1
1
  import { nanoid } from 'nanoid';
2
- import { HumanMessage } from '@langchain/core/messages';
3
2
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
4
- import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
3
+ import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
4
+ import {
5
+ AIMessage,
6
+ BaseMessage,
7
+ HumanMessage,
8
+ ToolMessage,
9
+ } from '@langchain/core/messages';
10
+ import {
11
+ Command,
12
+ END,
13
+ GraphInterrupt,
14
+ INTERRUPT,
15
+ MessagesAnnotation,
16
+ START,
17
+ StateGraph,
18
+ copyCheckpoint,
19
+ isGraphInterrupt,
20
+ isInterrupted,
21
+ } from '@langchain/langgraph';
22
+ import type {
23
+ Interrupt,
24
+ StateSnapshot,
25
+ CheckpointTuple,
26
+ BaseCheckpointSaver,
27
+ } from '@langchain/langgraph';
5
28
  import type { ChatGeneration, LLMResult } from '@langchain/core/outputs';
6
29
  import type { Callbacks } from '@langchain/core/callbacks/manager';
30
+ import type { RunnableConfig } from '@langchain/core/runnables';
31
+ import type { UsageMetadata } from '@langchain/core/messages';
32
+ import type { ToolCall } from '@langchain/core/messages/tool';
7
33
  import type {
8
34
  AgentInputs,
35
+ BaseGraphState,
36
+ CompiledStateWorkflow,
37
+ HumanInTheLoopConfig,
9
38
  InjectedMessage,
10
39
  MessageDeltaEvent,
11
40
  ProcessedToolCall,
@@ -19,25 +48,51 @@ import type {
19
48
  SubagentUpdateEvent,
20
49
  SubagentUpdatePhase,
21
50
  SubagentUsageSink,
51
+ ToolApprovalInterruptPayload,
22
52
  ToolExecuteBatchRequest,
23
53
  ToolCallDelta,
24
54
  TokenCounter,
55
+ ToolApprovalDecision,
56
+ ToolApprovalDecisionMap,
25
57
  } from '@/types';
58
+ import type {
59
+ SubagentResumeExecution,
60
+ SubagentResumeManifest,
61
+ SubagentCheckpointReference,
62
+ SettledSubagentToolOutput,
63
+ } from './SubagentReplay';
26
64
  import type { AggregatedHookResult, HookRegistry } from '@/hooks';
27
65
  import type { AgentContext } from '@/agents/AgentContext';
28
66
  import type { StandardGraph } from '@/graphs/Graph';
29
67
  import type { HandlerRegistry } from '@/events';
30
- import { Constants, GraphEvents, Callback, StepTypes } from '@/common';
68
+ import {
69
+ getSubagentResumeManifest,
70
+ attachSubagentResumeManifest,
71
+ SUBAGENT_PARENT_BATCH_CONFIG_KEY,
72
+ SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
73
+ SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
74
+ } from './SubagentReplay';
31
75
  import {
32
76
  StreamLimitExceededError,
33
77
  RUN_BREAKER_SCOPE_CONFIG_KEY,
34
78
  } from '@/llm/streamLimits';
35
- import { executeHooks } from '@/hooks';
79
+ import {
80
+ ContentTypes,
81
+ Constants,
82
+ GraphEvents,
83
+ Callback,
84
+ StepTypes,
85
+ } from '@/common';
86
+ import {
87
+ executeHooks,
88
+ TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
89
+ } from '@/hooks';
36
90
 
37
91
  const DEFAULT_MAX_TURNS = 25;
38
92
  const RECURSION_MULTIPLIER = 3;
39
93
  const ERROR_MESSAGE_MAX_CHARS = 200;
40
94
  const MAX_PENDING_SUBAGENT_UPDATES = 64;
95
+ const TEXT_DELTA_CONTENT_TYPE = `${ContentTypes.TEXT}_delta`;
41
96
 
42
97
  const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
43
98
  additionalContexts: [] as string[],
@@ -119,7 +174,10 @@ type SanitizedStepCompleted =
119
174
  };
120
175
 
121
176
  type SanitizedProcessedToolCall = Partial<
122
- Pick<ProcessedToolCall, 'args' | 'id' | 'name' | 'output' | 'progress' | 'outcome'>
177
+ Pick<
178
+ ProcessedToolCall,
179
+ 'args' | 'id' | 'name' | 'output' | 'progress' | 'outcome'
180
+ >
123
181
  >;
124
182
 
125
183
  type SanitizedRunStepCompleted = {
@@ -150,12 +208,449 @@ type ForwarderCallback = {
150
208
  drain: () => Promise<void>;
151
209
  };
152
210
 
211
+ type StatefulCompiledWorkflow = Omit<CompiledStateWorkflow, 'invoke'> & {
212
+ invoke(
213
+ input: BaseGraphState | Command | null,
214
+ config?: RunnableConfig
215
+ ): Promise<BaseGraphState>;
216
+ getState(config: RunnableConfig): Promise<StateSnapshot>;
217
+ updateState?(
218
+ config: RunnableConfig,
219
+ values: Record<string, unknown>,
220
+ asNode?: string
221
+ ): Promise<RunnableConfig>;
222
+ };
223
+
224
+ type ReplayCheckpointWorkflow = {
225
+ updateState(
226
+ config: RunnableConfig,
227
+ values: { messages: BaseMessage[] },
228
+ asNode: string
229
+ ): Promise<RunnableConfig>;
230
+ };
231
+
232
+ type ActiveChildRun = {
233
+ graph: StandardGraph;
234
+ workflow: StatefulCompiledWorkflow;
235
+ pendingInterrupts: Interrupt[];
236
+ invokeConfig?: RunnableConfig;
237
+ childAgentId: string;
238
+ childRunId: string;
239
+ };
240
+
241
+ type PersistedToolOutput = {
242
+ content: ToolMessage['content'];
243
+ toolCallId: string;
244
+ id?: string;
245
+ name?: string;
246
+ status?: 'success' | 'error';
247
+ additionalKwargs: ToolMessage['additional_kwargs'];
248
+ responseMetadata: ToolMessage['response_metadata'];
249
+ metadata?: Record<string, unknown>;
250
+ additionalContexts: string[];
251
+ resolvedArgs?: Record<string, unknown>;
252
+ referenceContent?: string;
253
+ };
254
+
255
+ type SubagentCheckpointMarker = {
256
+ version: 1;
257
+ parentToolCallId: string;
258
+ lifecycleComplete: true;
259
+ hookSessionId?: string;
260
+ childRunId?: string;
261
+ settledOutput?: PersistedToolOutput;
262
+ };
263
+
264
+ type ChildExecutionIdentity = {
265
+ childRunId: string;
266
+ childThreadId: string;
267
+ approvalExecutionScope: string;
268
+ resumeExecution?: SubagentResumeExecution;
269
+ };
270
+
271
+ type ChildExecutionIdentityParams = {
272
+ threadId?: string;
273
+ parentToolCallId: string;
274
+ parentConfigurable?: Record<string, unknown>;
275
+ };
276
+
153
277
  const LANGGRAPH_RUNTIME_CONFIG_PREFIX = '__pregel_';
278
+ const LANGGRAPH_RESUME_MAP_CONFIG_KEY = '__pregel_resume_map';
154
279
  const LANGGRAPH_CHECKPOINT_CONFIG_KEYS = new Set([
155
280
  'checkpoint_id',
156
281
  'checkpoint_map',
157
282
  'checkpoint_ns',
158
283
  ]);
284
+ const SUBAGENT_CHECKPOINT_MARKER_KEY = '__librechat_subagent_checkpoint';
285
+ const SUBAGENT_HOOK_SESSION_KEY = '__librechat_subagent_hook_session';
286
+ const SUBAGENT_RUN_ID_KEY = '__librechat_subagent_run_id';
287
+ const SUBAGENT_REPLAY_NODE = 'subagent-replay';
288
+
289
+ function isCheckpointSaver(value: unknown): value is BaseCheckpointSaver {
290
+ if (value == null || typeof value !== 'object') {
291
+ return false;
292
+ }
293
+ const candidate = value as Partial<BaseCheckpointSaver>;
294
+ return (
295
+ typeof candidate.getTuple === 'function' &&
296
+ typeof candidate.list === 'function' &&
297
+ typeof candidate.put === 'function' &&
298
+ typeof candidate.putWrites === 'function' &&
299
+ typeof candidate.deleteThread === 'function'
300
+ );
301
+ }
302
+
303
+ function isSubagentCheckpointMarker(
304
+ value: unknown
305
+ ): value is SubagentCheckpointMarker {
306
+ if (value == null || typeof value !== 'object') {
307
+ return false;
308
+ }
309
+ const marker = value as Partial<SubagentCheckpointMarker>;
310
+ return (
311
+ marker.version === 1 &&
312
+ marker.lifecycleComplete === true &&
313
+ typeof marker.parentToolCallId === 'string' &&
314
+ (marker.hookSessionId == null ||
315
+ typeof marker.hookSessionId === 'string') &&
316
+ (marker.childRunId == null || typeof marker.childRunId === 'string') &&
317
+ (marker.settledOutput == null ||
318
+ isPersistedToolOutput(marker.settledOutput))
319
+ );
320
+ }
321
+
322
+ function isPersistedToolOutput(value: unknown): value is PersistedToolOutput {
323
+ if (value == null || typeof value !== 'object') {
324
+ return false;
325
+ }
326
+ const output = value as {
327
+ content?: unknown;
328
+ toolCallId?: unknown;
329
+ status?: unknown;
330
+ additionalKwargs?: unknown;
331
+ responseMetadata?: unknown;
332
+ additionalContexts?: unknown;
333
+ resolvedArgs?: unknown;
334
+ referenceContent?: unknown;
335
+ };
336
+ const contentIsValid =
337
+ typeof output.content === 'string' || Array.isArray(output.content);
338
+ const statusIsValid =
339
+ output.status == null ||
340
+ output.status === 'success' ||
341
+ output.status === 'error';
342
+ const contextsAreValid =
343
+ Array.isArray(output.additionalContexts) &&
344
+ output.additionalContexts.every((context) => typeof context === 'string');
345
+ const resolvedArgsAreValid =
346
+ output.resolvedArgs == null ||
347
+ (typeof output.resolvedArgs === 'object' &&
348
+ !Array.isArray(output.resolvedArgs));
349
+ return (
350
+ contentIsValid &&
351
+ typeof output.toolCallId === 'string' &&
352
+ output.additionalKwargs != null &&
353
+ typeof output.additionalKwargs === 'object' &&
354
+ output.responseMetadata != null &&
355
+ typeof output.responseMetadata === 'object' &&
356
+ statusIsValid &&
357
+ contextsAreValid &&
358
+ resolvedArgsAreValid &&
359
+ (output.referenceContent == null ||
360
+ typeof output.referenceContent === 'string')
361
+ );
362
+ }
363
+
364
+ function getSubagentCheckpointMarker(
365
+ messages: BaseMessage[],
366
+ parentToolCallId: string
367
+ ): SubagentCheckpointMarker | undefined {
368
+ for (let i = messages.length - 1; i >= 0; i--) {
369
+ const marker =
370
+ messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
371
+ if (
372
+ isSubagentCheckpointMarker(marker) &&
373
+ marker.parentToolCallId === parentToolCallId
374
+ ) {
375
+ return marker;
376
+ }
377
+ }
378
+ return undefined;
379
+ }
380
+
381
+ function getSubagentHookSessionId(messages: BaseMessage[]): string | undefined {
382
+ for (let i = messages.length - 1; i >= 0; i--) {
383
+ const sessionId = messages[i].additional_kwargs[SUBAGENT_HOOK_SESSION_KEY];
384
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
385
+ return sessionId;
386
+ }
387
+ }
388
+ return undefined;
389
+ }
390
+
391
+ function getSubagentRunId(messages: BaseMessage[]): string | undefined {
392
+ for (let i = messages.length - 1; i >= 0; i--) {
393
+ const runId = messages[i].additional_kwargs[SUBAGENT_RUN_ID_KEY];
394
+ if (typeof runId === 'string' && runId.length > 0) {
395
+ return runId;
396
+ }
397
+ const marker =
398
+ messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
399
+ if (
400
+ isSubagentCheckpointMarker(marker) &&
401
+ marker.childRunId != null &&
402
+ marker.childRunId.length > 0
403
+ ) {
404
+ return marker.childRunId;
405
+ }
406
+ }
407
+ return undefined;
408
+ }
409
+
410
+ function isSubagentCheckpointMarkerMessage(message: BaseMessage): boolean {
411
+ return isSubagentCheckpointMarker(
412
+ message.additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY]
413
+ );
414
+ }
415
+
416
+ function createSubagentCheckpointMarkerMessage(
417
+ marker: SubagentCheckpointMarker
418
+ ): AIMessage {
419
+ return new AIMessage({
420
+ content: '',
421
+ additional_kwargs: { [SUBAGENT_CHECKPOINT_MARKER_KEY]: marker },
422
+ });
423
+ }
424
+
425
+ function getCheckpointMessages(value: unknown): BaseMessage[] {
426
+ return Array.isArray(value) ? value.filter(BaseMessage.isInstance) : [];
427
+ }
428
+
429
+ function getConfigurableString(
430
+ config: RunnableConfig | undefined,
431
+ key: string
432
+ ): string | undefined {
433
+ const value = config?.configurable?.[key];
434
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
435
+ }
436
+
437
+ function getTupleMessages(tuple: CheckpointTuple | undefined): BaseMessage[] {
438
+ return getCheckpointMessages(tuple?.checkpoint.channel_values.messages);
439
+ }
440
+
441
+ function getCheckpointReference(
442
+ tuple: CheckpointTuple | undefined
443
+ ): SubagentCheckpointReference | undefined {
444
+ const configurable = tuple?.config.configurable;
445
+ const threadId = configurable?.thread_id;
446
+ const checkpointId = configurable?.checkpoint_id;
447
+ const checkpointNs = configurable?.checkpoint_ns ?? '';
448
+ if (
449
+ typeof threadId !== 'string' ||
450
+ threadId.length === 0 ||
451
+ typeof checkpointId !== 'string' ||
452
+ checkpointId.length === 0 ||
453
+ typeof checkpointNs !== 'string'
454
+ ) {
455
+ return undefined;
456
+ }
457
+ return { threadId, checkpointId, checkpointNs };
458
+ }
459
+
460
+ function serializeToolOutput(
461
+ settled: SettledSubagentToolOutput
462
+ ): PersistedToolOutput {
463
+ const { output } = settled;
464
+ return {
465
+ content: output.content,
466
+ toolCallId: output.tool_call_id,
467
+ ...(output.id == null ? {} : { id: output.id }),
468
+ ...(output.name == null ? {} : { name: output.name }),
469
+ ...(output.status == null ? {} : { status: output.status }),
470
+ additionalKwargs: output.additional_kwargs,
471
+ responseMetadata: output.response_metadata,
472
+ ...(output.metadata == null ? {} : { metadata: output.metadata }),
473
+ additionalContexts: settled.additionalContexts,
474
+ ...(settled.resolvedArgs == null
475
+ ? {}
476
+ : { resolvedArgs: settled.resolvedArgs }),
477
+ ...(settled.referenceContent == null
478
+ ? {}
479
+ : { referenceContent: settled.referenceContent }),
480
+ };
481
+ }
482
+
483
+ function deserializeToolOutput(
484
+ output: PersistedToolOutput
485
+ ): SettledSubagentToolOutput {
486
+ return {
487
+ output: new ToolMessage({
488
+ content: output.content,
489
+ tool_call_id: output.toolCallId,
490
+ ...(output.id == null ? {} : { id: output.id }),
491
+ ...(output.name == null ? {} : { name: output.name }),
492
+ ...(output.status == null ? {} : { status: output.status }),
493
+ additional_kwargs: output.additionalKwargs,
494
+ response_metadata: output.responseMetadata,
495
+ ...(output.metadata == null ? {} : { metadata: output.metadata }),
496
+ }),
497
+ additionalContexts: output.additionalContexts,
498
+ ...(output.resolvedArgs == null
499
+ ? {}
500
+ : { resolvedArgs: output.resolvedArgs }),
501
+ ...(output.referenceContent == null
502
+ ? {}
503
+ : { referenceContent: output.referenceContent }),
504
+ };
505
+ }
506
+
507
+ function getParentCheckpointFork(
508
+ configurable: Record<string, unknown> | undefined
509
+ ): string {
510
+ const checkpointId = configurable?.checkpoint_id;
511
+ return typeof checkpointId === 'string' && checkpointId.length > 0
512
+ ? checkpointId
513
+ : 'root';
514
+ }
515
+
516
+ function getResumeAttemptId(
517
+ configurable: Record<string, unknown> | undefined,
518
+ fallback: string
519
+ ): string {
520
+ const attemptId = configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
521
+ return typeof attemptId === 'string' && attemptId.length > 0
522
+ ? attemptId
523
+ : fallback;
524
+ }
525
+
526
+ function getApprovalExecutionScope(
527
+ childRunId: string,
528
+ resumeAttemptId: string
529
+ ): string {
530
+ return `subagent-approval:${Buffer.from(
531
+ JSON.stringify([childRunId, resumeAttemptId])
532
+ ).toString('base64url')}`;
533
+ }
534
+
535
+ function getChildThreadId(args: {
536
+ parentRunId: string;
537
+ parentAgentId?: string;
538
+ threadId?: string;
539
+ parentToolCallId: string;
540
+ parentConfigurable?: Record<string, unknown>;
541
+ branchId?: string;
542
+ }): string {
543
+ const durableParentId = args.threadId ?? args.parentRunId;
544
+ const parentFork = getParentCheckpointFork(args.parentConfigurable);
545
+ const parentBatch =
546
+ args.parentConfigurable?.[SUBAGENT_PARENT_BATCH_CONFIG_KEY];
547
+ const identity = [
548
+ durableParentId,
549
+ parentFork,
550
+ args.parentAgentId ?? 'agent',
551
+ args.parentToolCallId,
552
+ typeof parentBatch === 'string' && parentBatch.length > 0
553
+ ? parentBatch
554
+ : 'batch',
555
+ ];
556
+ if (args.branchId != null) {
557
+ identity.push(args.branchId);
558
+ }
559
+ return `subagent:${Buffer.from(JSON.stringify(identity)).toString('base64url')}`;
560
+ }
561
+
562
+ function isToolApprovalPayload(
563
+ value: unknown
564
+ ): value is ToolApprovalInterruptPayload {
565
+ return (
566
+ value != null &&
567
+ typeof value === 'object' &&
568
+ (value as { type?: unknown }).type === 'tool_approval'
569
+ );
570
+ }
571
+
572
+ function addSubagentScope(
573
+ interrupts: Interrupt[],
574
+ scope: NonNullable<ToolApprovalInterruptPayload['subagent']>,
575
+ resumeManifest?: SubagentResumeManifest
576
+ ): Interrupt[] {
577
+ return interrupts.map((childInterrupt) => {
578
+ const payload = isToolApprovalPayload(childInterrupt.value)
579
+ ? {
580
+ ...childInterrupt.value,
581
+ subagent: childInterrupt.value.subagent ?? scope,
582
+ }
583
+ : childInterrupt.value;
584
+ return {
585
+ ...childInterrupt,
586
+ value:
587
+ resumeManifest == null
588
+ ? payload
589
+ : attachSubagentResumeManifest(payload, resumeManifest),
590
+ };
591
+ });
592
+ }
593
+
594
+ type ToolApprovalResumeValue = ToolApprovalDecision[] | ToolApprovalDecisionMap;
595
+
596
+ function getChildResumeMap(
597
+ pendingInterrupts: Interrupt[],
598
+ parentConfigurable: Record<string, unknown> | undefined
599
+ ): Record<string, ToolApprovalResumeValue> | undefined {
600
+ const resumeMap = parentConfigurable?.[LANGGRAPH_RESUME_MAP_CONFIG_KEY];
601
+ if (resumeMap == null || typeof resumeMap !== 'object') {
602
+ return undefined;
603
+ }
604
+
605
+ const parentResumeMap = resumeMap as Record<string, ToolApprovalResumeValue>;
606
+ const childResumeMap: Record<string, ToolApprovalResumeValue> = {};
607
+ for (const childInterrupt of pendingInterrupts) {
608
+ const interruptId = childInterrupt.id;
609
+ if (
610
+ typeof interruptId === 'string' &&
611
+ Object.prototype.hasOwnProperty.call(parentResumeMap, interruptId)
612
+ ) {
613
+ childResumeMap[interruptId] = parentResumeMap[interruptId];
614
+ }
615
+ }
616
+ return Object.keys(childResumeMap).length > 0 ? childResumeMap : undefined;
617
+ }
618
+
619
+ function getPersistedInterrupts(snapshot: StateSnapshot): Interrupt[] {
620
+ const interrupts: Interrupt[] = [];
621
+ for (const task of snapshot.tasks) {
622
+ for (const pendingInterrupt of task.interrupts) {
623
+ interrupts.push(pendingInterrupt);
624
+ }
625
+ }
626
+ return interrupts;
627
+ }
628
+
629
+ function getPersistedMessages(
630
+ snapshot: StateSnapshot
631
+ ): BaseMessage[] | undefined {
632
+ if (snapshot.values == null || typeof snapshot.values !== 'object') {
633
+ return undefined;
634
+ }
635
+ const values = snapshot.values as { messages?: BaseMessage[] };
636
+ if (!Array.isArray(values.messages) || values.messages.length === 0) {
637
+ return undefined;
638
+ }
639
+ const messages = values.messages.filter(
640
+ (message) => !isSubagentCheckpointMarkerMessage(message)
641
+ );
642
+ return messages.length > 0 ? messages : undefined;
643
+ }
644
+
645
+ function createReplayCheckpointWorkflow(
646
+ checkpointer: BaseCheckpointSaver
647
+ ): ReplayCheckpointWorkflow {
648
+ return new StateGraph(MessagesAnnotation)
649
+ .addNode(SUBAGENT_REPLAY_NODE, (state) => state)
650
+ .addEdge(START, SUBAGENT_REPLAY_NODE)
651
+ .addEdge(SUBAGENT_REPLAY_NODE, END)
652
+ .compile({ checkpointer }) as ReplayCheckpointWorkflow;
653
+ }
159
654
 
160
655
  export type SubagentExecuteParams = {
161
656
  description: string;
@@ -187,11 +682,12 @@ export type SubagentExecuteParams = {
187
682
  *
188
683
  * Inheritance details (verified empirically against LangGraph):
189
684
  * - host-set keys propagate as-is into the child's tool dispatches;
190
- * - `thread_id` propagates (with `childRunId` as a fallback when
191
- * parent did not supply one) — matches the "subagent is part of
192
- * the same conversation" mental model and aligns with the
193
- * `sessionId: this.parentRunId` convention this executor already
194
- * uses for `SubagentStart` / `SubagentStop` hooks;
685
+ * - with nested HITL enabled, `thread_id` is replaced with a stable
686
+ * child checkpoint id derived from the parent's durable thread id,
687
+ * checkpoint fork, parent agent id, and spawning tool call id so parent
688
+ * and child checkpoints cannot collide, sibling parent forks stay
689
+ * isolated, and reconstruction returns to the same child checkpoint;
690
+ * parent-scoped hook lookup remains keyed by the inherited `run_id`;
195
691
  * - `parent_run_id` propagates when the host put it on parent's
196
692
  * configurable;
197
693
  * - `run_id` is *overwritten by the LangGraph runtime* at child
@@ -251,6 +747,11 @@ export type SubagentExecutorOptions = {
251
747
  * revert to the defaults.
252
748
  */
253
749
  streamLimits?: StandardGraphInput['streamLimits'];
750
+ humanInTheLoop?: HumanInTheLoopConfig;
751
+ /** Shared durable saver used to recover outer tool lifecycle results before
752
+ * parent hooks re-enter after a process rebuild. Narrowed structurally at
753
+ * construction because graph compile options also permit framework flags. */
754
+ checkpointer?: unknown;
254
755
  /** Remaining nesting budget. 0 or negative blocks execution. */
255
756
  maxDepth?: number;
256
757
  /**
@@ -300,9 +801,27 @@ export class SubagentExecutor {
300
801
  private readonly langfuse?: StandardGraphInput['langfuse'];
301
802
  private readonly tokenCounter?: TokenCounter;
302
803
  private readonly streamLimits?: StandardGraphInput['streamLimits'];
804
+ private readonly humanInTheLoop?: HumanInTheLoopConfig;
805
+ private readonly checkpointer?: BaseCheckpointSaver;
303
806
  private readonly maxDepth: number;
304
807
  private readonly createChildGraph: ChildGraphFactory;
305
808
  private readonly usageSink?: SubagentUsageSink;
809
+ private readonly checkpointThreadIds = new Set<string>();
810
+ private readonly startedChildRuns = new Set<string>();
811
+ private readonly completedChildRuns = new Set<string>();
812
+ private readonly completedChildResults = new Map<
813
+ string,
814
+ SubagentExecuteResult
815
+ >();
816
+ private readonly childExecutionIdentities = new Map<
817
+ string,
818
+ Pick<
819
+ ChildExecutionIdentity,
820
+ 'childRunId' | 'childThreadId' | 'approvalExecutionScope'
821
+ >
822
+ >();
823
+ private readonly activeChildRuns = new Map<string, ActiveChildRun>();
824
+ private replayCheckpointWorkflow?: ReplayCheckpointWorkflow;
306
825
  private readonly resolveParentHandlerRegistry?: () =>
307
826
  | HandlerRegistry
308
827
  | undefined;
@@ -317,6 +836,10 @@ export class SubagentExecutor {
317
836
  this.langfuse = options.langfuse;
318
837
  this.tokenCounter = options.tokenCounter;
319
838
  this.streamLimits = options.streamLimits;
839
+ this.humanInTheLoop = options.humanInTheLoop;
840
+ this.checkpointer = isCheckpointSaver(options.checkpointer)
841
+ ? options.checkpointer
842
+ : undefined;
320
843
  this.maxDepth = options.maxDepth ?? 1;
321
844
  this.createChildGraph = options.createChildGraph;
322
845
  this.usageSink = options.usageSink;
@@ -350,6 +873,533 @@ export class SubagentExecutor {
350
873
  return this.resolveParentHandlerRegistry?.();
351
874
  }
352
875
 
876
+ /**
877
+ * Keeps the original child thread as an immutable resume source once a
878
+ * different parent Run reconstructs it. Each rebuilt parent gets a private
879
+ * checkpoint fork, while the persisted child run ID remains stable for
880
+ * activity and usage correlation across the interrupt boundary.
881
+ */
882
+ private async resolveChildExecutionIdentity(
883
+ params: ChildExecutionIdentityParams
884
+ ): Promise<ChildExecutionIdentity> {
885
+ const currentChildRunId = `${this.parentRunId}_sub_${params.parentToolCallId}`;
886
+ const resumeAttemptId = getResumeAttemptId(
887
+ params.parentConfigurable,
888
+ this.parentRunId
889
+ );
890
+ const baseChildThreadId = getChildThreadId({
891
+ parentRunId: this.parentRunId,
892
+ parentAgentId: this.parentAgentId,
893
+ threadId: params.threadId,
894
+ parentToolCallId: params.parentToolCallId,
895
+ parentConfigurable: params.parentConfigurable,
896
+ });
897
+ if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null) {
898
+ return {
899
+ childRunId: currentChildRunId,
900
+ childThreadId: baseChildThreadId,
901
+ approvalExecutionScope: currentChildRunId,
902
+ };
903
+ }
904
+
905
+ const resumeManifest = getSubagentResumeManifest(params.parentConfigurable);
906
+ const resumeExecution = resumeManifest?.executions.find(
907
+ (execution) => execution.parentToolCallId === params.parentToolCallId
908
+ );
909
+ const branchChildThreadId = getChildThreadId({
910
+ parentRunId: this.parentRunId,
911
+ parentAgentId: this.parentAgentId,
912
+ threadId: params.threadId,
913
+ parentToolCallId: params.parentToolCallId,
914
+ parentConfigurable: params.parentConfigurable,
915
+ branchId: resumeAttemptId,
916
+ });
917
+ if (resumeExecution != null) {
918
+ const approvalExecutionScope = getApprovalExecutionScope(
919
+ resumeExecution.childRunId,
920
+ resumeAttemptId
921
+ );
922
+ await this.forkCheckpointSnapshot(
923
+ resumeExecution.checkpoints,
924
+ branchChildThreadId
925
+ );
926
+ this.hookRegistry?.restorePendingToolApprovals(
927
+ approvalExecutionScope,
928
+ approvalExecutionScope,
929
+ resumeExecution.approvalReplays
930
+ );
931
+ this.checkpointThreadIds.add(branchChildThreadId);
932
+ return {
933
+ childRunId: resumeExecution.childRunId,
934
+ childThreadId: branchChildThreadId,
935
+ approvalExecutionScope,
936
+ resumeExecution,
937
+ };
938
+ }
939
+
940
+ const branchTuple = await this.checkpointer.getTuple({
941
+ configurable: { thread_id: branchChildThreadId },
942
+ });
943
+ if (branchTuple != null) {
944
+ const childRunId =
945
+ getSubagentRunId(getTupleMessages(branchTuple)) ?? currentChildRunId;
946
+ this.checkpointThreadIds.add(branchChildThreadId);
947
+ return {
948
+ childRunId,
949
+ childThreadId: branchChildThreadId,
950
+ approvalExecutionScope: getApprovalExecutionScope(
951
+ childRunId,
952
+ resumeAttemptId
953
+ ),
954
+ };
955
+ }
956
+
957
+ const baseTuple = await this.checkpointer.getTuple({
958
+ configurable: { thread_id: baseChildThreadId },
959
+ });
960
+ const persistedChildRunId = getSubagentRunId(getTupleMessages(baseTuple));
961
+ if (
962
+ baseTuple == null ||
963
+ persistedChildRunId == null ||
964
+ persistedChildRunId === currentChildRunId
965
+ ) {
966
+ this.checkpointThreadIds.add(baseChildThreadId);
967
+ return {
968
+ childRunId: persistedChildRunId ?? currentChildRunId,
969
+ childThreadId: baseChildThreadId,
970
+ approvalExecutionScope: getApprovalExecutionScope(
971
+ persistedChildRunId ?? currentChildRunId,
972
+ resumeAttemptId
973
+ ),
974
+ };
975
+ }
976
+
977
+ const sourceCheckpoints =
978
+ await this.getLatestCheckpointSnapshot(baseChildThreadId);
979
+ if (sourceCheckpoints.length === 0) {
980
+ throw new Error(
981
+ `Cannot fork subagent checkpoint thread "${baseChildThreadId}" without a checkpoint ID.`
982
+ );
983
+ }
984
+ await this.forkCheckpointSnapshot(sourceCheckpoints, branchChildThreadId);
985
+ this.checkpointThreadIds.add(branchChildThreadId);
986
+ return {
987
+ childRunId: persistedChildRunId,
988
+ childThreadId: branchChildThreadId,
989
+ approvalExecutionScope: getApprovalExecutionScope(
990
+ persistedChildRunId,
991
+ resumeAttemptId
992
+ ),
993
+ };
994
+ }
995
+
996
+ /** Captures one exact checkpoint head per namespace for a child thread. */
997
+ private async getLatestCheckpointSnapshot(
998
+ threadId: string
999
+ ): Promise<SubagentCheckpointReference[]> {
1000
+ if (this.checkpointer == null) {
1001
+ return [];
1002
+ }
1003
+ const checkpointsByNamespace = new Map<
1004
+ string,
1005
+ SubagentCheckpointReference
1006
+ >();
1007
+ for await (const tuple of this.checkpointer.list({
1008
+ configurable: { thread_id: threadId },
1009
+ })) {
1010
+ const checkpoint = getCheckpointReference(tuple);
1011
+ const current =
1012
+ checkpoint == null
1013
+ ? undefined
1014
+ : checkpointsByNamespace.get(checkpoint.checkpointNs);
1015
+ if (
1016
+ checkpoint != null &&
1017
+ (current == null ||
1018
+ checkpoint.checkpointId.localeCompare(current.checkpointId) > 0)
1019
+ ) {
1020
+ checkpointsByNamespace.set(checkpoint.checkpointNs, checkpoint);
1021
+ }
1022
+ }
1023
+ return [...checkpointsByNamespace.values()].sort((left, right) =>
1024
+ left.checkpointNs.localeCompare(right.checkpointNs)
1025
+ );
1026
+ }
1027
+
1028
+ /** Copies exact checkpoint lineages, including pending task writes. */
1029
+ private async forkCheckpointSnapshot(
1030
+ sources: ReadonlyArray<SubagentCheckpointReference>,
1031
+ targetThreadId: string
1032
+ ): Promise<void> {
1033
+ if (
1034
+ this.checkpointer == null ||
1035
+ sources.length === 0 ||
1036
+ sources.every((source) => source.threadId === targetThreadId)
1037
+ ) {
1038
+ return;
1039
+ }
1040
+ for (const source of sources) {
1041
+ const tuples: CheckpointTuple[] = [];
1042
+ const visited = new Set<string>();
1043
+ let tuple = await this.checkpointer.getTuple({
1044
+ configurable: {
1045
+ thread_id: source.threadId,
1046
+ checkpoint_ns: source.checkpointNs,
1047
+ checkpoint_id: source.checkpointId,
1048
+ },
1049
+ });
1050
+ if (tuple == null) {
1051
+ throw new Error(
1052
+ `Subagent checkpoint "${source.checkpointId}" is unavailable.`
1053
+ );
1054
+ }
1055
+ for (;;) {
1056
+ const reference = getCheckpointReference(tuple);
1057
+ if (reference == null) {
1058
+ throw new Error(
1059
+ 'Subagent checkpoint lineage contains an invalid tuple.'
1060
+ );
1061
+ }
1062
+ if (
1063
+ reference.threadId !== source.threadId ||
1064
+ reference.checkpointNs !== source.checkpointNs
1065
+ ) {
1066
+ throw new Error(
1067
+ 'Subagent checkpoint lineage escapes its source namespace.'
1068
+ );
1069
+ }
1070
+ const lineageKey = JSON.stringify([
1071
+ reference.threadId,
1072
+ reference.checkpointNs,
1073
+ reference.checkpointId,
1074
+ ]);
1075
+ if (visited.has(lineageKey)) {
1076
+ throw new Error('Subagent checkpoint lineage contains a cycle.');
1077
+ }
1078
+ visited.add(lineageKey);
1079
+ tuples.push(tuple);
1080
+ if (tuple.parentConfig == null) {
1081
+ break;
1082
+ }
1083
+ tuple = await this.checkpointer.getTuple(tuple.parentConfig);
1084
+ if (tuple == null) {
1085
+ throw new Error('Subagent checkpoint lineage is incomplete.');
1086
+ }
1087
+ }
1088
+ tuples.reverse();
1089
+
1090
+ for (const lineageTuple of tuples) {
1091
+ const checkpointNs =
1092
+ getConfigurableString(lineageTuple.config, 'checkpoint_ns') ?? '';
1093
+ const parentCheckpointId = getConfigurableString(
1094
+ lineageTuple.parentConfig,
1095
+ 'checkpoint_id'
1096
+ );
1097
+ const storedConfig = await this.checkpointer.put(
1098
+ {
1099
+ configurable: {
1100
+ thread_id: targetThreadId,
1101
+ checkpoint_ns: checkpointNs,
1102
+ ...(parentCheckpointId == null
1103
+ ? {}
1104
+ : { checkpoint_id: parentCheckpointId }),
1105
+ },
1106
+ },
1107
+ copyCheckpoint(lineageTuple.checkpoint),
1108
+ lineageTuple.metadata ?? {
1109
+ source: 'fork',
1110
+ step: -1,
1111
+ parents: {},
1112
+ },
1113
+ lineageTuple.checkpoint.channel_versions
1114
+ );
1115
+ const writesByTask = new Map<string, Array<[string, unknown]>>();
1116
+ for (const [taskId, channel, value] of lineageTuple.pendingWrites ??
1117
+ []) {
1118
+ let writes = writesByTask.get(taskId);
1119
+ if (writes == null) {
1120
+ writes = [];
1121
+ writesByTask.set(taskId, writes);
1122
+ }
1123
+ writes.push([channel, value]);
1124
+ }
1125
+ for (const [taskId, writes] of writesByTask) {
1126
+ await this.checkpointer.putWrites(storedConfig, writes, taskId);
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+
1132
+ private async createResumeManifest(
1133
+ parentToolCallIds?: ReadonlySet<string>
1134
+ ): Promise<SubagentResumeManifest | undefined> {
1135
+ if (this.checkpointer == null) {
1136
+ return undefined;
1137
+ }
1138
+ const executions: SubagentResumeExecution[] = [];
1139
+ for (const [parentToolCallId, identity] of this.childExecutionIdentities) {
1140
+ if (
1141
+ parentToolCallIds != null &&
1142
+ !parentToolCallIds.has(parentToolCallId)
1143
+ ) {
1144
+ continue;
1145
+ }
1146
+ const checkpoints = await this.getLatestCheckpointSnapshot(
1147
+ identity.childThreadId
1148
+ );
1149
+ if (checkpoints.length === 0) {
1150
+ continue;
1151
+ }
1152
+ const activeChildRun = this.activeChildRuns.get(identity.childThreadId);
1153
+ const configuredHookSessionId =
1154
+ activeChildRun?.invokeConfig?.configurable?.run_id;
1155
+ const hookSessionId =
1156
+ typeof configuredHookSessionId === 'string' &&
1157
+ configuredHookSessionId.length > 0
1158
+ ? configuredHookSessionId
1159
+ : this.parentRunId;
1160
+ const approvalReplays =
1161
+ this.hookRegistry?.snapshotPendingToolApprovals(
1162
+ identity.approvalExecutionScope,
1163
+ identity.approvalExecutionScope
1164
+ ) ?? [];
1165
+ const descendant = activeChildRun?.pendingInterrupts
1166
+ .map((pendingInterrupt) =>
1167
+ getSubagentResumeManifest(pendingInterrupt.value)
1168
+ )
1169
+ .find((manifest) => manifest != null);
1170
+ const graphState = activeChildRun?.graph.createSubagentResumeState(
1171
+ hookSessionId
1172
+ ) ?? {
1173
+ toolCallSteps: [],
1174
+ toolSessions: [],
1175
+ toolNodes: [],
1176
+ eagerToolUsage: [],
1177
+ eagerToolSuppressions: [],
1178
+ };
1179
+ executions.push({
1180
+ parentToolCallId,
1181
+ childRunId: identity.childRunId,
1182
+ approvalExecutionScope: identity.approvalExecutionScope,
1183
+ checkpoints,
1184
+ graphState,
1185
+ approvalReplays,
1186
+ ...(descendant == null ? {} : { descendant }),
1187
+ });
1188
+ }
1189
+ if (executions.length === 0) {
1190
+ return undefined;
1191
+ }
1192
+ return {
1193
+ version: 1,
1194
+ executions,
1195
+ };
1196
+ }
1197
+
1198
+ getResumeManifest(
1199
+ parentToolCallIds?: ReadonlySet<string>
1200
+ ): Promise<SubagentResumeManifest | undefined> {
1201
+ return this.createResumeManifest(parentToolCallIds);
1202
+ }
1203
+
1204
+ getChildCheckpointThreadIds(): string[] {
1205
+ const threadIds = new Set(this.checkpointThreadIds);
1206
+ for (const activeChildRun of this.activeChildRuns.values()) {
1207
+ for (const threadId of this.getGraphChildCheckpointThreadIds(
1208
+ activeChildRun.graph
1209
+ )) {
1210
+ threadIds.add(threadId);
1211
+ }
1212
+ }
1213
+ return [...threadIds];
1214
+ }
1215
+
1216
+ resetCheckpointThreadIds(): void {
1217
+ this.checkpointThreadIds.clear();
1218
+ }
1219
+
1220
+ private getGraphChildCheckpointThreadIds(graph: StandardGraph): string[] {
1221
+ const checkpointGraph = graph as {
1222
+ getChildCheckpointThreadIds?: () => string[];
1223
+ };
1224
+ return checkpointGraph.getChildCheckpointThreadIds?.() ?? [];
1225
+ }
1226
+
1227
+ private clearChildGraph(graph: StandardGraph): void {
1228
+ for (const threadId of this.getGraphChildCheckpointThreadIds(graph)) {
1229
+ this.checkpointThreadIds.add(threadId);
1230
+ }
1231
+ graph.clearHeavyState();
1232
+ }
1233
+
1234
+ clearHeavyState(): void {
1235
+ for (const activeChildRun of this.activeChildRuns.values()) {
1236
+ this.clearChildGraph(activeChildRun.graph);
1237
+ }
1238
+ this.activeChildRuns.clear();
1239
+ this.completedChildResults.clear();
1240
+ for (const identity of this.childExecutionIdentities.values()) {
1241
+ this.hookRegistry?.clearSession(identity.approvalExecutionScope);
1242
+ }
1243
+ this.childExecutionIdentities.clear();
1244
+ this.startedChildRuns.clear();
1245
+ this.completedChildRuns.clear();
1246
+ this.replayCheckpointWorkflow = undefined;
1247
+ }
1248
+
1249
+ async getSettledToolOutput(
1250
+ call: ToolCall,
1251
+ config: RunnableConfig
1252
+ ): Promise<SettledSubagentToolOutput | undefined> {
1253
+ const parentToolCallId = call.id;
1254
+ if (
1255
+ this.humanInTheLoop?.enabled !== true ||
1256
+ this.checkpointer == null ||
1257
+ parentToolCallId == null ||
1258
+ parentToolCallId === ''
1259
+ ) {
1260
+ return undefined;
1261
+ }
1262
+ const parentConfigurable = config.configurable as
1263
+ | Record<string, unknown>
1264
+ | undefined;
1265
+ const threadId = parentConfigurable?.thread_id;
1266
+ const { childRunId, childThreadId, approvalExecutionScope } =
1267
+ await this.resolveChildExecutionIdentity({
1268
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1269
+ parentToolCallId,
1270
+ parentConfigurable,
1271
+ });
1272
+ this.childExecutionIdentities.set(parentToolCallId, {
1273
+ childRunId,
1274
+ childThreadId,
1275
+ approvalExecutionScope,
1276
+ });
1277
+ this.checkpointThreadIds.add(childThreadId);
1278
+ const checkpoint = await this.checkpointer.getTuple({
1279
+ configurable: { thread_id: childThreadId },
1280
+ });
1281
+ const messages = getCheckpointMessages(
1282
+ checkpoint?.checkpoint.channel_values.messages
1283
+ );
1284
+ const marker = getSubagentCheckpointMarker(messages, parentToolCallId);
1285
+ const persistedHookSessionId =
1286
+ marker?.hookSessionId ?? getSubagentHookSessionId(messages);
1287
+ const currentHookSessionId = parentConfigurable?.run_id;
1288
+ if (
1289
+ persistedHookSessionId != null &&
1290
+ typeof currentHookSessionId === 'string' &&
1291
+ currentHookSessionId.length > 0
1292
+ ) {
1293
+ this.hookRegistry?.copySession(
1294
+ persistedHookSessionId,
1295
+ currentHookSessionId
1296
+ );
1297
+ }
1298
+ return marker?.settledOutput == null
1299
+ ? undefined
1300
+ : deserializeToolOutput(marker.settledOutput);
1301
+ }
1302
+
1303
+ async persistSettledToolOutput(
1304
+ call: ToolCall,
1305
+ config: RunnableConfig,
1306
+ settled: SettledSubagentToolOutput
1307
+ ): Promise<void> {
1308
+ const parentToolCallId = call.id;
1309
+ if (
1310
+ this.humanInTheLoop?.enabled !== true ||
1311
+ this.checkpointer == null ||
1312
+ parentToolCallId == null ||
1313
+ parentToolCallId === ''
1314
+ ) {
1315
+ return;
1316
+ }
1317
+ const parentConfigurable = config.configurable as
1318
+ | Record<string, unknown>
1319
+ | undefined;
1320
+ const threadId = parentConfigurable?.thread_id;
1321
+ const { childRunId, childThreadId, approvalExecutionScope } =
1322
+ await this.resolveChildExecutionIdentity({
1323
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1324
+ parentToolCallId,
1325
+ parentConfigurable,
1326
+ });
1327
+ this.childExecutionIdentities.set(parentToolCallId, {
1328
+ childRunId,
1329
+ childThreadId,
1330
+ approvalExecutionScope,
1331
+ });
1332
+ this.checkpointThreadIds.add(childThreadId);
1333
+ const activeChildRun = this.activeChildRuns.get(childThreadId);
1334
+ const persistedOutput = serializeToolOutput(settled);
1335
+ if (activeChildRun != null) {
1336
+ await this.persistChildCheckpointMarker(
1337
+ activeChildRun,
1338
+ parentToolCallId,
1339
+ persistedOutput
1340
+ );
1341
+ this.clearChildGraph(activeChildRun.graph);
1342
+ this.activeChildRuns.delete(childThreadId);
1343
+ return;
1344
+ }
1345
+ this.replayCheckpointWorkflow ??= createReplayCheckpointWorkflow(
1346
+ this.checkpointer
1347
+ );
1348
+ await this.replayCheckpointWorkflow.updateState(
1349
+ { configurable: { thread_id: childThreadId } },
1350
+ {
1351
+ messages: [
1352
+ createSubagentCheckpointMarkerMessage({
1353
+ version: 1,
1354
+ parentToolCallId,
1355
+ lifecycleComplete: true,
1356
+ hookSessionId:
1357
+ typeof parentConfigurable?.run_id === 'string'
1358
+ ? parentConfigurable.run_id
1359
+ : this.parentRunId,
1360
+ childRunId,
1361
+ settledOutput: persistedOutput,
1362
+ }),
1363
+ ],
1364
+ },
1365
+ SUBAGENT_REPLAY_NODE
1366
+ );
1367
+ }
1368
+
1369
+ private async persistChildCheckpointMarker(
1370
+ activeChildRun: ActiveChildRun,
1371
+ parentToolCallId: string,
1372
+ settledOutput?: PersistedToolOutput
1373
+ ): Promise<void> {
1374
+ if (
1375
+ this.humanInTheLoop?.enabled !== true ||
1376
+ activeChildRun.workflow.updateState == null ||
1377
+ activeChildRun.invokeConfig == null
1378
+ ) {
1379
+ return;
1380
+ }
1381
+ await activeChildRun.workflow.updateState(
1382
+ activeChildRun.invokeConfig,
1383
+ {
1384
+ messages: [
1385
+ createSubagentCheckpointMarkerMessage({
1386
+ version: 1,
1387
+ parentToolCallId,
1388
+ lifecycleComplete: true,
1389
+ hookSessionId:
1390
+ typeof activeChildRun.invokeConfig.configurable?.run_id ===
1391
+ 'string'
1392
+ ? activeChildRun.invokeConfig.configurable.run_id
1393
+ : this.parentRunId,
1394
+ childRunId: activeChildRun.childRunId,
1395
+ ...(settledOutput == null ? {} : { settledOutput }),
1396
+ }),
1397
+ ],
1398
+ },
1399
+ activeChildRun.childAgentId
1400
+ );
1401
+ }
1402
+
353
1403
  async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
354
1404
  const { description, subagentType, threadId, parentToolCallId } = params;
355
1405
  /** Captured ONCE per execution, preferring the controller the parent
@@ -378,39 +1428,41 @@ export class SubagentExecutor {
378
1428
  };
379
1429
  }
380
1430
 
381
- const childAgentId =
382
- config.agentInputs.agentId ||
383
- `${this.parentAgentId ?? 'agent'}_sub_${nanoid(8)}`;
384
-
385
1431
  if (
386
- this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) === true
1432
+ this.humanInTheLoop?.enabled === true &&
1433
+ (parentToolCallId == null || parentToolCallId === '')
387
1434
  ) {
388
- const hookResult = await executeHooks({
389
- registry: this.hookRegistry,
390
- input: {
391
- hook_event_name: 'SubagentStart',
392
- runId: this.parentRunId,
393
- threadId,
394
- parentAgentId: this.parentAgentId,
395
- agentId: childAgentId,
396
- agentType: subagentType,
397
- inputs: [new HumanMessage(description)],
398
- },
399
- sessionId: this.parentRunId,
400
- matchQuery: subagentType,
401
- }).catch((): AggregatedHookResult => HOOK_FALLBACK);
1435
+ return {
1436
+ content:
1437
+ 'Error: Resumable subagent execution requires a parent tool call ID.',
1438
+ messages: [],
1439
+ };
1440
+ }
402
1441
 
403
- /**
404
- * `ask` is treated identically to `deny` in the subagent context:
405
- * subagents are non-interactive, so there is no prompt path for `ask`.
406
- * Both decisions block execution and return a "Blocked" tool result.
407
- */
408
- if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
409
- return {
410
- content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
411
- messages: [],
412
- };
413
- }
1442
+ const executionSuffix = parentToolCallId ?? nanoid(8);
1443
+ const {
1444
+ childRunId,
1445
+ childThreadId,
1446
+ approvalExecutionScope,
1447
+ resumeExecution,
1448
+ } = await this.resolveChildExecutionIdentity({
1449
+ threadId,
1450
+ parentToolCallId: executionSuffix,
1451
+ parentConfigurable: params.parentConfigurable,
1452
+ });
1453
+ this.childExecutionIdentities.set(executionSuffix, {
1454
+ childRunId,
1455
+ childThreadId,
1456
+ approvalExecutionScope,
1457
+ });
1458
+ const childExecutionKey = childThreadId;
1459
+ const childAgentId =
1460
+ config.agentInputs.agentId ||
1461
+ `${this.parentAgentId ?? 'agent'}_sub_${executionSuffix}`;
1462
+ const completedChildResult =
1463
+ this.completedChildResults.get(childExecutionKey);
1464
+ if (completedChildResult != null) {
1465
+ return completedChildResult;
414
1466
  }
415
1467
 
416
1468
  const parentRegistry = this.getParentHandlerRegistry();
@@ -432,42 +1484,43 @@ export class SubagentExecutor {
432
1484
  this.maxDepth,
433
1485
  /* keepToolDefinitions */ hasToolExecuteHandler
434
1486
  );
435
- const childRunId = `${this.parentRunId}_sub_${nanoid(8)}`;
436
1487
  const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
437
1488
 
438
1489
  const hostUsageSink = this.usageSink;
439
- const childGraph = this.createChildGraph({
440
- runId: childRunId,
441
- signal: childSignal,
442
- agents: [childInputs],
443
- langfuse: this.langfuse,
444
- tokenCounter: this.tokenCounter,
445
- streamLimits: this.streamLimits,
446
- subagentScope: true,
447
- /**
448
- * Forwarded so the child graph's own `SubagentExecutor` (created in
449
- * its `createAgentNode` when `allowNested` keeps subagentConfigs)
450
- * reports nested-child usage through the same host sink. Each nesting
451
- * level attaches its own capture callback `workflow.invoke` replaces
452
- * the inherited callback chain, so a single top-level handler would
453
- * never see grandchild model calls.
454
- *
455
- * The wrapper rewrites `runId` to THIS executor's parent run: nested
456
- * executors emit with their own `parentRunId` (a `*_sub_*` child id),
457
- * and each wrapper layer rewrites upward, so by the time an event
458
- * reaches the host sink its `runId` is the ROOT run — hosts keying
459
- * billing by run id never see intermediate child run ids there
460
- * (`subagentRunId` still identifies the emitting child).
461
- */
462
- subagentUsageSink:
463
- hostUsageSink == null
464
- ? undefined
465
- : /** Returns the host sink's result so async sinks stay awaited
466
- * through every wrapper layer. */
467
- (event): void | Promise<void> =>
468
- hostUsageSink({ ...event, runId: this.parentRunId }),
469
- });
470
-
1490
+ const cachedChildRun = this.activeChildRuns.get(childExecutionKey);
1491
+ const childGraph =
1492
+ cachedChildRun?.graph ??
1493
+ this.createChildGraph({
1494
+ runId: childRunId,
1495
+ signal: childSignal,
1496
+ agents: [childInputs],
1497
+ langfuse: this.langfuse,
1498
+ tokenCounter: this.tokenCounter,
1499
+ streamLimits: this.streamLimits,
1500
+ subagentScope: true,
1501
+ /**
1502
+ * Forwarded so the child graph's own `SubagentExecutor` (created in
1503
+ * its `createAgentNode` when `allowNested` keeps subagentConfigs)
1504
+ * reports nested-child usage through the same host sink. Each nesting
1505
+ * level attaches its own capture callback — `workflow.invoke` replaces
1506
+ * the inherited callback chain, so a single top-level handler would
1507
+ * never see grandchild model calls.
1508
+ *
1509
+ * The wrapper rewrites `runId` to THIS executor's parent run: nested
1510
+ * executors emit with their own `parentRunId` (a `*_sub_*` child id),
1511
+ * and each wrapper layer rewrites upward, so by the time an event
1512
+ * reaches the host sink its `runId` is the ROOT run — hosts keying
1513
+ * billing by run id never see intermediate child run ids there
1514
+ * (`subagentRunId` still identifies the emitting child).
1515
+ */
1516
+ subagentUsageSink:
1517
+ hostUsageSink == null
1518
+ ? undefined
1519
+ : /** Returns the host sink's result so async sinks stay awaited
1520
+ * through every wrapper layer. */
1521
+ (event): void | Promise<void> =>
1522
+ hostUsageSink({ ...event, runId: this.parentRunId }),
1523
+ });
471
1524
  let forwarding: ForwarderCallback | undefined;
472
1525
  if (forwardingEnabled) {
473
1526
  forwarding = this.createForwarderCallback({
@@ -479,21 +1532,25 @@ export class SubagentExecutor {
479
1532
  });
480
1533
  }
481
1534
  const forwarder = forwarding?.handler;
1535
+ let childAlreadyStarted = this.startedChildRuns.has(childExecutionKey);
1536
+ let childAlreadyCompleted = this.completedChildRuns.has(childExecutionKey);
482
1537
 
483
- if (forwarder) {
484
- await this.emitSubagentUpdate(parentRegistry!, {
485
- childRunId,
486
- subagentType,
487
- subagentAgentId: childAgentId,
488
- parentToolCallId,
489
- phase: 'start',
490
- label: `Subagent "${subagentType}" started`,
491
- });
492
- }
493
-
494
- let result: { messages: BaseMessage[] };
1538
+ let result: { messages: BaseMessage[] } | undefined;
1539
+ let recoveredComplete = false;
1540
+ let recoveredInProgress = false;
495
1541
  try {
496
- const workflow = childGraph.createWorkflow();
1542
+ const workflow = (cachedChildRun?.workflow ??
1543
+ childGraph.createWorkflow()) as StatefulCompiledWorkflow;
1544
+ const activeChildRun = cachedChildRun ?? {
1545
+ graph: childGraph,
1546
+ workflow,
1547
+ pendingInterrupts: [],
1548
+ childAgentId,
1549
+ childRunId,
1550
+ };
1551
+ if (cachedChildRun == null) {
1552
+ this.activeChildRuns.set(childExecutionKey, activeChildRun);
1553
+ }
497
1554
  /**
498
1555
  * When `parentHandlerRegistry` is provided (forwarding mode), attach a
499
1556
  * lightweight callback that intercepts the child's `on_custom_event`
@@ -537,50 +1594,219 @@ export class SubagentExecutor {
537
1594
  }
538
1595
  const callbacks: Callbacks = callbackHandlers;
539
1596
  /**
540
- * Inherit the parent's host `configurable` host-set fields
541
- * (`requestBody`, `user`, `userMCPAuthMap`, etc.) AND the run-
542
- * identity fields (`run_id`, `parent_run_id`, `thread_id`) all
543
- * propagate. LangGraph's own runtime keys are excluded because the
544
- * child graph creates its own scratchpad/checkpoint/abort plumbing.
545
- *
546
- * Run-identity propagation is intentional and matches the
547
- * convention this executor itself already uses for `SubagentStart`
548
- * / `SubagentStop` hooks (`sessionId: this.parentRunId`): the
549
- * subagent runs under the parent's session scope, not its own.
550
- * Forwarding `run_id` / `parent_run_id` / `thread_id` makes
551
- * `ToolNode`'s hook lookups (`hasHookFor(eventName, runId)`),
552
- * `ToolOutputReferenceRegistry` keying, and trace lineage all
553
- * resolve to the parent's session for tools dispatched from the
554
- * subagent — so `PreToolUse` / `PostToolUse` hooks the host
555
- * registered against the parent's run fire for subagent tool
556
- * calls too. "Same run" matches the user-perceptual mental model.
557
- *
558
- * `thread_id` falls back to `childRunId` only when the parent
559
- * didn't supply one (legacy behavior preserved for hosts that
560
- * never set thread_id).
1597
+ * Inherit the parent's host `configurable` while binding LangGraph's
1598
+ * checkpoint identity to a stable child id derived from the durable
1599
+ * parent thread and checkpoint fork. The parent thread id cannot be
1600
+ * reused here: parent and child share one checkpointer when nested HITL
1601
+ * is enabled, and root checkpoint namespaces are normalized by
1602
+ * LangGraph, so a shared `thread_id` would collide with the parent.
561
1603
  *
562
- * NOTE: a future revision will likely make this configurable per
563
- * spawn type e.g. a background / async subagent that runs after
564
- * the parent's run completes wants isolation, not inheritance.
565
- * For now the inheritance path matches LibreChat's primary use
566
- * case (synchronous subagents within a single user turn).
1604
+ * `run_id` still propagates as the parent run id, which is the key used
1605
+ * for session-scoped hook lookup. Child hook inputs therefore retain
1606
+ * the parent policy scope while their `threadId` truthfully identifies
1607
+ * the independently checkpointed child execution.
567
1608
  */
568
1609
  const inheritedConfigurable: Record<string, unknown> =
569
1610
  sanitizeChildConfigurable(params.parentConfigurable);
570
- result = await workflow.invoke(
571
- { messages: [new HumanMessage(description)] },
572
- {
573
- recursionLimit: maxTurns * RECURSION_MULTIPLIER,
574
- signal: childSignal,
575
- callbacks,
576
- runName: `subagent:${subagentType}`,
577
- configurable: {
578
- thread_id: childRunId,
579
- ...inheritedConfigurable,
580
- },
581
- }
1611
+ const resumeAttemptId = getResumeAttemptId(
1612
+ params.parentConfigurable,
1613
+ this.parentRunId
582
1614
  );
1615
+ const currentHookSessionId =
1616
+ typeof inheritedConfigurable.run_id === 'string' &&
1617
+ inheritedConfigurable.run_id.length > 0
1618
+ ? inheritedConfigurable.run_id
1619
+ : this.parentRunId;
1620
+ if (cachedChildRun == null && resumeExecution != null) {
1621
+ childGraph.restoreSubagentResumeState(
1622
+ resumeExecution.graphState,
1623
+ currentHookSessionId
1624
+ );
1625
+ }
1626
+ const childInvokeConfig = {
1627
+ recursionLimit: maxTurns * RECURSION_MULTIPLIER,
1628
+ signal: childSignal,
1629
+ callbacks,
1630
+ runName: `subagent:${subagentType}`,
1631
+ configurable: {
1632
+ ...inheritedConfigurable,
1633
+ ...(this.humanInTheLoop?.enabled === true
1634
+ ? {
1635
+ [TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY]:
1636
+ approvalExecutionScope,
1637
+ }
1638
+ : {}),
1639
+ ...(resumeExecution?.descendant == null
1640
+ ? {}
1641
+ : {
1642
+ [SUBAGENT_RESUME_MANIFEST_CONFIG_KEY]:
1643
+ resumeExecution.descendant,
1644
+ [SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: resumeAttemptId,
1645
+ }),
1646
+ thread_id:
1647
+ this.humanInTheLoop?.enabled === true
1648
+ ? childThreadId
1649
+ : (inheritedConfigurable.thread_id ?? childRunId),
1650
+ },
1651
+ };
1652
+ activeChildRun.invokeConfig = childInvokeConfig;
1653
+ if (cachedChildRun == null && this.humanInTheLoop?.enabled === true) {
1654
+ /** Rehydrate child-owned interrupt state when a host rebuilds Run
1655
+ * around the same durable checkpointer after a process boundary. */
1656
+ const persistedState = await workflow.getState(childInvokeConfig);
1657
+ const checkpointMessages = getCheckpointMessages(
1658
+ (persistedState.values as { messages?: unknown } | undefined)
1659
+ ?.messages
1660
+ );
1661
+ const persistedHookSessionId =
1662
+ getSubagentHookSessionId(checkpointMessages);
1663
+ if (persistedHookSessionId != null) {
1664
+ this.hookRegistry?.copySession(
1665
+ persistedHookSessionId,
1666
+ currentHookSessionId
1667
+ );
1668
+ }
1669
+ const persistedInterrupts = getPersistedInterrupts(persistedState);
1670
+ if (persistedInterrupts.length > 0) {
1671
+ activeChildRun.pendingInterrupts = persistedInterrupts;
1672
+ this.startedChildRuns.add(childExecutionKey);
1673
+ childAlreadyStarted = true;
1674
+ } else if (persistedState.next.length > 0) {
1675
+ recoveredInProgress = true;
1676
+ childAlreadyStarted = true;
1677
+ this.startedChildRuns.add(childExecutionKey);
1678
+ } else if (persistedState.next.length === 0) {
1679
+ const persistedMessages = getPersistedMessages(persistedState);
1680
+ if (persistedMessages != null) {
1681
+ const marker = getSubagentCheckpointMarker(
1682
+ checkpointMessages,
1683
+ executionSuffix
1684
+ );
1685
+ result = { messages: persistedMessages };
1686
+ recoveredComplete = true;
1687
+ childAlreadyStarted = true;
1688
+ childAlreadyCompleted = marker?.lifecycleComplete === true;
1689
+ this.startedChildRuns.add(childExecutionKey);
1690
+ }
1691
+ }
1692
+ }
1693
+ if (!recoveredComplete) {
1694
+ const childResumeMap = getChildResumeMap(
1695
+ activeChildRun.pendingInterrupts,
1696
+ params.parentConfigurable
1697
+ );
1698
+ if (
1699
+ activeChildRun.pendingInterrupts.length > 0 &&
1700
+ childResumeMap == null
1701
+ ) {
1702
+ throw new GraphInterrupt(activeChildRun.pendingInterrupts);
1703
+ }
1704
+ let childInput: BaseGraphState | Command | null;
1705
+ if (childResumeMap != null) {
1706
+ childInput = new Command({ resume: childResumeMap });
1707
+ } else if (recoveredInProgress) {
1708
+ childInput = null;
1709
+ } else {
1710
+ childInput = {
1711
+ messages: [
1712
+ new HumanMessage({
1713
+ content: description,
1714
+ additional_kwargs: {
1715
+ [SUBAGENT_HOOK_SESSION_KEY]: currentHookSessionId,
1716
+ [SUBAGENT_RUN_ID_KEY]: childRunId,
1717
+ },
1718
+ }),
1719
+ ],
1720
+ };
1721
+ }
1722
+
1723
+ if (
1724
+ !childAlreadyStarted &&
1725
+ this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) ===
1726
+ true
1727
+ ) {
1728
+ const hookResult = await executeHooks({
1729
+ registry: this.hookRegistry,
1730
+ input: {
1731
+ hook_event_name: 'SubagentStart',
1732
+ runId: this.parentRunId,
1733
+ threadId,
1734
+ parentAgentId: this.parentAgentId,
1735
+ agentId: childAgentId,
1736
+ agentType: subagentType,
1737
+ inputs: [new HumanMessage(description)],
1738
+ },
1739
+ sessionId: this.parentRunId,
1740
+ matchQuery: subagentType,
1741
+ }).catch((): AggregatedHookResult => HOOK_FALLBACK);
1742
+
1743
+ if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
1744
+ this.clearChildGraph(childGraph);
1745
+ this.activeChildRuns.delete(childExecutionKey);
1746
+ return {
1747
+ content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
1748
+ messages: [],
1749
+ };
1750
+ }
1751
+ }
1752
+ this.startedChildRuns.add(childExecutionKey);
1753
+
1754
+ if (forwarder && !childAlreadyStarted) {
1755
+ await this.emitSubagentUpdate(parentRegistry!, {
1756
+ childRunId,
1757
+ subagentType,
1758
+ subagentAgentId: childAgentId,
1759
+ parentToolCallId,
1760
+ phase: 'start',
1761
+ label: `Subagent "${subagentType}" started`,
1762
+ });
1763
+ }
1764
+
1765
+ let childResult: BaseGraphState;
1766
+ if (this.humanInTheLoop?.enabled === true) {
1767
+ /** Execute as an independently checkpointed root instead of inheriting
1768
+ * the parent's Pregel namespace. Parent decisions are routed explicitly
1769
+ * by interrupt id, so concurrent children keep isolated resume state. */
1770
+ childResult = await AsyncLocalStorageProviderSingleton.runWithConfig(
1771
+ childInvokeConfig,
1772
+ (): Promise<BaseGraphState> =>
1773
+ workflow.invoke(childInput, childInvokeConfig)
1774
+ );
1775
+ } else {
1776
+ childResult = await workflow.invoke(childInput, childInvokeConfig);
1777
+ }
1778
+ const childInterrupts = isInterrupted(childResult)
1779
+ ? childResult[INTERRUPT]
1780
+ : undefined;
1781
+ if (childInterrupts != null && childInterrupts.length > 0) {
1782
+ throw new GraphInterrupt(childInterrupts);
1783
+ }
1784
+ result = { messages: childResult.messages };
1785
+ }
583
1786
  } catch (error) {
1787
+ if (isGraphInterrupt(error)) {
1788
+ const activeChildRun = this.activeChildRuns.get(childExecutionKey);
1789
+ if (activeChildRun != null) {
1790
+ activeChildRun.pendingInterrupts = error.interrupts;
1791
+ }
1792
+ const resumeManifest =
1793
+ activeChildRun == null || parentToolCallId == null
1794
+ ? undefined
1795
+ : await this.createResumeManifest();
1796
+ await forwarding?.drain();
1797
+ throw new GraphInterrupt(
1798
+ addSubagentScope(
1799
+ error.interrupts,
1800
+ {
1801
+ run_id: childRunId,
1802
+ agent_id: childAgentId,
1803
+ subagent_type: subagentType,
1804
+ parent_tool_call_id: parentToolCallId,
1805
+ },
1806
+ resumeManifest
1807
+ )
1808
+ );
1809
+ }
584
1810
  /** Aborted before any observational work below: parallel siblings — in
585
1811
  * this executor and, via the graph-scoped breaker, under other
586
1812
  * parallel agent nodes — stream on the composed child signal, and
@@ -604,7 +1830,8 @@ export class SubagentExecutor {
604
1830
  data: { message: errorMessage },
605
1831
  });
606
1832
  }
607
- childGraph.clearHeavyState();
1833
+ this.clearChildGraph(childGraph);
1834
+ this.activeChildRuns.delete(childExecutionKey);
608
1835
  /**
609
1836
  * A tripped stream circuit breaker is a safety abort, not a recoverable
610
1837
  * subagent failure: converting it into a tool result would let the
@@ -621,9 +1848,13 @@ export class SubagentExecutor {
621
1848
  };
622
1849
  }
623
1850
 
1851
+ if (result == null) {
1852
+ throw new Error('Subagent completed without producing graph state.');
1853
+ }
624
1854
  const filteredContent = filterSubagentResult(result.messages);
625
1855
 
626
1856
  if (
1857
+ !childAlreadyCompleted &&
627
1858
  this.hookRegistry?.hasHookFor('SubagentStop', this.parentRunId) === true
628
1859
  ) {
629
1860
  /**
@@ -649,7 +1880,7 @@ export class SubagentExecutor {
649
1880
  });
650
1881
  }
651
1882
 
652
- if (forwarding) {
1883
+ if (forwarding && !childAlreadyCompleted) {
653
1884
  await forwarding.drain();
654
1885
  await this.emitSubagentUpdate(parentRegistry!, {
655
1886
  childRunId,
@@ -660,10 +1891,25 @@ export class SubagentExecutor {
660
1891
  label: `Subagent "${subagentType}" finished`,
661
1892
  });
662
1893
  }
1894
+ if (!childAlreadyCompleted) {
1895
+ const activeChildRun = this.activeChildRuns.get(childExecutionKey);
1896
+ if (activeChildRun != null && parentToolCallId != null) {
1897
+ await this.persistChildCheckpointMarker(
1898
+ activeChildRun,
1899
+ parentToolCallId
1900
+ );
1901
+ }
1902
+ }
1903
+ this.completedChildRuns.add(childExecutionKey);
663
1904
 
664
- childGraph.clearHeavyState();
1905
+ this.clearChildGraph(childGraph);
665
1906
 
666
- return { content: filteredContent, messages: result.messages };
1907
+ const completedResult = {
1908
+ content: filteredContent,
1909
+ messages: result.messages,
1910
+ };
1911
+ this.completedChildResults.set(childExecutionKey, completedResult);
1912
+ return completedResult;
667
1913
  }
668
1914
 
669
1915
  /**
@@ -1047,6 +2293,9 @@ function isLangGraphRuntimeConfigKey(key: string): boolean {
1047
2293
  return (
1048
2294
  key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) ||
1049
2295
  LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key) ||
2296
+ key === SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY ||
2297
+ key === SUBAGENT_RESUME_MANIFEST_CONFIG_KEY ||
2298
+ key === SUBAGENT_PARENT_BATCH_CONFIG_KEY ||
1050
2299
  /** The parent batch's breaker scope must not leak into the child
1051
2300
  * workflow's configurable — children own separate controllers. */
1052
2301
  key === RUN_BREAKER_SCOPE_CONFIG_KEY
@@ -1401,6 +2650,9 @@ export function summarizeEvent(eventName: string, data: unknown): string {
1401
2650
  * pure tool_use (e.g. the subagent hit `maxTurns` mid-tool-call), the walk
1402
2651
  * continues to earlier AIMessages so partial progress is salvaged — this
1403
2652
  * matches Claude Code's behavior in `agentToolUtils.finalizeAgentTool`.
2653
+ * Consecutive streamed text-delta blocks with the same provider index are
2654
+ * coalesced without adding whitespace. Annotation-only text blocks are
2655
+ * ignored; complete text blocks and distinct delta indexes remain separated.
1404
2656
  * Returns "Task completed" only when no AIMessage in the history contains
1405
2657
  * any text.
1406
2658
  */
@@ -1422,13 +2674,64 @@ export function filterSubagentResult(messages: BaseMessage[]): string {
1422
2674
  }
1423
2675
 
1424
2676
  const textParts: string[] = [];
2677
+ let textDeltaParts: string[] = [];
2678
+ let textDeltaIndex: string | number | undefined;
2679
+ const flushTextDeltaParts = (): void => {
2680
+ if (textDeltaParts.length === 0) {
2681
+ return;
2682
+ }
2683
+ textParts.push(textDeltaParts.join(''));
2684
+ textDeltaParts = [];
2685
+ textDeltaIndex = undefined;
2686
+ };
1425
2687
  for (const block of content) {
1426
2688
  if (typeof block === 'string') {
1427
- textParts.push(block);
1428
- } else if ('type' in block && block.type === 'text' && 'text' in block) {
1429
- textParts.push(block.text as string);
2689
+ flushTextDeltaParts();
2690
+ if (block !== '') {
2691
+ textParts.push(block);
2692
+ }
2693
+ continue;
2694
+ }
2695
+
2696
+ const type =
2697
+ 'type' in block && typeof block.type === 'string' ? block.type : '';
2698
+ const isTextDelta = type === TEXT_DELTA_CONTENT_TYPE;
2699
+ const isText = type === ContentTypes.TEXT || isTextDelta;
2700
+ const text =
2701
+ isText && 'text' in block && typeof block.text === 'string'
2702
+ ? block.text
2703
+ : '';
2704
+ if (isTextDelta) {
2705
+ if (text === '') {
2706
+ continue;
2707
+ }
2708
+ const index =
2709
+ 'index' in block &&
2710
+ (typeof block.index === 'string' || typeof block.index === 'number')
2711
+ ? block.index
2712
+ : undefined;
2713
+ if (
2714
+ textDeltaIndex != null &&
2715
+ index != null &&
2716
+ index !== textDeltaIndex
2717
+ ) {
2718
+ flushTextDeltaParts();
2719
+ }
2720
+ textDeltaIndex ??= index;
2721
+ textDeltaParts.push(text);
2722
+ continue;
2723
+ }
2724
+
2725
+ if (type === ContentTypes.TEXT && text === '') {
2726
+ continue;
2727
+ }
2728
+
2729
+ flushTextDeltaParts();
2730
+ if (text !== '') {
2731
+ textParts.push(text);
1430
2732
  }
1431
2733
  }
2734
+ flushTextDeltaParts();
1432
2735
 
1433
2736
  if (textParts.length > 0) {
1434
2737
  return textParts.join('\n');
@@ -1523,11 +2826,10 @@ export function buildChildInputs(
1523
2826
  * Host-supplied direct tools are scrubbed from INHERITED configs only.
1524
2827
  * A self-spawn config's `agentInputs` is a shallow spread of the parent's
1525
2828
  * `_sourceInputs`, so without this a parent-scoped graph tool (e.g. an
1526
- * interrupt-raising ask_user_question, which needs the parent's
1527
- * checkpointer child graphs compile without one) would silently leak
1528
- * into the child and deterministically throw `No checkpointer set`. An
1529
- * EXPLICIT child config that lists its own `graphTools` is a deliberate
1530
- * host choice and keeps them (Codex #289 P2).
2829
+ * interrupt-raising ask_user_question) would silently become available to
2830
+ * the child. An EXPLICIT child config that lists its own `graphTools` is a
2831
+ * deliberate host choice and keeps them (Codex #289 P2); with HITL enabled,
2832
+ * those tools use the shared checkpointer and can pause and resume safely.
1531
2833
  */
1532
2834
  graphTools: config.self === true ? undefined : agentInputs.graphTools,
1533
2835
  };