@librechat/agents 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +165 -57
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  6. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
  7. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
  8. package/dist/cjs/graphs/createGraph.cjs +15 -0
  9. package/dist/cjs/graphs/createGraph.cjs.map +1 -0
  10. package/dist/cjs/graphs/index.cjs +1 -0
  11. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  14. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  15. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  16. package/dist/cjs/hooks/index.cjs.map +1 -1
  17. package/dist/cjs/hooks/types.cjs +2 -0
  18. package/dist/cjs/hooks/types.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +3 -3
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/main.cjs +34 -24
  22. package/dist/cjs/messages/format.cjs +40 -23
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +130 -53
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/session/AgentSession.cjs +37 -2
  27. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  29. package/dist/cjs/stream.cjs +1 -1
  30. package/dist/cjs/summarization/node.cjs +6 -3
  31. package/dist/cjs/summarization/node.cjs.map +1 -1
  32. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  35. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +321 -95
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  40. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  41. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  42. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  43. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  44. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  45. package/dist/cjs/tools/search/tool.cjs +4 -1
  46. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
  50. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  51. package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
  52. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  53. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  54. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  55. package/dist/cjs/tools/subagent/index.cjs +1 -0
  56. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  57. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  58. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  59. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  60. package/dist/cjs/utils/index.cjs +2 -2
  61. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  62. package/dist/esm/graphs/Graph.mjs +165 -57
  63. package/dist/esm/graphs/Graph.mjs.map +1 -1
  64. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  65. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  66. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  67. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  68. package/dist/esm/graphs/createGraph.mjs +15 -0
  69. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  70. package/dist/esm/graphs/index.mjs +1 -0
  71. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  72. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  73. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  74. package/dist/esm/hooks/executeHooks.mjs +60 -14
  75. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  76. package/dist/esm/hooks/index.mjs.map +1 -1
  77. package/dist/esm/hooks/types.mjs +2 -1
  78. package/dist/esm/hooks/types.mjs.map +1 -1
  79. package/dist/esm/llm/init.mjs +1 -1
  80. package/dist/esm/llm/invoke.mjs +2 -2
  81. package/dist/esm/main.mjs +21 -19
  82. package/dist/esm/messages/format.mjs +40 -23
  83. package/dist/esm/messages/format.mjs.map +1 -1
  84. package/dist/esm/run.mjs +131 -54
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/session/AgentSession.mjs +37 -2
  87. package/dist/esm/session/AgentSession.mjs.map +1 -1
  88. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  89. package/dist/esm/stream.mjs +1 -1
  90. package/dist/esm/summarization/node.mjs +6 -3
  91. package/dist/esm/summarization/node.mjs.map +1 -1
  92. package/dist/esm/tools/BashExecutor.mjs +1 -1
  93. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  94. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  95. package/dist/esm/tools/SubagentTool.mjs +4 -4
  96. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  97. package/dist/esm/tools/ToolNode.mjs +322 -96
  98. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  99. package/dist/esm/tools/ToolSearch.mjs +1 -1
  100. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  101. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  102. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  103. package/dist/esm/tools/search/rerankers.mjs +154 -1
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +4 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  108. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  109. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
  110. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  111. package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
  112. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  113. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  114. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  115. package/dist/esm/tools/subagent/index.mjs +1 -0
  116. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  117. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  118. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  119. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  120. package/dist/esm/utils/index.mjs +2 -2
  121. package/dist/types/agents/AgentContext.d.ts +1 -1
  122. package/dist/types/graphs/Graph.d.ts +22 -2
  123. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  124. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  125. package/dist/types/graphs/createGraph.d.ts +13 -0
  126. package/dist/types/graphs/graphFactory.d.ts +13 -0
  127. package/dist/types/graphs/index.d.ts +2 -0
  128. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  129. package/dist/types/hooks/executeHooks.d.ts +5 -1
  130. package/dist/types/hooks/index.d.ts +2 -2
  131. package/dist/types/hooks/types.d.ts +12 -0
  132. package/dist/types/run.d.ts +6 -3
  133. package/dist/types/session/AgentSession.d.ts +1 -0
  134. package/dist/types/tools/SubagentTool.d.ts +1 -1
  135. package/dist/types/tools/ToolNode.d.ts +23 -15
  136. package/dist/types/tools/search/rerankers.d.ts +28 -0
  137. package/dist/types/tools/search/types.d.ts +38 -1
  138. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  139. package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
  140. package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
  141. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  142. package/dist/types/tools/subagent/index.d.ts +1 -1
  143. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  144. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  145. package/dist/types/types/graph.d.ts +189 -24
  146. package/dist/types/types/hitl.d.ts +15 -0
  147. package/package.json +8 -2
  148. package/src/agents/AgentContext.ts +1 -1
  149. package/src/graphs/Graph.ts +349 -104
  150. package/src/graphs/MultiAgentGraph.ts +149 -47
  151. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  152. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  153. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  154. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  155. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  156. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  157. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  158. package/src/graphs/createGraph.ts +32 -0
  159. package/src/graphs/graphFactory.ts +12 -0
  160. package/src/graphs/index.ts +2 -0
  161. package/src/hooks/HookRegistry.ts +151 -1
  162. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  163. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  164. package/src/hooks/executeHooks.ts +133 -15
  165. package/src/hooks/index.ts +3 -1
  166. package/src/hooks/types.ts +16 -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 +214 -56
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/graph-subagent.live.test.ts +242 -0
  175. package/src/specs/graph-subagent.test.ts +322 -0
  176. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  177. package/src/specs/subagent.test.ts +130 -4
  178. package/src/summarization/__tests__/node.test.ts +32 -14
  179. package/src/summarization/node.ts +15 -11
  180. package/src/tools/SubagentTool.ts +4 -4
  181. package/src/tools/ToolNode.ts +643 -157
  182. package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
  183. package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
  184. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  185. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
  186. package/src/tools/__tests__/hitl.test.ts +234 -5
  187. package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
  188. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  189. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  190. package/src/tools/search/rerankers.ts +260 -0
  191. package/src/tools/search/tool.ts +6 -0
  192. package/src/tools/search/types.ts +45 -1
  193. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  194. package/src/tools/subagent/SubagentExecutor.ts +2232 -312
  195. package/src/tools/subagent/SubagentReplay.ts +584 -0
  196. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  197. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  198. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  199. package/src/tools/subagent/childGraphConfig.ts +694 -0
  200. package/src/tools/subagent/index.ts +6 -0
  201. package/src/tools/subagent/runtimeLimits.ts +7 -0
  202. package/src/tools/toolOutputReferences.ts +40 -1
  203. package/src/types/graph.ts +222 -24
  204. package/src/types/hitl.ts +16 -0
@@ -1,11 +1,43 @@
1
1
  import { nanoid } from 'nanoid';
2
- import { HumanMessage } from '@langchain/core/messages';
2
+ import { createHash } from 'crypto';
3
3
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
4
- import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
4
+ import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
5
+ import {
6
+ AIMessage,
7
+ BaseMessage,
8
+ HumanMessage,
9
+ ToolMessage,
10
+ } from '@langchain/core/messages';
11
+ import {
12
+ Command,
13
+ END,
14
+ GraphInterrupt,
15
+ INTERRUPT,
16
+ MessagesAnnotation,
17
+ START,
18
+ StateGraph,
19
+ copyCheckpoint,
20
+ isGraphInterrupt,
21
+ isInterrupted,
22
+ } from '@langchain/langgraph';
23
+ import type {
24
+ Interrupt,
25
+ StateSnapshot,
26
+ CheckpointTuple,
27
+ BaseCheckpointSaver,
28
+ } from '@langchain/langgraph';
5
29
  import type { ChatGeneration, LLMResult } from '@langchain/core/outputs';
6
30
  import type { Callbacks } from '@langchain/core/callbacks/manager';
31
+ import type { RunnableConfig } from '@langchain/core/runnables';
32
+ import type { UsageMetadata } from '@langchain/core/messages';
33
+ import type { ToolCall } from '@langchain/core/messages/tool';
7
34
  import type {
8
35
  AgentInputs,
36
+ BaseGraphState,
37
+ CompiledStateWorkflow,
38
+ EventHandler,
39
+ MultiAgentGraphState,
40
+ HumanInTheLoopConfig,
9
41
  InjectedMessage,
10
42
  MessageDeltaEvent,
11
43
  ProcessedToolCall,
@@ -13,24 +45,67 @@ import type {
13
45
  RunStep,
14
46
  RunStepDeltaEvent,
15
47
  StandardGraphInput,
48
+ ExecutableSubagentConfigEntry,
16
49
  ResolvedSubagentConfig,
50
+ ResolvedSubagentConfigEntry,
51
+ SubagentExecutionContext,
52
+ SubagentResolveConfigurable,
53
+ SubagentResolveRequestContext,
54
+ SubagentResolveUserContext,
17
55
  StepCompleted,
18
- SubagentConfig,
19
56
  SubagentUpdateEvent,
20
57
  SubagentUpdatePhase,
21
58
  SubagentUsageSink,
59
+ ToolApprovalInterruptPayload,
22
60
  ToolExecuteBatchRequest,
23
61
  ToolCallDelta,
24
62
  TokenCounter,
63
+ ToolApprovalDecision,
64
+ ToolApprovalDecisionMap,
25
65
  } from '@/types';
26
- import type { AggregatedHookResult, HookRegistry } from '@/hooks';
27
- import type { AgentContext } from '@/agents/AgentContext';
66
+ import type {
67
+ PreparedSubagentExecutionIdentity,
68
+ SubagentDefinitionBinding,
69
+ SubagentExecutionIdentity,
70
+ SubagentExecutionPreparationLease,
71
+ SubagentExecutionRecord,
72
+ } from './SubagentExecutionRegistry';
73
+ import type {
74
+ SubagentResumeExecution,
75
+ SubagentResumeManifest,
76
+ SubagentCheckpointReference,
77
+ SettledSubagentToolOutput,
78
+ } from './SubagentReplay';
79
+ import type {
80
+ AggregatedHookResult,
81
+ HookRegistry,
82
+ ToolApprovalReplaySnapshot,
83
+ } from '@/hooks';
84
+ import type { GraphFactory } from '@/graphs/graphFactory';
28
85
  import type { StandardGraph } from '@/graphs/Graph';
29
86
  import type { HandlerRegistry } from '@/events';
87
+ import {
88
+ getSubagentApprovalExecutionScope,
89
+ SubagentDefinitionBindingError,
90
+ SubagentExecutionRegistry,
91
+ SubagentInvocationBindingError,
92
+ SubagentSettlementBindingError,
93
+ } from './SubagentExecutionRegistry';
94
+ import {
95
+ getSubagentResumeManifest,
96
+ attachSubagentResumeManifest,
97
+ SUBAGENT_PARENT_BATCH_CONFIG_KEY,
98
+ SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
99
+ SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
100
+ } from './SubagentReplay';
30
101
  import {
31
102
  StreamLimitExceededError,
32
103
  RUN_BREAKER_SCOPE_CONFIG_KEY,
33
104
  } from '@/llm/streamLimits';
105
+ import {
106
+ DEFAULT_SUBAGENT_MAX_TURNS,
107
+ SUBAGENT_RECURSION_MULTIPLIER,
108
+ } from './runtimeLimits';
34
109
  import {
35
110
  ContentTypes,
36
111
  Constants,
@@ -38,13 +113,63 @@ import {
38
113
  Callback,
39
114
  StepTypes,
40
115
  } from '@/common';
41
- import { executeHooks } from '@/hooks';
116
+ import {
117
+ executeHooks,
118
+ TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
119
+ } from '@/hooks';
120
+ import {
121
+ createChildGraphPlan,
122
+ isGraphSubagentConfig,
123
+ } from './childGraphConfig';
124
+ import { stableStringify } from '@/tools/eagerEventExecution';
125
+ import { composeAbortSignals } from '@/utils/misc';
126
+
127
+ export {
128
+ buildChildInputs,
129
+ isGraphSubagentConfig,
130
+ normalizeSubagentConfigs,
131
+ normalizeSubagentConfigEntries,
132
+ resolveSubagentConfigs,
133
+ resolveSubagentConfigEntries,
134
+ } from './childGraphConfig';
42
135
 
43
- const DEFAULT_MAX_TURNS = 25;
44
- const RECURSION_MULTIPLIER = 3;
45
136
  const ERROR_MESSAGE_MAX_CHARS = 200;
46
- const MAX_PENDING_SUBAGENT_UPDATES = 64;
137
+ const MAX_QUEUED_SUBAGENT_UPDATES = 64;
138
+ const SUBAGENT_UPDATE_HANDLER_TIMEOUT_MS = 5_000;
47
139
  const TEXT_DELTA_CONTENT_TYPE = `${ContentTypes.TEXT}_delta`;
140
+ const SUBAGENT_RESOLUTION_ERROR_MESSAGE =
141
+ 'Subagent error: Unable to initialize the selected subagent.';
142
+ const SUBAGENT_CONFIG_CHANGED_MESSAGE =
143
+ 'Subagent error: Subagent configuration changed since this execution was paused.';
144
+ const SUBAGENT_INVOCATION_CHANGED_MESSAGE =
145
+ 'Subagent error: Subagent invocation changed for this execution.';
146
+
147
+ async function dispatchObservationalSubagentUpdate(
148
+ handler: EventHandler,
149
+ event: SubagentUpdateEvent
150
+ ): Promise<boolean> {
151
+ let timeout: ReturnType<typeof setTimeout> | undefined;
152
+ try {
153
+ return await Promise.race([
154
+ Promise.resolve()
155
+ .then(() => handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event))
156
+ .then(
157
+ () => true,
158
+ () => true
159
+ ),
160
+ new Promise<boolean>((resolve) => {
161
+ timeout = setTimeout(
162
+ () => resolve(false),
163
+ SUBAGENT_UPDATE_HANDLER_TIMEOUT_MS
164
+ );
165
+ }),
166
+ ]);
167
+ } finally {
168
+ if (timeout != null) {
169
+ clearTimeout(timeout);
170
+ }
171
+ }
172
+ }
48
173
 
49
174
  const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
50
175
  additionalContexts: [] as string[],
@@ -153,6 +278,7 @@ type QueuedSubagentUpdate = {
153
278
  eventName: string;
154
279
  phase: SubagentUpdatePhase;
155
280
  data: unknown;
281
+ memberAgentId?: string;
156
282
  };
157
283
 
158
284
  type ForwarderCallback = {
@@ -160,17 +286,444 @@ type ForwarderCallback = {
160
286
  drain: () => Promise<void>;
161
287
  };
162
288
 
289
+ type StatefulCompiledWorkflow = Omit<CompiledStateWorkflow, 'invoke'> & {
290
+ invoke(
291
+ input: BaseGraphState | Command | null,
292
+ config?: RunnableConfig
293
+ ): Promise<MultiAgentGraphState>;
294
+ getState(config: RunnableConfig): Promise<StateSnapshot>;
295
+ updateState?(
296
+ config: RunnableConfig,
297
+ values: Record<string, unknown>,
298
+ asNode?: string
299
+ ): Promise<RunnableConfig>;
300
+ };
301
+
302
+ type ReplayCheckpointWorkflow = {
303
+ updateState(
304
+ config: RunnableConfig,
305
+ values: { messages: BaseMessage[] },
306
+ asNode: string
307
+ ): Promise<RunnableConfig>;
308
+ };
309
+
310
+ type ActiveChildRun = {
311
+ graph: StandardGraph;
312
+ workflow: StatefulCompiledWorkflow;
313
+ pendingInterrupts: Interrupt[];
314
+ invokeConfig?: RunnableConfig;
315
+ childAgentId: string;
316
+ checkpointNodeId: string;
317
+ childRunId: string;
318
+ };
319
+
320
+ type PersistedToolOutput = {
321
+ content: ToolMessage['content'];
322
+ toolCallId: string;
323
+ id?: string;
324
+ name?: string;
325
+ status?: 'success' | 'error';
326
+ additionalKwargs: ToolMessage['additional_kwargs'];
327
+ responseMetadata: ToolMessage['response_metadata'];
328
+ metadata?: Record<string, unknown>;
329
+ additionalContexts: string[];
330
+ resolvedArgs?: Record<string, unknown>;
331
+ referenceContent?: string;
332
+ };
333
+
334
+ type SubagentCheckpointMarker = {
335
+ version: 1;
336
+ parentToolCallId: string;
337
+ lifecycleComplete: true;
338
+ hookSessionId?: string;
339
+ childRunId?: string;
340
+ settledOutput?: PersistedToolOutput;
341
+ };
342
+
343
+ function isResumeExecutionCompatible(
344
+ resumeExecution: SubagentResumeExecution | undefined,
345
+ subagentType: string | undefined,
346
+ executableConfig: ExecutableSubagentConfigEntry | undefined,
347
+ hasPersistedTypeEvidence = false
348
+ ): boolean {
349
+ if (resumeExecution == null) {
350
+ return true;
351
+ }
352
+ if (
353
+ subagentType == null ||
354
+ executableConfig == null ||
355
+ resumeExecution.configId !== executableConfig.configId
356
+ ) {
357
+ return false;
358
+ }
359
+ if (resumeExecution.subagentType != null) {
360
+ return resumeExecution.subagentType === subagentType;
361
+ }
362
+ if (hasPersistedTypeEvidence) {
363
+ return true;
364
+ }
365
+ return (
366
+ isGraphSubagentConfig(executableConfig) ||
367
+ executableConfig.agentInputs != null
368
+ );
369
+ }
370
+
371
+ function getSubagentTypeFromArgs(args: unknown): string | undefined {
372
+ if (!isObjectLike(args)) {
373
+ return undefined;
374
+ }
375
+ return asNonEmptyString((args as { subagent_type?: unknown }).subagent_type);
376
+ }
377
+
378
+ function getSubagentDescriptionFromArgs(args: unknown): string | undefined {
379
+ if (!isObjectLike(args)) {
380
+ return undefined;
381
+ }
382
+ const description = (args as { description?: unknown }).description;
383
+ return typeof description === 'string' && description.trim().length > 0
384
+ ? description
385
+ : undefined;
386
+ }
387
+
388
+ function getSubagentType(call: ToolCall): string | undefined {
389
+ return getSubagentTypeFromArgs(call.args);
390
+ }
391
+
163
392
  const LANGGRAPH_RUNTIME_CONFIG_PREFIX = '__pregel_';
393
+ const LANGGRAPH_RESUME_MAP_CONFIG_KEY = '__pregel_resume_map';
164
394
  const LANGGRAPH_CHECKPOINT_CONFIG_KEYS = new Set([
165
395
  'checkpoint_id',
166
396
  'checkpoint_map',
167
397
  'checkpoint_ns',
168
398
  ]);
399
+ const SUBAGENT_CHECKPOINT_MARKER_KEY = '__librechat_subagent_checkpoint';
400
+ const SUBAGENT_HOOK_SESSION_KEY = '__librechat_subagent_hook_session';
401
+ const SUBAGENT_RUN_ID_KEY = '__librechat_subagent_run_id';
402
+ const SUBAGENT_REPLAY_NODE = 'subagent-replay';
403
+ export const DEFAULT_SUBAGENT_DESCRIPTION = 'No task description provided';
404
+
405
+ function isCheckpointSaver(value: unknown): value is BaseCheckpointSaver {
406
+ if (value == null || typeof value !== 'object') {
407
+ return false;
408
+ }
409
+ const candidate = value as Partial<BaseCheckpointSaver>;
410
+ return (
411
+ typeof candidate.getTuple === 'function' &&
412
+ typeof candidate.list === 'function' &&
413
+ typeof candidate.put === 'function' &&
414
+ typeof candidate.putWrites === 'function' &&
415
+ typeof candidate.deleteThread === 'function'
416
+ );
417
+ }
418
+
419
+ function isSubagentCheckpointMarker(
420
+ value: unknown
421
+ ): value is SubagentCheckpointMarker {
422
+ if (value == null || typeof value !== 'object') {
423
+ return false;
424
+ }
425
+ const marker = value as Partial<SubagentCheckpointMarker>;
426
+ return (
427
+ marker.version === 1 &&
428
+ marker.lifecycleComplete === true &&
429
+ typeof marker.parentToolCallId === 'string' &&
430
+ (marker.hookSessionId == null ||
431
+ typeof marker.hookSessionId === 'string') &&
432
+ (marker.childRunId == null || typeof marker.childRunId === 'string') &&
433
+ (marker.settledOutput == null ||
434
+ isPersistedToolOutput(marker.settledOutput))
435
+ );
436
+ }
437
+
438
+ function isPersistedToolOutput(value: unknown): value is PersistedToolOutput {
439
+ if (value == null || typeof value !== 'object') {
440
+ return false;
441
+ }
442
+ const output = value as {
443
+ content?: unknown;
444
+ toolCallId?: unknown;
445
+ status?: unknown;
446
+ additionalKwargs?: unknown;
447
+ responseMetadata?: unknown;
448
+ additionalContexts?: unknown;
449
+ resolvedArgs?: unknown;
450
+ referenceContent?: unknown;
451
+ };
452
+ const contentIsValid =
453
+ typeof output.content === 'string' || Array.isArray(output.content);
454
+ const statusIsValid =
455
+ output.status == null ||
456
+ output.status === 'success' ||
457
+ output.status === 'error';
458
+ const contextsAreValid =
459
+ Array.isArray(output.additionalContexts) &&
460
+ output.additionalContexts.every((context) => typeof context === 'string');
461
+ const resolvedArgsAreValid =
462
+ output.resolvedArgs == null ||
463
+ (typeof output.resolvedArgs === 'object' &&
464
+ !Array.isArray(output.resolvedArgs));
465
+ return (
466
+ contentIsValid &&
467
+ typeof output.toolCallId === 'string' &&
468
+ output.additionalKwargs != null &&
469
+ typeof output.additionalKwargs === 'object' &&
470
+ output.responseMetadata != null &&
471
+ typeof output.responseMetadata === 'object' &&
472
+ statusIsValid &&
473
+ contextsAreValid &&
474
+ resolvedArgsAreValid &&
475
+ (output.referenceContent == null ||
476
+ typeof output.referenceContent === 'string')
477
+ );
478
+ }
479
+
480
+ function getSubagentCheckpointMarker(
481
+ messages: BaseMessage[],
482
+ parentToolCallId: string
483
+ ): SubagentCheckpointMarker | undefined {
484
+ for (let i = messages.length - 1; i >= 0; i--) {
485
+ const marker =
486
+ messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
487
+ if (
488
+ isSubagentCheckpointMarker(marker) &&
489
+ marker.parentToolCallId === parentToolCallId
490
+ ) {
491
+ return marker;
492
+ }
493
+ }
494
+ return undefined;
495
+ }
496
+
497
+ function getSubagentHookSessionId(messages: BaseMessage[]): string | undefined {
498
+ for (let i = messages.length - 1; i >= 0; i--) {
499
+ const sessionId = messages[i].additional_kwargs[SUBAGENT_HOOK_SESSION_KEY];
500
+ if (typeof sessionId === 'string' && sessionId.length > 0) {
501
+ return sessionId;
502
+ }
503
+ }
504
+ return undefined;
505
+ }
506
+
507
+ function getSubagentRunId(messages: BaseMessage[]): string | undefined {
508
+ for (let i = messages.length - 1; i >= 0; i--) {
509
+ const runId = messages[i].additional_kwargs[SUBAGENT_RUN_ID_KEY];
510
+ if (typeof runId === 'string' && runId.length > 0) {
511
+ return runId;
512
+ }
513
+ const marker =
514
+ messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
515
+ if (
516
+ isSubagentCheckpointMarker(marker) &&
517
+ marker.childRunId != null &&
518
+ marker.childRunId.length > 0
519
+ ) {
520
+ return marker.childRunId;
521
+ }
522
+ }
523
+ return undefined;
524
+ }
525
+
526
+ function isSubagentCheckpointMarkerMessage(message: BaseMessage): boolean {
527
+ return isSubagentCheckpointMarker(
528
+ message.additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY]
529
+ );
530
+ }
531
+
532
+ function createSubagentCheckpointMarkerMessage(
533
+ marker: SubagentCheckpointMarker
534
+ ): AIMessage {
535
+ return new AIMessage({
536
+ content: '',
537
+ additional_kwargs: { [SUBAGENT_CHECKPOINT_MARKER_KEY]: marker },
538
+ });
539
+ }
540
+
541
+ function getCheckpointMessages(value: unknown): BaseMessage[] {
542
+ return Array.isArray(value) ? value.filter(BaseMessage.isInstance) : [];
543
+ }
544
+
545
+ function getConfigurableString(
546
+ config: RunnableConfig | undefined,
547
+ key: string
548
+ ): string | undefined {
549
+ const value = config?.configurable?.[key];
550
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
551
+ }
552
+
553
+ function getTupleMessages(tuple: CheckpointTuple | undefined): BaseMessage[] {
554
+ return getCheckpointMessages(tuple?.checkpoint.channel_values.messages);
555
+ }
556
+
557
+ function getCheckpointReference(
558
+ tuple: CheckpointTuple | undefined
559
+ ): SubagentCheckpointReference | undefined {
560
+ const configurable = tuple?.config.configurable;
561
+ const threadId = configurable?.thread_id;
562
+ const checkpointId = configurable?.checkpoint_id;
563
+ const checkpointNs = configurable?.checkpoint_ns ?? '';
564
+ if (
565
+ typeof threadId !== 'string' ||
566
+ threadId.length === 0 ||
567
+ typeof checkpointId !== 'string' ||
568
+ checkpointId.length === 0 ||
569
+ typeof checkpointNs !== 'string'
570
+ ) {
571
+ return undefined;
572
+ }
573
+ return { threadId, checkpointId, checkpointNs };
574
+ }
575
+
576
+ function serializeToolOutput(
577
+ settled: SettledSubagentToolOutput
578
+ ): PersistedToolOutput {
579
+ const { output } = settled;
580
+ return {
581
+ content: output.content,
582
+ toolCallId: output.tool_call_id,
583
+ ...(output.id == null ? {} : { id: output.id }),
584
+ ...(output.name == null ? {} : { name: output.name }),
585
+ ...(output.status == null ? {} : { status: output.status }),
586
+ additionalKwargs: output.additional_kwargs,
587
+ responseMetadata: output.response_metadata,
588
+ ...(output.metadata == null ? {} : { metadata: output.metadata }),
589
+ additionalContexts: settled.additionalContexts,
590
+ ...(settled.resolvedArgs == null
591
+ ? {}
592
+ : { resolvedArgs: settled.resolvedArgs }),
593
+ ...(settled.referenceContent == null
594
+ ? {}
595
+ : { referenceContent: settled.referenceContent }),
596
+ };
597
+ }
598
+
599
+ function getSettlementFingerprint(output: PersistedToolOutput): string {
600
+ return createHash('sha256').update(stableStringify(output)).digest('hex');
601
+ }
602
+
603
+ function deserializeToolOutput(
604
+ output: PersistedToolOutput
605
+ ): SettledSubagentToolOutput {
606
+ return {
607
+ output: new ToolMessage({
608
+ content: output.content,
609
+ tool_call_id: output.toolCallId,
610
+ ...(output.id == null ? {} : { id: output.id }),
611
+ ...(output.name == null ? {} : { name: output.name }),
612
+ ...(output.status == null ? {} : { status: output.status }),
613
+ additional_kwargs: output.additionalKwargs,
614
+ response_metadata: output.responseMetadata,
615
+ ...(output.metadata == null ? {} : { metadata: output.metadata }),
616
+ }),
617
+ additionalContexts: output.additionalContexts,
618
+ ...(output.resolvedArgs == null
619
+ ? {}
620
+ : { resolvedArgs: output.resolvedArgs }),
621
+ ...(output.referenceContent == null
622
+ ? {}
623
+ : { referenceContent: output.referenceContent }),
624
+ };
625
+ }
626
+
627
+ function isToolApprovalPayload(
628
+ value: unknown
629
+ ): value is ToolApprovalInterruptPayload {
630
+ return (
631
+ value != null &&
632
+ typeof value === 'object' &&
633
+ (value as { type?: unknown }).type === 'tool_approval'
634
+ );
635
+ }
636
+
637
+ function addSubagentScope(
638
+ interrupts: Interrupt[],
639
+ scope: NonNullable<ToolApprovalInterruptPayload['subagent']>,
640
+ resumeManifest?: SubagentResumeManifest
641
+ ): Interrupt[] {
642
+ return interrupts.map((childInterrupt) => {
643
+ let payload = childInterrupt.value;
644
+ if (isToolApprovalPayload(payload)) {
645
+ payload = {
646
+ ...childInterrupt.value,
647
+ subagent: childInterrupt.value.subagent ?? scope,
648
+ };
649
+ }
650
+ return {
651
+ ...childInterrupt,
652
+ value:
653
+ resumeManifest == null
654
+ ? payload
655
+ : attachSubagentResumeManifest(payload, resumeManifest),
656
+ };
657
+ });
658
+ }
659
+
660
+ type ToolApprovalResumeValue = ToolApprovalDecision[] | ToolApprovalDecisionMap;
661
+
662
+ function getChildResumeMap(
663
+ pendingInterrupts: Interrupt[],
664
+ parentConfigurable: Record<string, unknown> | undefined
665
+ ): Record<string, ToolApprovalResumeValue> | undefined {
666
+ const resumeMap = parentConfigurable?.[LANGGRAPH_RESUME_MAP_CONFIG_KEY];
667
+ if (resumeMap == null || typeof resumeMap !== 'object') {
668
+ return undefined;
669
+ }
670
+
671
+ const parentResumeMap = resumeMap as Record<string, ToolApprovalResumeValue>;
672
+ const childResumeMap: Record<string, ToolApprovalResumeValue> = {};
673
+ for (const childInterrupt of pendingInterrupts) {
674
+ const interruptId = childInterrupt.id;
675
+ if (
676
+ typeof interruptId === 'string' &&
677
+ Object.prototype.hasOwnProperty.call(parentResumeMap, interruptId)
678
+ ) {
679
+ childResumeMap[interruptId] = parentResumeMap[interruptId];
680
+ }
681
+ }
682
+ return Object.keys(childResumeMap).length > 0 ? childResumeMap : undefined;
683
+ }
684
+
685
+ function getPersistedInterrupts(snapshot: StateSnapshot): Interrupt[] {
686
+ const interrupts: Interrupt[] = [];
687
+ for (const task of snapshot.tasks) {
688
+ for (const pendingInterrupt of task.interrupts) {
689
+ interrupts.push(pendingInterrupt);
690
+ }
691
+ }
692
+ return interrupts;
693
+ }
694
+
695
+ function getPersistedMessages(
696
+ snapshot: StateSnapshot
697
+ ): BaseMessage[] | undefined {
698
+ if (snapshot.values == null || typeof snapshot.values !== 'object') {
699
+ return undefined;
700
+ }
701
+ const values = snapshot.values as { messages?: BaseMessage[] };
702
+ if (!Array.isArray(values.messages) || values.messages.length === 0) {
703
+ return undefined;
704
+ }
705
+ const messages = values.messages.filter(
706
+ (message) => !isSubagentCheckpointMarkerMessage(message)
707
+ );
708
+ return messages.length > 0 ? messages : undefined;
709
+ }
710
+
711
+ function createReplayCheckpointWorkflow(
712
+ checkpointer: BaseCheckpointSaver
713
+ ): ReplayCheckpointWorkflow {
714
+ return new StateGraph(MessagesAnnotation)
715
+ .addNode(SUBAGENT_REPLAY_NODE, (state) => state)
716
+ .addEdge(START, SUBAGENT_REPLAY_NODE)
717
+ .addEdge(SUBAGENT_REPLAY_NODE, END)
718
+ .compile({ checkpointer }) as ReplayCheckpointWorkflow;
719
+ }
169
720
 
170
721
  export type SubagentExecuteParams = {
171
722
  description: string;
172
723
  subagentType: string;
173
724
  threadId?: string;
725
+ /** Signal attached to this specific parent tool invocation. */
726
+ signal?: AbortSignal;
174
727
  /**
175
728
  * Breaker controller captured at the parent TOOL BATCH's entry, before
176
729
  * PreToolUse hooks. Preferred over the live scope accessor: a graph reset
@@ -197,11 +750,12 @@ export type SubagentExecuteParams = {
197
750
  *
198
751
  * Inheritance details (verified empirically against LangGraph):
199
752
  * - host-set keys propagate as-is into the child's tool dispatches;
200
- * - `thread_id` propagates (with `childRunId` as a fallback when
201
- * parent did not supply one) — matches the "subagent is part of
202
- * the same conversation" mental model and aligns with the
203
- * `sessionId: this.parentRunId` convention this executor already
204
- * uses for `SubagentStart` / `SubagentStop` hooks;
753
+ * - with nested HITL enabled, `thread_id` is replaced with a stable
754
+ * child checkpoint id derived from the parent's durable thread id,
755
+ * checkpoint fork, parent agent id, and spawning tool call id so parent
756
+ * and child checkpoints cannot collide, sibling parent forks stay
757
+ * isolated, and reconstruction returns to the same child checkpoint;
758
+ * parent-scoped hook lookup remains keyed by the inherited `run_id`;
205
759
  * - `parent_run_id` propagates when the host put it on parent's
206
760
  * configurable;
207
761
  * - `run_id` is *overwritten by the LangGraph runtime* at child
@@ -234,7 +788,7 @@ export type SubagentExecuteResult = {
234
788
  export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
235
789
 
236
790
  export type SubagentExecutorOptions = {
237
- configs: Map<string, ResolvedSubagentConfig>;
791
+ configs: ReadonlyMap<string, ExecutableSubagentConfigEntry>;
238
792
  parentSignal?: AbortSignal;
239
793
  /** Run-scoped breaker abort shared by every executor of one graph, so a
240
794
  * child tripping a stream limit stops subagents running under OTHER
@@ -250,6 +804,8 @@ export type SubagentExecutorOptions = {
250
804
  hookRegistry?: HookRegistry;
251
805
  parentRunId: string;
252
806
  parentAgentId?: string;
807
+ /** Root identity, policy session, and lineage inherited by a child graph. */
808
+ executionContext?: SubagentExecutionContext;
253
809
  langfuse?: StandardGraphInput['langfuse'];
254
810
  tokenCounter?: TokenCounter;
255
811
  /**
@@ -261,6 +817,11 @@ export type SubagentExecutorOptions = {
261
817
  * revert to the defaults.
262
818
  */
263
819
  streamLimits?: StandardGraphInput['streamLimits'];
820
+ humanInTheLoop?: HumanInTheLoopConfig;
821
+ /** Shared durable saver used to recover outer tool lifecycle results before
822
+ * parent hooks re-enter after a process rebuild. Narrowed structurally at
823
+ * construction because graph compile options also permit framework flags. */
824
+ checkpointer?: unknown;
264
825
  /** Remaining nesting budget. 0 or negative blocks execution. */
265
826
  maxDepth?: number;
266
827
  /**
@@ -269,6 +830,9 @@ export type SubagentExecutorOptions = {
269
830
  * module dependency.
270
831
  */
271
832
  createChildGraph: ChildGraphFactory;
833
+ /** Preferred polymorphic child constructor. The legacy standard-only
834
+ * factory remains required for source compatibility. */
835
+ createChildGraphByKind?: GraphFactory;
272
836
  /**
273
837
  * Parent's event handler registry. When provided, child-graph events are
274
838
  * forwarded through this registry so hosts can:
@@ -293,8 +857,15 @@ export type SubagentExecutorOptions = {
293
857
  usageSink?: SubagentUsageSink;
294
858
  };
295
859
 
860
+ type DurableExecutionRecord = SubagentExecutionRecord<
861
+ SubagentExecuteResult,
862
+ ResolvedSubagentConfig,
863
+ ActiveChildRun,
864
+ PersistedToolOutput
865
+ >;
866
+
296
867
  export class SubagentExecutor {
297
- private readonly configs: Map<string, ResolvedSubagentConfig>;
868
+ private readonly configs: ReadonlyMap<string, ExecutableSubagentConfigEntry>;
298
869
  private readonly parentSignal?: AbortSignal;
299
870
  /** Aborted when a child trips a stream circuit breaker: parallel sibling
300
871
  * subagents run concurrently on the parent's signal, and rejecting the
@@ -307,12 +878,23 @@ export class SubagentExecutor {
307
878
  private readonly hookRegistry?: HookRegistry;
308
879
  private readonly parentRunId: string;
309
880
  private readonly parentAgentId?: string;
881
+ private readonly executionContext: SubagentExecutionContext;
310
882
  private readonly langfuse?: StandardGraphInput['langfuse'];
311
883
  private readonly tokenCounter?: TokenCounter;
312
884
  private readonly streamLimits?: StandardGraphInput['streamLimits'];
885
+ private readonly humanInTheLoop?: HumanInTheLoopConfig;
886
+ private readonly checkpointer?: BaseCheckpointSaver;
313
887
  private readonly maxDepth: number;
314
888
  private readonly createChildGraph: ChildGraphFactory;
889
+ private readonly createChildGraphByKind?: GraphFactory;
315
890
  private readonly usageSink?: SubagentUsageSink;
891
+ private readonly executions: SubagentExecutionRegistry<
892
+ SubagentExecuteResult,
893
+ ResolvedSubagentConfig,
894
+ ActiveChildRun,
895
+ PersistedToolOutput
896
+ >;
897
+ private replayCheckpointWorkflow?: ReplayCheckpointWorkflow;
316
898
  private readonly resolveParentHandlerRegistry?: () =>
317
899
  | HandlerRegistry
318
900
  | undefined;
@@ -324,11 +906,28 @@ export class SubagentExecutor {
324
906
  this.hookRegistry = options.hookRegistry;
325
907
  this.parentRunId = options.parentRunId;
326
908
  this.parentAgentId = options.parentAgentId;
909
+ this.executionContext = options.executionContext ?? {
910
+ rootRunId: options.parentRunId,
911
+ hookSessionId: options.parentRunId,
912
+ depth: 0,
913
+ ancestry: [],
914
+ };
327
915
  this.langfuse = options.langfuse;
328
916
  this.tokenCounter = options.tokenCounter;
329
917
  this.streamLimits = options.streamLimits;
918
+ this.humanInTheLoop = options.humanInTheLoop;
919
+ this.checkpointer = isCheckpointSaver(options.checkpointer)
920
+ ? options.checkpointer
921
+ : undefined;
922
+ this.executions = new SubagentExecutionRegistry({
923
+ parentRunId: options.parentRunId,
924
+ parentAgentId: options.parentAgentId,
925
+ durable:
926
+ options.humanInTheLoop?.enabled === true && this.checkpointer != null,
927
+ });
330
928
  this.maxDepth = options.maxDepth ?? 1;
331
929
  this.createChildGraph = options.createChildGraph;
930
+ this.createChildGraphByKind = options.createChildGraphByKind;
332
931
  this.usageSink = options.usageSink;
333
932
  const rawRegistry = options.parentHandlerRegistry;
334
933
  if (typeof rawRegistry === 'function') {
@@ -347,20 +946,1018 @@ export class SubagentExecutor {
347
946
  /** One signal that fires on the parent's abort or the breaker abort,
348
947
  * collapsed to a single signal when possible (mirrors
349
948
  * `composeAbortSignals` in Graph.ts). */
350
- private composeChildSignal(breaker: AbortController): AbortSignal {
351
- const child = breaker.signal;
352
- if (this.parentSignal == null || this.parentSignal === child) {
353
- return child;
949
+ private composeChildSignal(
950
+ breaker: AbortController,
951
+ executionSignal?: AbortSignal
952
+ ): AbortSignal {
953
+ const parentAndBreaker = composeAbortSignals(
954
+ this.parentSignal,
955
+ breaker.signal
956
+ );
957
+ return (
958
+ composeAbortSignals(parentAndBreaker, executionSignal) ?? breaker.signal
959
+ );
960
+ }
961
+
962
+ /** Snapshot of the parent's registry at the moment a subagent is dispatched. */
963
+ private getParentHandlerRegistry(): HandlerRegistry | undefined {
964
+ return this.resolveParentHandlerRegistry?.();
965
+ }
966
+
967
+ private bindExecutionDefinition(
968
+ execution: DurableExecutionRecord,
969
+ binding: SubagentDefinitionBinding,
970
+ authority: 'provisional' | 'effective'
971
+ ): boolean {
972
+ try {
973
+ execution.bindDefinition(binding, authority);
974
+ return true;
975
+ } catch (error) {
976
+ if (error instanceof SubagentDefinitionBindingError) {
977
+ return false;
978
+ }
979
+ throw error;
980
+ }
981
+ }
982
+
983
+ /** Resolve one lazy descriptor per stable child execution. Concurrent
984
+ * duplicate dispatches share the same in-flight resolution; HITL re-entry
985
+ * on this executor reuses the resolved config until the child settles. */
986
+ private resolveExecutionConfig(
987
+ config: ExecutableSubagentConfigEntry,
988
+ execution: DurableExecutionRecord,
989
+ context: {
990
+ childRunId: string;
991
+ childSignal: AbortSignal;
992
+ threadId?: string;
993
+ parentToolCallId?: string;
994
+ parentConfigurable?: Record<string, unknown>;
995
+ }
996
+ ): Promise<ResolvedSubagentConfigEntry> {
997
+ if (isGraphSubagentConfig(config) || config.agentInputs != null) {
998
+ return Promise.resolve(config as ResolvedSubagentConfigEntry);
999
+ }
1000
+ const resolver = config.resolveAgentInputs;
1001
+ const configId = config.configId;
1002
+ return execution.resolveConfig(
1003
+ (): Promise<ResolvedSubagentConfig> =>
1004
+ resolveAgentInputsWithSignal(
1005
+ (): Promise<AgentInputs> =>
1006
+ resolver({
1007
+ descriptor: {
1008
+ type: config.type,
1009
+ name: config.name,
1010
+ description: config.description,
1011
+ configId,
1012
+ },
1013
+ executionId: context.childRunId,
1014
+ parentRunId: this.parentRunId,
1015
+ parentAgentId: this.parentAgentId,
1016
+ parentToolCallId: context.parentToolCallId,
1017
+ threadId: context.threadId,
1018
+ signal: context.childSignal,
1019
+ configurable:
1020
+ context.parentConfigurable == null
1021
+ ? undefined
1022
+ : sanitizeResolverConfigurable(context.parentConfigurable),
1023
+ }),
1024
+ context.childSignal
1025
+ ).then(
1026
+ (agentInputs): ResolvedSubagentConfig => ({
1027
+ ...config,
1028
+ agentInputs,
1029
+ })
1030
+ ),
1031
+ context.childSignal
1032
+ );
1033
+ }
1034
+
1035
+ /**
1036
+ * Keeps the original child thread as an immutable resume source once a
1037
+ * different parent Run reconstructs it. Each rebuilt parent gets a private
1038
+ * checkpoint fork, while the persisted child run ID remains stable for
1039
+ * activity and usage correlation across the interrupt boundary.
1040
+ */
1041
+ private async resolveChildExecutionIdentity(
1042
+ execution: DurableExecutionRecord
1043
+ ): Promise<SubagentExecutionIdentity> {
1044
+ return execution.resolveIdentity(async () => {
1045
+ const preparation = await this.createChildExecutionIdentity(execution);
1046
+ const { identity } = preparation;
1047
+ return {
1048
+ identity,
1049
+ lease: preparation.lease,
1050
+ commit: (lease): void => {
1051
+ const { resumeExecution } = execution;
1052
+ if (resumeExecution != null) {
1053
+ this.executions.retireResumeSources(
1054
+ execution,
1055
+ new Set(
1056
+ resumeExecution.checkpoints.map(
1057
+ (checkpoint) => checkpoint.threadId
1058
+ )
1059
+ ),
1060
+ (source) => {
1061
+ if (source.activeRun != null) {
1062
+ this.clearChildGraph(source.activeRun.graph);
1063
+ }
1064
+ if (source.identity != null) {
1065
+ this.hookRegistry?.clearSession(
1066
+ source.identity.approvalExecutionScope
1067
+ );
1068
+ }
1069
+ }
1070
+ );
1071
+ }
1072
+ preparation.commit?.(lease);
1073
+ if (
1074
+ this.humanInTheLoop?.enabled === true &&
1075
+ this.checkpointer != null
1076
+ ) {
1077
+ this.executions.rememberCheckpointThread(identity.childThreadId);
1078
+ }
1079
+ },
1080
+ rollback: async (lease): Promise<void> => {
1081
+ await preparation.rollback?.(lease);
1082
+ },
1083
+ };
1084
+ });
1085
+ }
1086
+
1087
+ private async createChildExecutionIdentity(
1088
+ execution: DurableExecutionRecord
1089
+ ): Promise<PreparedSubagentExecutionIdentity> {
1090
+ const {
1091
+ baseChildThreadId,
1092
+ branchChildThreadId,
1093
+ currentChildRunId,
1094
+ explicitResumeAttempt,
1095
+ resumeAttemptId,
1096
+ } = execution.address;
1097
+ if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null) {
1098
+ return this.prepareIdentity(execution, {
1099
+ childRunId: currentChildRunId,
1100
+ childThreadId: baseChildThreadId,
1101
+ approvalExecutionScope: currentChildRunId,
1102
+ });
1103
+ }
1104
+
1105
+ const { resumeExecution } = execution;
1106
+ if (resumeExecution != null) {
1107
+ const approvalExecutionScope = getSubagentApprovalExecutionScope(
1108
+ resumeExecution.childRunId,
1109
+ resumeAttemptId
1110
+ );
1111
+ const identity = {
1112
+ childRunId: resumeExecution.childRunId,
1113
+ childThreadId: branchChildThreadId,
1114
+ approvalExecutionScope,
1115
+ };
1116
+ const lease = this.executions.beginIdentityPreparation(
1117
+ execution,
1118
+ identity
1119
+ );
1120
+ await this.prepareCheckpointFork(
1121
+ resumeExecution.checkpoints,
1122
+ branchChildThreadId,
1123
+ lease
1124
+ );
1125
+ let previousApprovals: ToolApprovalReplaySnapshot[] = [];
1126
+ let approvalsMutated = false;
1127
+ return {
1128
+ identity,
1129
+ lease,
1130
+ commit: (currentLease): void => {
1131
+ const hookRegistry = this.hookRegistry;
1132
+ if (hookRegistry == null) {
1133
+ return;
1134
+ }
1135
+ previousApprovals = hookRegistry.snapshotPendingToolApprovals(
1136
+ approvalExecutionScope,
1137
+ approvalExecutionScope
1138
+ );
1139
+ const approvalReplayCreatesScope =
1140
+ previousApprovals.length === 0 &&
1141
+ resumeExecution.approvalReplays.length > 0;
1142
+ const approvalsChanged =
1143
+ previousApprovals.length > 0 ||
1144
+ resumeExecution.approvalReplays.length > 0;
1145
+ if (approvalReplayCreatesScope) {
1146
+ currentLease.markApprovalScopeCreated(async (): Promise<void> => {
1147
+ hookRegistry.restorePendingToolApprovals(
1148
+ approvalExecutionScope,
1149
+ approvalExecutionScope,
1150
+ previousApprovals
1151
+ );
1152
+ });
1153
+ }
1154
+ approvalsMutated = approvalsChanged && !approvalReplayCreatesScope;
1155
+ hookRegistry.restorePendingToolApprovals(
1156
+ approvalExecutionScope,
1157
+ approvalExecutionScope,
1158
+ resumeExecution.approvalReplays
1159
+ );
1160
+ },
1161
+ rollback: async (currentLease): Promise<void> => {
1162
+ if (
1163
+ approvalsMutated &&
1164
+ currentLease.ownsApprovalScope &&
1165
+ this.hookRegistry != null
1166
+ ) {
1167
+ this.hookRegistry.restorePendingToolApprovals(
1168
+ approvalExecutionScope,
1169
+ approvalExecutionScope,
1170
+ previousApprovals
1171
+ );
1172
+ }
1173
+ },
1174
+ };
1175
+ }
1176
+
1177
+ const branchTuple = await this.checkpointer.getTuple({
1178
+ configurable: { thread_id: branchChildThreadId },
1179
+ });
1180
+ if (branchTuple != null) {
1181
+ const childRunId =
1182
+ getSubagentRunId(getTupleMessages(branchTuple)) ?? currentChildRunId;
1183
+ return this.prepareIdentity(execution, {
1184
+ childRunId,
1185
+ childThreadId: branchChildThreadId,
1186
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1187
+ childRunId,
1188
+ resumeAttemptId
1189
+ ),
1190
+ });
1191
+ }
1192
+
1193
+ const baseTuple = await this.checkpointer.getTuple({
1194
+ configurable: { thread_id: baseChildThreadId },
1195
+ });
1196
+ if (baseTuple == null) {
1197
+ return this.prepareIdentity(execution, {
1198
+ childRunId: currentChildRunId,
1199
+ childThreadId: explicitResumeAttempt
1200
+ ? branchChildThreadId
1201
+ : baseChildThreadId,
1202
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1203
+ currentChildRunId,
1204
+ resumeAttemptId
1205
+ ),
1206
+ });
1207
+ }
1208
+ const persistedChildRunId = getSubagentRunId(getTupleMessages(baseTuple));
1209
+ if (
1210
+ !explicitResumeAttempt &&
1211
+ (persistedChildRunId == null || persistedChildRunId === currentChildRunId)
1212
+ ) {
1213
+ const childRunId = persistedChildRunId ?? currentChildRunId;
1214
+ return this.prepareIdentity(execution, {
1215
+ childRunId,
1216
+ childThreadId: baseChildThreadId,
1217
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1218
+ childRunId,
1219
+ resumeAttemptId
1220
+ ),
1221
+ });
1222
+ }
1223
+ const sourceCheckpoints =
1224
+ await this.getLatestCheckpointSnapshot(baseChildThreadId);
1225
+ if (sourceCheckpoints.length === 0) {
1226
+ throw new Error(
1227
+ `Cannot fork subagent checkpoint thread "${baseChildThreadId}" without a checkpoint ID.`
1228
+ );
1229
+ }
1230
+ const childRunId = persistedChildRunId ?? currentChildRunId;
1231
+ const identity = {
1232
+ childRunId,
1233
+ childThreadId: branchChildThreadId,
1234
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1235
+ childRunId,
1236
+ resumeAttemptId
1237
+ ),
1238
+ };
1239
+ const lease = this.executions.beginIdentityPreparation(execution, identity);
1240
+ await this.prepareCheckpointFork(
1241
+ sourceCheckpoints,
1242
+ branchChildThreadId,
1243
+ lease
1244
+ );
1245
+ return {
1246
+ identity,
1247
+ lease,
1248
+ };
1249
+ }
1250
+
1251
+ private prepareIdentity(
1252
+ execution: DurableExecutionRecord,
1253
+ identity: SubagentExecutionIdentity
1254
+ ): PreparedSubagentExecutionIdentity {
1255
+ return {
1256
+ identity,
1257
+ lease: this.executions.beginIdentityPreparation(execution, identity),
1258
+ };
1259
+ }
1260
+
1261
+ private async prepareCheckpointFork(
1262
+ sources: ReadonlyArray<SubagentCheckpointReference>,
1263
+ targetThreadId: string,
1264
+ lease: SubagentExecutionPreparationLease
1265
+ ): Promise<void> {
1266
+ const checkpointer = this.checkpointer;
1267
+ try {
1268
+ if (
1269
+ checkpointer == null ||
1270
+ sources.length === 0 ||
1271
+ sources.every((source) => source.threadId === targetThreadId)
1272
+ ) {
1273
+ lease.assertOwned();
1274
+ return;
1275
+ }
1276
+ const targetExists = await this.checkpointThreadExists(targetThreadId);
1277
+ lease.assertOwned();
1278
+ if (!targetExists) {
1279
+ lease.markCheckpointCreated(() =>
1280
+ checkpointer.deleteThread(targetThreadId)
1281
+ );
1282
+ }
1283
+ await this.forkCheckpointSnapshot(sources, targetThreadId);
1284
+ lease.assertOwned();
1285
+ } catch (error) {
1286
+ return lease.rollback(error);
1287
+ }
1288
+ }
1289
+
1290
+ private async checkpointThreadExists(threadId: string): Promise<boolean> {
1291
+ if (this.checkpointer == null) {
1292
+ return false;
1293
+ }
1294
+ for await (const _tuple of this.checkpointer.list(
1295
+ { configurable: { thread_id: threadId } },
1296
+ { limit: 1 }
1297
+ )) {
1298
+ return true;
1299
+ }
1300
+ return false;
1301
+ }
1302
+
1303
+ /** Captures one exact checkpoint head per namespace for a child thread. */
1304
+ private async getLatestCheckpointSnapshot(
1305
+ threadId: string
1306
+ ): Promise<SubagentCheckpointReference[]> {
1307
+ if (this.checkpointer == null) {
1308
+ return [];
1309
+ }
1310
+ const checkpointsByNamespace = new Map<
1311
+ string,
1312
+ SubagentCheckpointReference
1313
+ >();
1314
+ for await (const tuple of this.checkpointer.list({
1315
+ configurable: { thread_id: threadId },
1316
+ })) {
1317
+ const checkpoint = getCheckpointReference(tuple);
1318
+ const current =
1319
+ checkpoint == null
1320
+ ? undefined
1321
+ : checkpointsByNamespace.get(checkpoint.checkpointNs);
1322
+ if (
1323
+ checkpoint != null &&
1324
+ (current == null ||
1325
+ checkpoint.checkpointId.localeCompare(current.checkpointId) > 0)
1326
+ ) {
1327
+ checkpointsByNamespace.set(checkpoint.checkpointNs, checkpoint);
1328
+ }
1329
+ }
1330
+ return [...checkpointsByNamespace.values()].sort((left, right) =>
1331
+ left.checkpointNs.localeCompare(right.checkpointNs)
1332
+ );
1333
+ }
1334
+
1335
+ /** Copies exact checkpoint lineages, including pending task writes. */
1336
+ private async forkCheckpointSnapshot(
1337
+ sources: ReadonlyArray<SubagentCheckpointReference>,
1338
+ targetThreadId: string
1339
+ ): Promise<void> {
1340
+ if (
1341
+ this.checkpointer == null ||
1342
+ sources.length === 0 ||
1343
+ sources.every((source) => source.threadId === targetThreadId)
1344
+ ) {
1345
+ return;
1346
+ }
1347
+ for (const source of sources) {
1348
+ const tuples: CheckpointTuple[] = [];
1349
+ const visited = new Set<string>();
1350
+ let tuple = await this.checkpointer.getTuple({
1351
+ configurable: {
1352
+ thread_id: source.threadId,
1353
+ checkpoint_ns: source.checkpointNs,
1354
+ checkpoint_id: source.checkpointId,
1355
+ },
1356
+ });
1357
+ if (tuple == null) {
1358
+ throw new Error(
1359
+ `Subagent checkpoint "${source.checkpointId}" is unavailable.`
1360
+ );
1361
+ }
1362
+ for (;;) {
1363
+ const reference = getCheckpointReference(tuple);
1364
+ if (reference == null) {
1365
+ throw new Error(
1366
+ 'Subagent checkpoint lineage contains an invalid tuple.'
1367
+ );
1368
+ }
1369
+ if (
1370
+ reference.threadId !== source.threadId ||
1371
+ reference.checkpointNs !== source.checkpointNs
1372
+ ) {
1373
+ throw new Error(
1374
+ 'Subagent checkpoint lineage escapes its source namespace.'
1375
+ );
1376
+ }
1377
+ const lineageKey = JSON.stringify([
1378
+ reference.threadId,
1379
+ reference.checkpointNs,
1380
+ reference.checkpointId,
1381
+ ]);
1382
+ if (visited.has(lineageKey)) {
1383
+ throw new Error('Subagent checkpoint lineage contains a cycle.');
1384
+ }
1385
+ visited.add(lineageKey);
1386
+ tuples.push(tuple);
1387
+ if (tuple.parentConfig == null) {
1388
+ break;
1389
+ }
1390
+ tuple = await this.checkpointer.getTuple(tuple.parentConfig);
1391
+ if (tuple == null) {
1392
+ throw new Error('Subagent checkpoint lineage is incomplete.');
1393
+ }
1394
+ }
1395
+ tuples.reverse();
1396
+
1397
+ for (const lineageTuple of tuples) {
1398
+ const checkpointNs =
1399
+ getConfigurableString(lineageTuple.config, 'checkpoint_ns') ?? '';
1400
+ const parentCheckpointId = getConfigurableString(
1401
+ lineageTuple.parentConfig,
1402
+ 'checkpoint_id'
1403
+ );
1404
+ const storedConfig = await this.checkpointer.put(
1405
+ {
1406
+ configurable: {
1407
+ thread_id: targetThreadId,
1408
+ checkpoint_ns: checkpointNs,
1409
+ ...(parentCheckpointId == null
1410
+ ? {}
1411
+ : { checkpoint_id: parentCheckpointId }),
1412
+ },
1413
+ },
1414
+ copyCheckpoint(lineageTuple.checkpoint),
1415
+ lineageTuple.metadata ?? {
1416
+ source: 'fork',
1417
+ step: -1,
1418
+ parents: {},
1419
+ },
1420
+ lineageTuple.checkpoint.channel_versions
1421
+ );
1422
+ const writesByTask = new Map<string, Array<[string, unknown]>>();
1423
+ for (const [taskId, channel, value] of lineageTuple.pendingWrites ??
1424
+ []) {
1425
+ let writes = writesByTask.get(taskId);
1426
+ if (writes == null) {
1427
+ writes = [];
1428
+ writesByTask.set(taskId, writes);
1429
+ }
1430
+ writes.push([channel, value]);
1431
+ }
1432
+ for (const [taskId, writes] of writesByTask) {
1433
+ await this.checkpointer.putWrites(storedConfig, writes, taskId);
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+
1439
+ private async createResumeManifest(
1440
+ records: ReadonlyArray<DurableExecutionRecord>
1441
+ ): Promise<SubagentResumeManifest | undefined> {
1442
+ if (this.checkpointer == null) {
1443
+ return undefined;
1444
+ }
1445
+ const executions: SubagentResumeExecution[] = [];
1446
+ for (const record of records) {
1447
+ const { identity, binding, activeRun } = record.snapshot;
1448
+ if (identity == null) {
1449
+ continue;
1450
+ }
1451
+ const { parentToolCallId } = record.address;
1452
+ const checkpoints = await this.getLatestCheckpointSnapshot(
1453
+ identity.childThreadId
1454
+ );
1455
+ if (checkpoints.length === 0) {
1456
+ continue;
1457
+ }
1458
+ const configuredHookSessionId =
1459
+ activeRun?.invokeConfig?.configurable?.run_id;
1460
+ const hookSessionId =
1461
+ typeof configuredHookSessionId === 'string' &&
1462
+ configuredHookSessionId.length > 0
1463
+ ? configuredHookSessionId
1464
+ : this.parentRunId;
1465
+ const approvalReplays =
1466
+ this.hookRegistry?.snapshotPendingToolApprovals(
1467
+ identity.approvalExecutionScope,
1468
+ identity.approvalExecutionScope
1469
+ ) ?? [];
1470
+ const descendant = activeRun?.pendingInterrupts
1471
+ .map((pendingInterrupt) =>
1472
+ getSubagentResumeManifest(pendingInterrupt.value)
1473
+ )
1474
+ .find((manifest) => manifest != null);
1475
+ const graphState = activeRun?.graph.createSubagentResumeState(
1476
+ hookSessionId
1477
+ ) ?? {
1478
+ toolCallSteps: [],
1479
+ toolSessions: [],
1480
+ toolNodes: [],
1481
+ eagerToolUsage: [],
1482
+ eagerToolSuppressions: [],
1483
+ };
1484
+ executions.push({
1485
+ parentToolCallId,
1486
+ childRunId: identity.childRunId,
1487
+ ...(binding?.subagentType == null
1488
+ ? {}
1489
+ : { subagentType: binding.subagentType }),
1490
+ ...(binding?.configId == null ? {} : { configId: binding.configId }),
1491
+ approvalExecutionScope: identity.approvalExecutionScope,
1492
+ checkpoints,
1493
+ graphState,
1494
+ approvalReplays,
1495
+ ...(descendant == null ? {} : { descendant }),
1496
+ });
1497
+ }
1498
+ if (executions.length === 0) {
1499
+ return undefined;
1500
+ }
1501
+ return {
1502
+ version: 1,
1503
+ executions,
1504
+ };
1505
+ }
1506
+
1507
+ getResumeManifest(
1508
+ parentToolCallIds?: ReadonlySet<string>,
1509
+ config?: RunnableConfig
1510
+ ): Promise<SubagentResumeManifest | undefined> {
1511
+ return this.createResumeManifest(
1512
+ this.executions.selectForResume({ parentToolCallIds, config })
1513
+ );
1514
+ }
1515
+
1516
+ getChildCheckpointThreadIds(): string[] {
1517
+ return this.executions.getCheckpointThreadIds((activeRun) =>
1518
+ this.getGraphChildCheckpointThreadIds(activeRun.graph)
1519
+ );
1520
+ }
1521
+
1522
+ resetCheckpointThreadIds(): void {
1523
+ this.executions.resetCheckpointThreadIds();
1524
+ }
1525
+
1526
+ private getGraphChildCheckpointThreadIds(graph: StandardGraph): string[] {
1527
+ const checkpointGraph = graph as {
1528
+ getChildCheckpointThreadIds?: () => string[];
1529
+ };
1530
+ return checkpointGraph.getChildCheckpointThreadIds?.() ?? [];
1531
+ }
1532
+
1533
+ private clearChildGraph(graph: StandardGraph): void {
1534
+ this.executions.rememberCheckpointThread(
1535
+ ...this.getGraphChildCheckpointThreadIds(graph)
1536
+ );
1537
+ graph.clearHeavyState();
1538
+ }
1539
+
1540
+ clearHeavyState(): void {
1541
+ this.executions.clear((record) => {
1542
+ if (record.activeRun != null) {
1543
+ this.clearChildGraph(record.activeRun.graph);
1544
+ }
1545
+ if (record.identity != null) {
1546
+ this.hookRegistry?.clearSession(record.identity.approvalExecutionScope);
1547
+ }
1548
+ });
1549
+ this.replayCheckpointWorkflow = undefined;
1550
+ }
1551
+
1552
+ private async getResumeCheckpointMarker(
1553
+ resumeExecution: SubagentResumeExecution,
1554
+ parentToolCallId: string
1555
+ ): Promise<
1556
+ { marker: SubagentCheckpointMarker; messages: BaseMessage[] } | undefined
1557
+ > {
1558
+ if (this.checkpointer == null) {
1559
+ return undefined;
1560
+ }
1561
+ let fallback:
1562
+ | { marker: SubagentCheckpointMarker; messages: BaseMessage[] }
1563
+ | undefined;
1564
+ for (const checkpoint of resumeExecution.checkpoints) {
1565
+ const tuple = await this.checkpointer.getTuple({
1566
+ configurable: {
1567
+ thread_id: checkpoint.threadId,
1568
+ checkpoint_ns: checkpoint.checkpointNs,
1569
+ checkpoint_id: checkpoint.checkpointId,
1570
+ },
1571
+ });
1572
+ const messages = getTupleMessages(tuple);
1573
+ const marker = getSubagentCheckpointMarker(messages, parentToolCallId);
1574
+ if (marker?.settledOutput != null) {
1575
+ return { marker, messages };
1576
+ }
1577
+ if (marker != null && fallback == null) {
1578
+ fallback = { marker, messages };
1579
+ }
1580
+ }
1581
+ return fallback;
1582
+ }
1583
+
1584
+ async getSettledToolOutput(
1585
+ call: ToolCall,
1586
+ config: RunnableConfig
1587
+ ): Promise<SettledSubagentToolOutput | undefined> {
1588
+ const parentToolCallId = call.id;
1589
+ if (
1590
+ this.humanInTheLoop?.enabled !== true ||
1591
+ this.checkpointer == null ||
1592
+ parentToolCallId == null ||
1593
+ parentToolCallId === ''
1594
+ ) {
1595
+ return undefined;
1596
+ }
1597
+ const parentConfigurable = config.configurable as
1598
+ | Record<string, unknown>
1599
+ | undefined;
1600
+ const threadId = parentConfigurable?.thread_id;
1601
+ const execution = this.executions.open({
1602
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1603
+ parentToolCallId,
1604
+ parentConfigurable,
1605
+ });
1606
+ const { resumeExecution } = execution;
1607
+ const inProcessSettledOutput = execution.settledOutput;
1608
+ if (inProcessSettledOutput != null) {
1609
+ const persistedSubagentType = getSubagentTypeFromArgs(
1610
+ inProcessSettledOutput.resolvedArgs
1611
+ );
1612
+ const subagentType =
1613
+ persistedSubagentType ??
1614
+ execution.binding?.subagentType ??
1615
+ getSubagentType(call);
1616
+ const executableConfig =
1617
+ subagentType == null ? undefined : this.configs.get(subagentType);
1618
+ if (
1619
+ !isResumeExecutionCompatible(
1620
+ resumeExecution,
1621
+ subagentType,
1622
+ executableConfig,
1623
+ persistedSubagentType != null
1624
+ )
1625
+ ) {
1626
+ return undefined;
1627
+ }
1628
+ return deserializeToolOutput(inProcessSettledOutput);
1629
+ }
1630
+ let resumeCheckpoint:
1631
+ | { marker: SubagentCheckpointMarker; messages: BaseMessage[] }
1632
+ | undefined;
1633
+ if (resumeExecution != null) {
1634
+ resumeCheckpoint = await this.getResumeCheckpointMarker(
1635
+ resumeExecution,
1636
+ parentToolCallId
1637
+ );
1638
+ }
1639
+ let marker = resumeCheckpoint?.marker;
1640
+ let messages = resumeCheckpoint?.messages ?? [];
1641
+ let persistedSubagentType = getSubagentTypeFromArgs(
1642
+ marker?.settledOutput?.resolvedArgs
1643
+ );
1644
+ let subagentType =
1645
+ persistedSubagentType ??
1646
+ resumeExecution?.subagentType ??
1647
+ getSubagentType(call);
1648
+ let executableConfig =
1649
+ subagentType == null ? undefined : this.configs.get(subagentType);
1650
+ if (
1651
+ !isResumeExecutionCompatible(
1652
+ resumeExecution,
1653
+ subagentType,
1654
+ executableConfig,
1655
+ persistedSubagentType != null
1656
+ )
1657
+ ) {
1658
+ this.executions.remove(execution);
1659
+ return undefined;
1660
+ }
1661
+ const { childThreadId } =
1662
+ await this.resolveChildExecutionIdentity(execution);
1663
+ if (marker == null) {
1664
+ const checkpoint = await this.checkpointer.getTuple({
1665
+ configurable: { thread_id: childThreadId },
1666
+ });
1667
+ messages = getCheckpointMessages(
1668
+ checkpoint?.checkpoint.channel_values.messages
1669
+ );
1670
+ marker = getSubagentCheckpointMarker(messages, parentToolCallId);
1671
+ persistedSubagentType = getSubagentTypeFromArgs(
1672
+ marker?.settledOutput?.resolvedArgs
1673
+ );
1674
+ if (persistedSubagentType != null) {
1675
+ subagentType = persistedSubagentType;
1676
+ executableConfig = this.configs.get(subagentType);
1677
+ }
1678
+ if (
1679
+ !isResumeExecutionCompatible(
1680
+ resumeExecution,
1681
+ subagentType,
1682
+ executableConfig,
1683
+ persistedSubagentType != null
1684
+ )
1685
+ ) {
1686
+ this.executions.remove(execution);
1687
+ return undefined;
1688
+ }
1689
+ }
1690
+ const configId = executableConfig?.configId;
1691
+ const bound = this.bindExecutionDefinition(
1692
+ execution,
1693
+ {
1694
+ ...(subagentType == null ? {} : { subagentType }),
1695
+ ...(configId == null ? {} : { configId }),
1696
+ },
1697
+ persistedSubagentType != null || resumeExecution?.subagentType != null
1698
+ ? 'effective'
1699
+ : 'provisional'
1700
+ );
1701
+ if (!bound) {
1702
+ return undefined;
1703
+ }
1704
+ const persistedHookSessionId =
1705
+ marker?.hookSessionId ?? getSubagentHookSessionId(messages);
1706
+ const currentHookSessionId = parentConfigurable?.run_id;
1707
+ if (
1708
+ persistedHookSessionId != null &&
1709
+ typeof currentHookSessionId === 'string' &&
1710
+ currentHookSessionId.length > 0
1711
+ ) {
1712
+ this.hookRegistry?.copySession(
1713
+ persistedHookSessionId,
1714
+ currentHookSessionId
1715
+ );
1716
+ }
1717
+ return marker?.settledOutput == null
1718
+ ? undefined
1719
+ : deserializeToolOutput(marker.settledOutput);
1720
+ }
1721
+
1722
+ async persistSettledToolOutput(
1723
+ call: ToolCall,
1724
+ config: RunnableConfig,
1725
+ settled: SettledSubagentToolOutput
1726
+ ): Promise<void> {
1727
+ const parentToolCallId = call.id;
1728
+ const checkpointer = this.checkpointer;
1729
+ if (
1730
+ this.humanInTheLoop?.enabled !== true ||
1731
+ checkpointer == null ||
1732
+ parentToolCallId == null ||
1733
+ parentToolCallId === ''
1734
+ ) {
1735
+ return;
1736
+ }
1737
+ const parentConfigurable = config.configurable as
1738
+ | Record<string, unknown>
1739
+ | undefined;
1740
+ const threadId = parentConfigurable?.thread_id;
1741
+ const execution = this.executions.open({
1742
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1743
+ parentToolCallId,
1744
+ parentConfigurable,
1745
+ });
1746
+ const { resumeExecution } = execution;
1747
+ const resolvedSubagentType = getSubagentTypeFromArgs(settled.resolvedArgs);
1748
+ const resolvedDescription = getSubagentDescriptionFromArgs(
1749
+ settled.resolvedArgs
1750
+ );
1751
+ const boundInvocation = execution.invocation;
1752
+ if (
1753
+ boundInvocation != null &&
1754
+ resolvedDescription != null &&
1755
+ resolvedDescription !== boundInvocation.description
1756
+ ) {
1757
+ return;
1758
+ }
1759
+ const description =
1760
+ boundInvocation?.description ??
1761
+ resolvedDescription ??
1762
+ getSubagentDescriptionFromArgs(call.args) ??
1763
+ DEFAULT_SUBAGENT_DESCRIPTION;
1764
+ const subagentType =
1765
+ resolvedSubagentType ??
1766
+ execution.binding?.subagentType ??
1767
+ getSubagentType(call);
1768
+ const executableConfig =
1769
+ subagentType == null ? undefined : this.configs.get(subagentType);
1770
+ const configId =
1771
+ resolvedSubagentType == null
1772
+ ? (execution.binding?.configId ??
1773
+ executableConfig?.configId ??
1774
+ resumeExecution?.configId)
1775
+ : executableConfig?.configId;
1776
+ if (subagentType == null) {
1777
+ return;
1778
+ }
1779
+ if (
1780
+ !isResumeExecutionCompatible(
1781
+ resumeExecution,
1782
+ subagentType,
1783
+ executableConfig
1784
+ )
1785
+ ) {
1786
+ this.executions.remove(execution);
1787
+ return;
1788
+ }
1789
+ const persistedOutput = serializeToolOutput(settled);
1790
+ const invocation = boundInvocation ?? {
1791
+ description,
1792
+ subagentType,
1793
+ ...(configId == null ? {} : { configId }),
1794
+ };
1795
+ try {
1796
+ await execution.settle(
1797
+ {
1798
+ definitionAuthority:
1799
+ resolvedSubagentType == null ? 'provisional' : 'effective',
1800
+ fingerprint: getSettlementFingerprint(persistedOutput),
1801
+ invocation,
1802
+ subagentType,
1803
+ ...(configId == null ? {} : { configId }),
1804
+ },
1805
+ persistedOutput,
1806
+ async (): Promise<void> => {
1807
+ const { childRunId, childThreadId } =
1808
+ await this.resolveChildExecutionIdentity(execution);
1809
+ const activeChildRun = execution.activeRun;
1810
+ if (activeChildRun != null) {
1811
+ await this.persistChildCheckpointMarker(
1812
+ activeChildRun,
1813
+ parentToolCallId,
1814
+ persistedOutput
1815
+ );
1816
+ this.clearChildGraph(activeChildRun.graph);
1817
+ return;
1818
+ }
1819
+ this.replayCheckpointWorkflow ??=
1820
+ createReplayCheckpointWorkflow(checkpointer);
1821
+ await this.replayCheckpointWorkflow.updateState(
1822
+ { configurable: { thread_id: childThreadId } },
1823
+ {
1824
+ messages: [
1825
+ createSubagentCheckpointMarkerMessage({
1826
+ version: 1,
1827
+ parentToolCallId,
1828
+ lifecycleComplete: true,
1829
+ hookSessionId:
1830
+ typeof parentConfigurable?.run_id === 'string'
1831
+ ? parentConfigurable.run_id
1832
+ : this.parentRunId,
1833
+ childRunId,
1834
+ settledOutput: persistedOutput,
1835
+ }),
1836
+ ],
1837
+ },
1838
+ SUBAGENT_REPLAY_NODE
1839
+ );
1840
+ }
1841
+ );
1842
+ } catch (error) {
1843
+ if (
1844
+ error instanceof SubagentDefinitionBindingError ||
1845
+ error instanceof SubagentInvocationBindingError ||
1846
+ error instanceof SubagentSettlementBindingError
1847
+ ) {
1848
+ return;
1849
+ }
1850
+ throw error;
1851
+ }
1852
+ }
1853
+
1854
+ private async persistChildCheckpointMarker(
1855
+ activeChildRun: ActiveChildRun,
1856
+ parentToolCallId: string,
1857
+ settledOutput?: PersistedToolOutput
1858
+ ): Promise<void> {
1859
+ if (
1860
+ this.humanInTheLoop?.enabled !== true ||
1861
+ activeChildRun.workflow.updateState == null ||
1862
+ activeChildRun.invokeConfig == null
1863
+ ) {
1864
+ return;
354
1865
  }
355
- return AbortSignal.any([this.parentSignal, child]);
1866
+ await activeChildRun.workflow.updateState(
1867
+ activeChildRun.invokeConfig,
1868
+ {
1869
+ messages: [
1870
+ createSubagentCheckpointMarkerMessage({
1871
+ version: 1,
1872
+ parentToolCallId,
1873
+ lifecycleComplete: true,
1874
+ hookSessionId:
1875
+ typeof activeChildRun.invokeConfig.configurable?.run_id ===
1876
+ 'string'
1877
+ ? activeChildRun.invokeConfig.configurable.run_id
1878
+ : this.parentRunId,
1879
+ childRunId: activeChildRun.childRunId,
1880
+ ...(settledOutput == null ? {} : { settledOutput }),
1881
+ }),
1882
+ ],
1883
+ },
1884
+ activeChildRun.checkpointNodeId
1885
+ );
356
1886
  }
357
1887
 
358
- /** Snapshot of the parent's registry at the moment a subagent is dispatched. */
359
- private getParentHandlerRegistry(): HandlerRegistry | undefined {
360
- return this.resolveParentHandlerRegistry?.();
1888
+ execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
1889
+ const executableConfig = this.configs.get(params.subagentType);
1890
+ if (executableConfig == null) {
1891
+ const available = [...this.configs.keys()].join(', ');
1892
+ return Promise.resolve({
1893
+ content: `Error: Unknown subagent type "${params.subagentType}". Available types: ${available}`,
1894
+ messages: [],
1895
+ });
1896
+ }
1897
+ if (this.maxDepth <= 0) {
1898
+ return Promise.resolve({
1899
+ content: 'Error: Maximum subagent nesting depth exceeded.',
1900
+ messages: [],
1901
+ });
1902
+ }
1903
+ if (
1904
+ isGraphSubagentConfig(executableConfig) &&
1905
+ this.humanInTheLoop?.enabled === true
1906
+ ) {
1907
+ return Promise.resolve({
1908
+ content:
1909
+ 'Error: Human-in-the-loop execution is not yet supported for graph subagents.',
1910
+ messages: [],
1911
+ });
1912
+ }
1913
+ if (
1914
+ this.humanInTheLoop?.enabled === true &&
1915
+ (params.parentToolCallId == null || params.parentToolCallId === '')
1916
+ ) {
1917
+ return Promise.resolve({
1918
+ content:
1919
+ 'Error: Resumable subagent execution requires a parent tool call ID.',
1920
+ messages: [],
1921
+ });
1922
+ }
1923
+ const execution = this.executions.open({
1924
+ threadId: params.threadId,
1925
+ parentToolCallId: params.parentToolCallId ?? nanoid(8),
1926
+ parentConfigurable: params.parentConfigurable,
1927
+ });
1928
+ try {
1929
+ return execution.execute(
1930
+ {
1931
+ description: params.description,
1932
+ subagentType: params.subagentType,
1933
+ ...(executableConfig.configId == null
1934
+ ? {}
1935
+ : { configId: executableConfig.configId }),
1936
+ },
1937
+ () => this.executeOnce(params, execution, executableConfig)
1938
+ );
1939
+ } catch (error) {
1940
+ if (error instanceof SubagentDefinitionBindingError) {
1941
+ return Promise.resolve({
1942
+ content: SUBAGENT_CONFIG_CHANGED_MESSAGE,
1943
+ messages: [],
1944
+ });
1945
+ }
1946
+ if (error instanceof SubagentInvocationBindingError) {
1947
+ return Promise.resolve({
1948
+ content: SUBAGENT_INVOCATION_CHANGED_MESSAGE,
1949
+ messages: [],
1950
+ });
1951
+ }
1952
+ throw error;
1953
+ }
361
1954
  }
362
1955
 
363
- async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
1956
+ private async executeOnce(
1957
+ params: SubagentExecuteParams,
1958
+ execution: DurableExecutionRecord,
1959
+ executableConfig: ExecutableSubagentConfigEntry
1960
+ ): Promise<SubagentExecuteResult> {
364
1961
  const { description, subagentType, threadId, parentToolCallId } = params;
365
1962
  /** Captured ONCE per execution, preferring the controller the parent
366
1963
  * tool batch captured at ITS entry (before PreToolUse hooks): a failed
@@ -370,57 +1967,73 @@ export class SubagentExecutor {
370
1967
  * new run's breaker from an old child's stream-limit breach. Signal and
371
1968
  * trip target both bind to this capture. */
372
1969
  const childBreaker = params.breaker ?? this.resolveBreakerController();
373
- const childSignal = this.composeChildSignal(childBreaker);
374
- const config = this.configs.get(subagentType);
1970
+ const childSignal = this.composeChildSignal(childBreaker, params.signal);
375
1971
 
376
- if (!config) {
377
- const available = [...this.configs.keys()].join(', ');
1972
+ const { parentToolCallId: executionSuffix } = execution.address;
1973
+ const { resumeExecution } = execution;
1974
+ if (
1975
+ !isResumeExecutionCompatible(
1976
+ resumeExecution,
1977
+ subagentType,
1978
+ executableConfig
1979
+ )
1980
+ ) {
1981
+ this.executions.remove(execution);
378
1982
  return {
379
- content: `Error: Unknown subagent type "${subagentType}". Available types: ${available}`,
1983
+ content: SUBAGENT_CONFIG_CHANGED_MESSAGE,
380
1984
  messages: [],
381
1985
  };
382
1986
  }
383
-
384
- if (this.maxDepth <= 0) {
1987
+ let identity: SubagentExecutionIdentity;
1988
+ try {
1989
+ identity = await this.resolveChildExecutionIdentity(execution);
1990
+ execution.assertUsable(childSignal);
1991
+ } catch (error) {
1992
+ if (error instanceof StreamLimitExceededError) {
1993
+ throw error;
1994
+ }
385
1995
  return {
386
- content: 'Error: Maximum subagent nesting depth exceeded.',
1996
+ content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
387
1997
  messages: [],
388
1998
  };
389
1999
  }
2000
+ const { childRunId, childThreadId, approvalExecutionScope } = identity;
2001
+ const bound = this.bindExecutionDefinition(
2002
+ execution,
2003
+ {
2004
+ subagentType,
2005
+ ...(executableConfig.configId == null
2006
+ ? {}
2007
+ : { configId: executableConfig.configId }),
2008
+ },
2009
+ 'effective'
2010
+ );
2011
+ if (!bound) {
2012
+ return { content: SUBAGENT_CONFIG_CHANGED_MESSAGE, messages: [] };
2013
+ }
2014
+ const completedChildResult = execution.completedResult;
2015
+ if (completedChildResult != null) {
2016
+ return completedChildResult;
2017
+ }
390
2018
 
391
- const childAgentId =
392
- config.agentInputs.agentId ||
393
- `${this.parentAgentId ?? 'agent'}_sub_${nanoid(8)}`;
394
-
395
- if (
396
- this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) === true
397
- ) {
398
- const hookResult = await executeHooks({
399
- registry: this.hookRegistry,
400
- input: {
401
- hook_event_name: 'SubagentStart',
402
- runId: this.parentRunId,
403
- threadId,
404
- parentAgentId: this.parentAgentId,
405
- agentId: childAgentId,
406
- agentType: subagentType,
407
- inputs: [new HumanMessage(description)],
408
- },
409
- sessionId: this.parentRunId,
410
- matchQuery: subagentType,
411
- }).catch((): AggregatedHookResult => HOOK_FALLBACK);
412
-
413
- /**
414
- * `ask` is treated identically to `deny` in the subagent context:
415
- * subagents are non-interactive, so there is no prompt path for `ask`.
416
- * Both decisions block execution and return a "Blocked" tool result.
417
- */
418
- if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
419
- return {
420
- content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
421
- messages: [],
422
- };
2019
+ let config: ResolvedSubagentConfigEntry;
2020
+ try {
2021
+ config = await this.resolveExecutionConfig(executableConfig, execution, {
2022
+ childRunId,
2023
+ childSignal,
2024
+ threadId,
2025
+ parentToolCallId,
2026
+ parentConfigurable: params.parentConfigurable,
2027
+ });
2028
+ execution.assertUsable(childSignal);
2029
+ } catch (error) {
2030
+ if (error instanceof StreamLimitExceededError) {
2031
+ throw error;
423
2032
  }
2033
+ return {
2034
+ content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
2035
+ messages: [],
2036
+ };
424
2037
  }
425
2038
 
426
2039
  const parentRegistry = this.getParentHandlerRegistry();
@@ -436,24 +2049,59 @@ export class SubagentExecutor {
436
2049
  */
437
2050
  const hasToolExecuteHandler =
438
2051
  parentRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) != null;
439
- const childInputs = buildChildInputs(
2052
+ const childPlan = createChildGraphPlan({
440
2053
  config,
441
- childAgentId,
442
- this.maxDepth,
443
- /* keepToolDefinitions */ hasToolExecuteHandler
444
- );
445
- const childRunId = `${this.parentRunId}_sub_${nanoid(8)}`;
446
- const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
2054
+ executionSuffix,
2055
+ parentAgentId: this.parentAgentId,
2056
+ parentMaxDepth: this.maxDepth,
2057
+ keepToolDefinitions: hasToolExecuteHandler,
2058
+ });
2059
+ const childAgentId = childPlan.subjectAgentId;
2060
+ const currentHookSessionId =
2061
+ asNonEmptyString(params.parentConfigurable?.run_id) ??
2062
+ this.executionContext.hookSessionId;
2063
+ const childExecutionContext: SubagentExecutionContext = {
2064
+ rootRunId: this.executionContext.rootRunId,
2065
+ hookSessionId: currentHookSessionId,
2066
+ depth: this.executionContext.depth + 1,
2067
+ ancestry: [
2068
+ ...this.executionContext.ancestry,
2069
+ {
2070
+ subagentRunId: childRunId,
2071
+ subagentType,
2072
+ subagentKind: childPlan.kind,
2073
+ subagentAgentId: childAgentId,
2074
+ parentRunId: this.parentRunId,
2075
+ parentAgentId: this.parentAgentId,
2076
+ parentToolCallId,
2077
+ },
2078
+ ],
2079
+ };
2080
+ const maxTurns = config.maxTurns ?? DEFAULT_SUBAGENT_MAX_TURNS;
2081
+ const memberRecursionLimit = maxTurns * SUBAGENT_RECURSION_MULTIPLIER;
2082
+ const recursionLimit =
2083
+ memberRecursionLimit *
2084
+ (childPlan.kind === 'graph' ? childPlan.agents.length : 1);
447
2085
 
448
2086
  const hostUsageSink = this.usageSink;
449
- const childGraph = this.createChildGraph({
2087
+ let subagentUsageSink: SubagentUsageSink | undefined;
2088
+ if (hostUsageSink != null) {
2089
+ subagentUsageSink = (event): void | Promise<void> =>
2090
+ hostUsageSink({
2091
+ ...event,
2092
+ runId: this.executionContext.rootRunId,
2093
+ });
2094
+ }
2095
+ const cachedChildRun = execution.activeRun;
2096
+ const childGraphInput: StandardGraphInput = {
450
2097
  runId: childRunId,
451
2098
  signal: childSignal,
452
- agents: [childInputs],
2099
+ agents: childPlan.agents,
453
2100
  langfuse: this.langfuse,
454
2101
  tokenCounter: this.tokenCounter,
455
2102
  streamLimits: this.streamLimits,
456
2103
  subagentScope: true,
2104
+ subagentExecutionContext: childExecutionContext,
457
2105
  /**
458
2106
  * Forwarded so the child graph's own `SubagentExecutor` (created in
459
2107
  * its `createAgentNode` when `allowNested` keeps subagentConfigs)
@@ -461,49 +2109,61 @@ export class SubagentExecutor {
461
2109
  * level attaches its own capture callback — `workflow.invoke` replaces
462
2110
  * the inherited callback chain, so a single top-level handler would
463
2111
  * never see grandchild model calls.
464
- *
465
- * The wrapper rewrites `runId` to THIS executor's parent run: nested
466
- * executors emit with their own `parentRunId` (a `*_sub_*` child id),
467
- * and each wrapper layer rewrites upward, so by the time an event
468
- * reaches the host sink its `runId` is the ROOT run — hosts keying
469
- * billing by run id never see intermediate child run ids there
470
- * (`subagentRunId` still identifies the emitting child).
471
2112
  */
472
- subagentUsageSink:
473
- hostUsageSink == null
474
- ? undefined
475
- : /** Returns the host sink's result so async sinks stay awaited
476
- * through every wrapper layer. */
477
- (event): void | Promise<void> =>
478
- hostUsageSink({ ...event, runId: this.parentRunId }),
479
- });
480
-
2113
+ subagentUsageSink,
2114
+ };
2115
+ let childGraph = cachedChildRun?.graph;
2116
+ if (childGraph == null && childPlan.kind === 'graph') {
2117
+ if (this.createChildGraphByKind == null) {
2118
+ return {
2119
+ content:
2120
+ 'Error: Graph subagent execution requires a polymorphic child graph factory.',
2121
+ messages: [],
2122
+ };
2123
+ }
2124
+ childGraph = this.createChildGraphByKind({
2125
+ kind: 'multi-agent',
2126
+ input: {
2127
+ ...childGraphInput,
2128
+ edges: childPlan.edges,
2129
+ resultAgentId: childPlan.resultAgentId,
2130
+ memberRecursionLimit,
2131
+ },
2132
+ });
2133
+ }
2134
+ childGraph ??= this.createChildGraph(childGraphInput);
481
2135
  let forwarding: ForwarderCallback | undefined;
482
2136
  if (forwardingEnabled) {
483
2137
  forwarding = this.createForwarderCallback({
484
2138
  parentRegistry: parentRegistry!,
485
2139
  subagentType,
2140
+ subagentKind: childPlan.kind,
486
2141
  subagentAgentId: childAgentId,
487
2142
  childRunId,
488
2143
  parentToolCallId,
2144
+ executionContext: childExecutionContext,
489
2145
  });
490
2146
  }
491
2147
  const forwarder = forwarding?.handler;
2148
+ let childAlreadyStarted = execution.started;
2149
+ let childAlreadyCompleted = execution.completed;
492
2150
 
493
- if (forwarder) {
494
- await this.emitSubagentUpdate(parentRegistry!, {
495
- childRunId,
496
- subagentType,
497
- subagentAgentId: childAgentId,
498
- parentToolCallId,
499
- phase: 'start',
500
- label: `Subagent "${subagentType}" started`,
501
- });
502
- }
503
-
504
- let result: { messages: BaseMessage[] };
2151
+ let result: MultiAgentGraphState | undefined;
2152
+ let recoveredComplete = false;
2153
+ let recoveredInProgress = false;
505
2154
  try {
506
- const workflow = childGraph.createWorkflow();
2155
+ const workflow = (cachedChildRun?.workflow ??
2156
+ childGraph.createWorkflow()) as StatefulCompiledWorkflow;
2157
+ const activeChildRun = execution.activate(
2158
+ cachedChildRun ?? {
2159
+ graph: childGraph,
2160
+ workflow,
2161
+ pendingInterrupts: [],
2162
+ childAgentId,
2163
+ checkpointNodeId: childPlan.checkpointNodeId,
2164
+ childRunId,
2165
+ }
2166
+ );
507
2167
  /**
508
2168
  * When `parentHandlerRegistry` is provided (forwarding mode), attach a
509
2169
  * lightweight callback that intercepts the child's `on_custom_event`
@@ -537,60 +2197,225 @@ export class SubagentExecutor {
537
2197
  createUsageCaptureHandler({
538
2198
  sink: this.usageSink,
539
2199
  subagentType,
2200
+ subagentKind: childPlan.kind,
540
2201
  subagentRunId: childRunId,
541
2202
  subagentAgentId: childAgentId,
542
2203
  parentRunId: this.parentRunId,
543
- provider: config.agentInputs.provider,
544
- fallbackModel: extractConfiguredModel(config.agentInputs),
2204
+ executionContext: childExecutionContext,
2205
+ memberInputs: childPlan.memberInputs,
545
2206
  })
546
2207
  );
547
2208
  }
548
2209
  const callbacks: Callbacks = callbackHandlers;
549
2210
  /**
550
- * Inherit the parent's host `configurable` host-set fields
551
- * (`requestBody`, `user`, `userMCPAuthMap`, etc.) AND the run-
552
- * identity fields (`run_id`, `parent_run_id`, `thread_id`) all
553
- * propagate. LangGraph's own runtime keys are excluded because the
554
- * child graph creates its own scratchpad/checkpoint/abort plumbing.
555
- *
556
- * Run-identity propagation is intentional and matches the
557
- * convention this executor itself already uses for `SubagentStart`
558
- * / `SubagentStop` hooks (`sessionId: this.parentRunId`): the
559
- * subagent runs under the parent's session scope, not its own.
560
- * Forwarding `run_id` / `parent_run_id` / `thread_id` makes
561
- * `ToolNode`'s hook lookups (`hasHookFor(eventName, runId)`),
562
- * `ToolOutputReferenceRegistry` keying, and trace lineage all
563
- * resolve to the parent's session for tools dispatched from the
564
- * subagent — so `PreToolUse` / `PostToolUse` hooks the host
565
- * registered against the parent's run fire for subagent tool
566
- * calls too. "Same run" matches the user-perceptual mental model.
2211
+ * Inherit the parent's host `configurable` while binding LangGraph's
2212
+ * checkpoint identity to a stable child id derived from the durable
2213
+ * parent thread and checkpoint fork. The parent thread id cannot be
2214
+ * reused here: parent and child share one checkpointer when nested HITL
2215
+ * is enabled, and root checkpoint namespaces are normalized by
2216
+ * LangGraph, so a shared `thread_id` would collide with the parent.
567
2217
  *
568
- * `thread_id` falls back to `childRunId` only when the parent
569
- * didn't supply one (legacy behavior preserved for hosts that
570
- * never set thread_id).
571
- *
572
- * NOTE: a future revision will likely make this configurable per
573
- * spawn type — e.g. a background / async subagent that runs after
574
- * the parent's run completes wants isolation, not inheritance.
575
- * For now the inheritance path matches LibreChat's primary use
576
- * case (synchronous subagents within a single user turn).
2218
+ * `run_id` still propagates as the parent run id, which is the key used
2219
+ * for session-scoped hook lookup. Child hook inputs therefore retain
2220
+ * the parent policy scope while their `threadId` truthfully identifies
2221
+ * the independently checkpointed child execution.
577
2222
  */
578
2223
  const inheritedConfigurable: Record<string, unknown> =
579
2224
  sanitizeChildConfigurable(params.parentConfigurable);
580
- result = await workflow.invoke(
581
- { messages: [new HumanMessage(description)] },
582
- {
583
- recursionLimit: maxTurns * RECURSION_MULTIPLIER,
584
- signal: childSignal,
585
- callbacks,
586
- runName: `subagent:${subagentType}`,
587
- configurable: {
588
- thread_id: childRunId,
589
- ...inheritedConfigurable,
590
- },
2225
+ const { resumeAttemptId } = execution.address;
2226
+ if (cachedChildRun == null && resumeExecution != null) {
2227
+ childGraph.restoreSubagentResumeState(
2228
+ resumeExecution.graphState,
2229
+ currentHookSessionId
2230
+ );
2231
+ }
2232
+ const childConfigurable: Record<string, unknown> = {
2233
+ ...inheritedConfigurable,
2234
+ };
2235
+ if (this.humanInTheLoop?.enabled === true) {
2236
+ childConfigurable[TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY] =
2237
+ approvalExecutionScope;
2238
+ }
2239
+ if (resumeExecution?.descendant != null) {
2240
+ childConfigurable[SUBAGENT_RESUME_MANIFEST_CONFIG_KEY] =
2241
+ resumeExecution.descendant;
2242
+ childConfigurable[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY] = resumeAttemptId;
2243
+ }
2244
+ childConfigurable.thread_id =
2245
+ this.humanInTheLoop?.enabled === true
2246
+ ? childThreadId
2247
+ : (inheritedConfigurable.thread_id ?? childRunId);
2248
+ const childInvokeConfig = {
2249
+ recursionLimit,
2250
+ signal: childSignal,
2251
+ callbacks,
2252
+ runName: `subagent:${subagentType}`,
2253
+ configurable: childConfigurable,
2254
+ };
2255
+ activeChildRun.invokeConfig = childInvokeConfig;
2256
+ if (cachedChildRun == null && this.humanInTheLoop?.enabled === true) {
2257
+ /** Rehydrate child-owned interrupt state when a host rebuilds Run
2258
+ * around the same durable checkpointer after a process boundary. */
2259
+ const persistedState = await workflow.getState(childInvokeConfig);
2260
+ const checkpointMessages = getCheckpointMessages(
2261
+ (persistedState.values as { messages?: unknown } | undefined)
2262
+ ?.messages
2263
+ );
2264
+ const persistedHookSessionId =
2265
+ getSubagentHookSessionId(checkpointMessages);
2266
+ if (persistedHookSessionId != null) {
2267
+ this.hookRegistry?.copySession(
2268
+ persistedHookSessionId,
2269
+ currentHookSessionId
2270
+ );
591
2271
  }
592
- );
2272
+ const persistedInterrupts = getPersistedInterrupts(persistedState);
2273
+ if (persistedInterrupts.length > 0) {
2274
+ activeChildRun.pendingInterrupts = persistedInterrupts;
2275
+ execution.markStarted();
2276
+ childAlreadyStarted = true;
2277
+ } else if (persistedState.next.length > 0) {
2278
+ recoveredInProgress = true;
2279
+ childAlreadyStarted = true;
2280
+ execution.markStarted();
2281
+ } else if (persistedState.next.length === 0) {
2282
+ const persistedMessages = getPersistedMessages(persistedState);
2283
+ if (persistedMessages != null) {
2284
+ const marker = getSubagentCheckpointMarker(
2285
+ checkpointMessages,
2286
+ executionSuffix
2287
+ );
2288
+ result = { messages: persistedMessages };
2289
+ recoveredComplete = true;
2290
+ childAlreadyStarted = true;
2291
+ childAlreadyCompleted = marker?.lifecycleComplete === true;
2292
+ execution.markStarted();
2293
+ }
2294
+ }
2295
+ }
2296
+ if (!recoveredComplete) {
2297
+ const childResumeMap = getChildResumeMap(
2298
+ activeChildRun.pendingInterrupts,
2299
+ params.parentConfigurable
2300
+ );
2301
+ if (
2302
+ activeChildRun.pendingInterrupts.length > 0 &&
2303
+ childResumeMap == null
2304
+ ) {
2305
+ throw new GraphInterrupt(activeChildRun.pendingInterrupts);
2306
+ }
2307
+ let childInput: BaseGraphState | Command | null;
2308
+ if (childResumeMap != null) {
2309
+ childInput = new Command({ resume: childResumeMap });
2310
+ } else if (recoveredInProgress) {
2311
+ childInput = null;
2312
+ } else {
2313
+ childInput = {
2314
+ messages: [
2315
+ new HumanMessage({
2316
+ content: description,
2317
+ additional_kwargs: {
2318
+ [SUBAGENT_HOOK_SESSION_KEY]: currentHookSessionId,
2319
+ [SUBAGENT_RUN_ID_KEY]: childRunId,
2320
+ },
2321
+ }),
2322
+ ],
2323
+ };
2324
+ }
2325
+
2326
+ if (
2327
+ !childAlreadyStarted &&
2328
+ this.hookRegistry?.hasHookFor(
2329
+ 'SubagentStart',
2330
+ currentHookSessionId
2331
+ ) === true
2332
+ ) {
2333
+ const hookResult = await executeHooks({
2334
+ registry: this.hookRegistry,
2335
+ input: {
2336
+ hook_event_name: 'SubagentStart',
2337
+ runId: currentHookSessionId,
2338
+ threadId,
2339
+ parentAgentId: this.parentAgentId,
2340
+ agentId: childAgentId,
2341
+ agentType: subagentType,
2342
+ inputs: [new HumanMessage(description)],
2343
+ },
2344
+ sessionId: currentHookSessionId,
2345
+ matchQuery: subagentType,
2346
+ }).catch((): AggregatedHookResult => HOOK_FALLBACK);
2347
+
2348
+ if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
2349
+ this.clearChildGraph(childGraph);
2350
+ execution.releaseActiveRun();
2351
+ this.executions.remove(execution);
2352
+ return {
2353
+ content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
2354
+ messages: [],
2355
+ };
2356
+ }
2357
+ }
2358
+ execution.markStarted();
2359
+
2360
+ if (forwarder && !childAlreadyStarted) {
2361
+ await this.emitSubagentUpdate(parentRegistry!, {
2362
+ childRunId,
2363
+ subagentType,
2364
+ subagentKind: childPlan.kind,
2365
+ subagentAgentId: childAgentId,
2366
+ parentToolCallId,
2367
+ executionContext: childExecutionContext,
2368
+ phase: 'start',
2369
+ label: `Subagent "${subagentType}" started`,
2370
+ });
2371
+ }
2372
+
2373
+ let childResult: MultiAgentGraphState;
2374
+ if (this.humanInTheLoop?.enabled === true) {
2375
+ /** Execute as an independently checkpointed root instead of inheriting
2376
+ * the parent's Pregel namespace. Parent decisions are routed explicitly
2377
+ * by interrupt id, so concurrent children keep isolated resume state. */
2378
+ childResult = await AsyncLocalStorageProviderSingleton.runWithConfig(
2379
+ childInvokeConfig,
2380
+ (): Promise<MultiAgentGraphState> =>
2381
+ workflow.invoke(childInput, childInvokeConfig)
2382
+ );
2383
+ } else {
2384
+ childResult = await workflow.invoke(childInput, childInvokeConfig);
2385
+ }
2386
+ const childInterrupts = isInterrupted(childResult)
2387
+ ? childResult[INTERRUPT]
2388
+ : undefined;
2389
+ if (childInterrupts != null && childInterrupts.length > 0) {
2390
+ throw new GraphInterrupt(childInterrupts);
2391
+ }
2392
+ result = childResult;
2393
+ }
593
2394
  } catch (error) {
2395
+ if (isGraphInterrupt(error)) {
2396
+ const activeChildRun = execution.activeRun;
2397
+ if (activeChildRun != null) {
2398
+ activeChildRun.pendingInterrupts = error.interrupts;
2399
+ execution.markInterrupted();
2400
+ }
2401
+ const resumeManifest =
2402
+ activeChildRun == null || parentToolCallId == null
2403
+ ? undefined
2404
+ : await this.createResumeManifest([execution]);
2405
+ await forwarding?.drain();
2406
+ throw new GraphInterrupt(
2407
+ addSubagentScope(
2408
+ error.interrupts,
2409
+ {
2410
+ run_id: childRunId,
2411
+ agent_id: childAgentId,
2412
+ subagent_type: subagentType,
2413
+ parent_tool_call_id: parentToolCallId,
2414
+ },
2415
+ resumeManifest
2416
+ )
2417
+ );
2418
+ }
594
2419
  /** Aborted before any observational work below: parallel siblings — in
595
2420
  * this executor and, via the graph-scoped breaker, under other
596
2421
  * parallel agent nodes — stream on the composed child signal, and
@@ -607,14 +2432,17 @@ export class SubagentExecutor {
607
2432
  await this.emitSubagentUpdate(parentRegistry!, {
608
2433
  childRunId,
609
2434
  subagentType,
2435
+ subagentKind: childPlan.kind,
610
2436
  subagentAgentId: childAgentId,
611
2437
  parentToolCallId,
2438
+ executionContext: childExecutionContext,
612
2439
  phase: 'error',
613
2440
  label: `Subagent "${subagentType}" errored: ${errorMessage}`,
614
2441
  data: { message: errorMessage },
615
2442
  });
616
2443
  }
617
- childGraph.clearHeavyState();
2444
+ this.clearChildGraph(childGraph);
2445
+ execution.markFailed();
618
2446
  /**
619
2447
  * A tripped stream circuit breaker is a safety abort, not a recoverable
620
2448
  * subagent failure: converting it into a tool result would let the
@@ -631,10 +2459,18 @@ export class SubagentExecutor {
631
2459
  };
632
2460
  }
633
2461
 
634
- const filteredContent = filterSubagentResult(result.messages);
2462
+ if (result == null) {
2463
+ throw new Error('Subagent completed without producing graph state.');
2464
+ }
2465
+ const filteredContent =
2466
+ childPlan.kind === 'graph'
2467
+ ? filterGraphSubagentResult(result, childPlan.resultAgentId)
2468
+ : filterSubagentResult(result.messages);
635
2469
 
636
2470
  if (
637
- this.hookRegistry?.hasHookFor('SubagentStop', this.parentRunId) === true
2471
+ !childAlreadyCompleted &&
2472
+ this.hookRegistry?.hasHookFor('SubagentStop', currentHookSessionId) ===
2473
+ true
638
2474
  ) {
639
2475
  /**
640
2476
  * Awaited (not fire-and-forget) for deterministic test synchronization
@@ -646,34 +2482,50 @@ export class SubagentExecutor {
646
2482
  registry: this.hookRegistry,
647
2483
  input: {
648
2484
  hook_event_name: 'SubagentStop',
649
- runId: this.parentRunId,
2485
+ runId: currentHookSessionId,
650
2486
  threadId,
651
2487
  agentId: childAgentId,
652
2488
  agentType: subagentType,
653
2489
  messages: result.messages,
654
2490
  },
655
- sessionId: this.parentRunId,
2491
+ sessionId: currentHookSessionId,
656
2492
  matchQuery: subagentType,
657
2493
  }).catch(() => {
658
2494
  /* SubagentStop is observational — swallow errors */
659
2495
  });
660
2496
  }
661
2497
 
662
- if (forwarding) {
2498
+ if (forwarding && !childAlreadyCompleted) {
663
2499
  await forwarding.drain();
664
2500
  await this.emitSubagentUpdate(parentRegistry!, {
665
2501
  childRunId,
666
2502
  subagentType,
2503
+ subagentKind: childPlan.kind,
667
2504
  subagentAgentId: childAgentId,
668
2505
  parentToolCallId,
2506
+ executionContext: childExecutionContext,
669
2507
  phase: 'stop',
670
2508
  label: `Subagent "${subagentType}" finished`,
671
2509
  });
672
2510
  }
2511
+ if (!childAlreadyCompleted) {
2512
+ const activeChildRun = execution.activeRun;
2513
+ if (activeChildRun != null && parentToolCallId != null) {
2514
+ await this.persistChildCheckpointMarker(
2515
+ activeChildRun,
2516
+ parentToolCallId
2517
+ );
2518
+ }
2519
+ }
673
2520
 
674
- childGraph.clearHeavyState();
2521
+ this.clearChildGraph(childGraph);
675
2522
 
676
- return { content: filteredContent, messages: result.messages };
2523
+ const completedResult = {
2524
+ content: filteredContent,
2525
+ messages: result.messages,
2526
+ };
2527
+ execution.markCompleted(completedResult);
2528
+ return completedResult;
677
2529
  }
678
2530
 
679
2531
  /**
@@ -686,8 +2538,11 @@ export class SubagentExecutor {
686
2538
  args: {
687
2539
  childRunId: string;
688
2540
  subagentType: string;
2541
+ subagentKind: 'agent' | 'graph';
689
2542
  subagentAgentId: string;
2543
+ memberAgentId?: string;
690
2544
  parentToolCallId?: string;
2545
+ executionContext: SubagentExecutionContext;
691
2546
  phase: SubagentUpdatePhase;
692
2547
  data?: unknown;
693
2548
  label?: string;
@@ -698,22 +2553,23 @@ export class SubagentExecutor {
698
2553
  return;
699
2554
  }
700
2555
  const event: SubagentUpdateEvent = {
701
- runId: this.parentRunId,
2556
+ runId: args.executionContext.rootRunId,
2557
+ parentRunId: this.parentRunId,
702
2558
  subagentRunId: args.childRunId,
703
2559
  subagentType: args.subagentType,
2560
+ subagentKind: args.subagentKind,
704
2561
  subagentAgentId: args.subagentAgentId,
2562
+ memberAgentId: args.memberAgentId,
705
2563
  parentAgentId: this.parentAgentId,
706
2564
  parentToolCallId: args.parentToolCallId,
2565
+ depth: args.executionContext.depth,
2566
+ ancestry: args.executionContext.ancestry,
707
2567
  phase: args.phase,
708
2568
  data: args.data,
709
2569
  label: args.label,
710
2570
  timestamp: new Date().toISOString(),
711
2571
  };
712
- try {
713
- await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
714
- } catch {
715
- /* observational — swallow */
716
- }
2572
+ await dispatchObservationalSubagentUpdate(handler, event);
717
2573
  }
718
2574
 
719
2575
  /**
@@ -731,60 +2587,72 @@ export class SubagentExecutor {
731
2587
  private createForwarderCallback(args: {
732
2588
  parentRegistry: HandlerRegistry;
733
2589
  subagentType: string;
2590
+ subagentKind: 'agent' | 'graph';
734
2591
  subagentAgentId: string;
735
2592
  childRunId: string;
736
2593
  parentToolCallId?: string;
2594
+ executionContext: SubagentExecutionContext;
737
2595
  }): ForwarderCallback {
738
2596
  const {
739
2597
  parentRegistry,
740
2598
  subagentType,
2599
+ subagentKind,
741
2600
  subagentAgentId,
742
2601
  childRunId,
743
2602
  parentToolCallId,
2603
+ executionContext,
744
2604
  } = args;
745
- const parentRunId = this.parentRunId;
2605
+ const immediateParentRunId = this.parentRunId;
746
2606
  const parentAgentId = this.parentAgentId;
747
2607
 
748
2608
  const wrap = async (
749
2609
  eventName: string,
750
2610
  phase: SubagentUpdatePhase,
751
- data: unknown
752
- ): Promise<void> => {
2611
+ data: unknown,
2612
+ memberAgentId?: string
2613
+ ): Promise<boolean> => {
753
2614
  const handler = parentRegistry.getHandler(GraphEvents.ON_SUBAGENT_UPDATE);
754
2615
  if (!handler) {
755
- return;
756
- }
757
- try {
758
- const event: SubagentUpdateEvent = {
759
- runId: parentRunId,
760
- subagentRunId: childRunId,
761
- subagentType,
762
- subagentAgentId,
763
- parentAgentId,
764
- parentToolCallId,
765
- phase,
766
- data: sanitizeForwardedSubagentUpdateData(eventName, data),
767
- label: summarizeEvent(eventName, data),
768
- timestamp: new Date().toISOString(),
769
- };
770
- await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
771
- } catch {
772
- /* observational — swallow */
2616
+ return true;
773
2617
  }
2618
+ const event: SubagentUpdateEvent = {
2619
+ runId: executionContext.rootRunId,
2620
+ parentRunId: immediateParentRunId,
2621
+ subagentRunId: childRunId,
2622
+ subagentType,
2623
+ subagentKind,
2624
+ subagentAgentId,
2625
+ memberAgentId,
2626
+ parentAgentId,
2627
+ parentToolCallId,
2628
+ depth: executionContext.depth,
2629
+ ancestry: executionContext.ancestry,
2630
+ phase,
2631
+ data: sanitizeForwardedSubagentUpdateData(eventName, data),
2632
+ label: summarizeEvent(eventName, data),
2633
+ timestamp: new Date().toISOString(),
2634
+ };
2635
+ return dispatchObservationalSubagentUpdate(handler, event);
774
2636
  };
775
2637
 
776
2638
  const queuedUpdates: QueuedSubagentUpdate[] = [];
777
2639
  let drainPromise: Promise<void> | undefined;
2640
+ let queueOpen = true;
778
2641
 
779
2642
  const enqueue = (update: QueuedSubagentUpdate): void => {
780
- if (queuedUpdates.length >= MAX_PENDING_SUBAGENT_UPDATES) {
2643
+ if (!queueOpen) {
2644
+ return;
2645
+ }
2646
+ if (queuedUpdates.length >= MAX_QUEUED_SUBAGENT_UPDATES) {
781
2647
  const dropIndex = queuedUpdates.findIndex((queued) =>
782
- isDroppableSubagentUpdatePhase(queued.phase)
2648
+ isLowPrioritySubagentUpdatePhase(queued.phase)
783
2649
  );
784
2650
  if (dropIndex >= 0) {
785
2651
  queuedUpdates.splice(dropIndex, 1);
786
- } else if (isDroppableSubagentUpdatePhase(update.phase)) {
2652
+ } else if (isLowPrioritySubagentUpdatePhase(update.phase)) {
787
2653
  return;
2654
+ } else {
2655
+ queuedUpdates.shift();
788
2656
  }
789
2657
  }
790
2658
  queuedUpdates.push(update);
@@ -801,7 +2669,16 @@ export class SubagentExecutor {
801
2669
  if (update == null) {
802
2670
  continue;
803
2671
  }
804
- await wrap(update.eventName, update.phase, update.data);
2672
+ const handlerSettled = await wrap(
2673
+ update.eventName,
2674
+ update.phase,
2675
+ update.data,
2676
+ update.memberAgentId
2677
+ );
2678
+ if (!handlerSettled) {
2679
+ queueOpen = false;
2680
+ queuedUpdates.length = 0;
2681
+ }
805
2682
  }
806
2683
  })();
807
2684
  try {
@@ -817,17 +2694,23 @@ export class SubagentExecutor {
817
2694
  const scheduleWrap = (
818
2695
  eventName: string,
819
2696
  phase: SubagentUpdatePhase,
820
- data: unknown
2697
+ data: unknown,
2698
+ memberAgentId?: string
821
2699
  ): void => {
822
- enqueue({ eventName, phase, data });
2700
+ enqueue({ eventName, phase, data, memberAgentId });
823
2701
  void drain();
824
2702
  };
825
2703
 
826
2704
  const handler = BaseCallbackHandler.fromMethods({
827
2705
  [Callback.CUSTOM_EVENT]: async (
828
2706
  eventName: string,
829
- data: unknown
2707
+ data: unknown,
2708
+ _runId?: string,
2709
+ _tags?: string[],
2710
+ metadata?: Record<string, unknown>
830
2711
  ): Promise<void> => {
2712
+ const memberAgentId =
2713
+ asNonEmptyString(metadata?.agentId) ?? getEventAgentId(data);
831
2714
  if (eventName === GraphEvents.ON_TOOL_EXECUTE) {
832
2715
  const toolHandler = parentRegistry.getHandler(
833
2716
  GraphEvents.ON_TOOL_EXECUTE
@@ -842,28 +2725,28 @@ export class SubagentExecutor {
842
2725
  * We also surface a short notice in the subagent-update stream so
843
2726
  * the UI can show "calling <tool>" for each tool the child spawns.
844
2727
  */
845
- scheduleWrap(eventName, 'run_step', data);
2728
+ scheduleWrap(eventName, 'run_step', data, memberAgentId);
846
2729
  return;
847
2730
  }
848
2731
 
849
2732
  if (eventName === GraphEvents.ON_RUN_STEP) {
850
- scheduleWrap(eventName, 'run_step', data);
2733
+ scheduleWrap(eventName, 'run_step', data, memberAgentId);
851
2734
  return;
852
2735
  }
853
2736
  if (eventName === GraphEvents.ON_RUN_STEP_DELTA) {
854
- scheduleWrap(eventName, 'run_step_delta', data);
2737
+ scheduleWrap(eventName, 'run_step_delta', data, memberAgentId);
855
2738
  return;
856
2739
  }
857
2740
  if (eventName === GraphEvents.ON_RUN_STEP_COMPLETED) {
858
- scheduleWrap(eventName, 'run_step_completed', data);
2741
+ scheduleWrap(eventName, 'run_step_completed', data, memberAgentId);
859
2742
  return;
860
2743
  }
861
2744
  if (eventName === GraphEvents.ON_MESSAGE_DELTA) {
862
- scheduleWrap(eventName, 'message_delta', data);
2745
+ scheduleWrap(eventName, 'message_delta', data, memberAgentId);
863
2746
  return;
864
2747
  }
865
2748
  if (eventName === GraphEvents.ON_REASONING_DELTA) {
866
- scheduleWrap(eventName, 'reasoning_delta', data);
2749
+ scheduleWrap(eventName, 'reasoning_delta', data, memberAgentId);
867
2750
  return;
868
2751
  }
869
2752
  },
@@ -898,29 +2781,25 @@ export class SubagentExecutor {
898
2781
  function createUsageCaptureHandler(args: {
899
2782
  sink: SubagentUsageSink;
900
2783
  subagentType: string;
2784
+ subagentKind: 'agent' | 'graph';
901
2785
  subagentRunId: string;
902
2786
  subagentAgentId: string;
903
2787
  parentRunId: string;
904
- /**
905
- * Child config's provider enum — the default tag when a call carries no
906
- * `INVOKED_PROVIDER` metadata (hosts key pricing/cache semantics off it).
907
- */
908
- provider?: string;
909
- /**
910
- * Child config's model, used when a call carries neither `ls_model_name`
911
- * nor `INVOKED_MODEL` metadata.
912
- */
913
- fallbackModel?: string;
2788
+ executionContext: SubagentExecutionContext;
2789
+ memberInputs: ReadonlyMap<string, AgentInputs>;
914
2790
  }): BaseCallbackHandler {
915
2791
  const {
916
2792
  sink,
917
2793
  subagentType,
2794
+ subagentKind,
918
2795
  subagentRunId,
919
2796
  subagentAgentId,
920
2797
  parentRunId,
921
- provider,
922
- fallbackModel,
2798
+ executionContext,
2799
+ memberInputs,
923
2800
  } = args;
2801
+ const defaultMember =
2802
+ memberInputs.size === 1 ? memberInputs.entries().next().value : undefined;
924
2803
  /**
925
2804
  * Per-call attribution keyed by LangChain callback runId. `model` joins
926
2805
  * `ls_model_name` (provider-reported) with `INVOKED_MODEL` (stamped by
@@ -932,7 +2811,7 @@ function createUsageCaptureHandler(args: {
932
2811
  */
933
2812
  const callInfoByCallId = new Map<
934
2813
  string,
935
- { model?: string; provider?: string }
2814
+ { model?: string; provider?: string; memberAgentId?: string }
936
2815
  >();
937
2816
  const handler = BaseCallbackHandler.fromMethods({
938
2817
  handleChatModelStart: (
@@ -950,18 +2829,30 @@ function createUsageCaptureHandler(args: {
950
2829
  const callProvider = asNonEmptyString(
951
2830
  metadata?.[Constants.INVOKED_PROVIDER]
952
2831
  );
953
- if (callModel != null || callProvider != null) {
2832
+ const memberAgentId = asNonEmptyString(metadata?.agentId);
2833
+ if (callModel != null || callProvider != null || memberAgentId != null) {
954
2834
  callInfoByCallId.set(runId, {
955
2835
  model: callModel,
956
2836
  provider: callProvider,
2837
+ memberAgentId,
957
2838
  });
958
2839
  }
959
2840
  },
960
2841
  handleLLMEnd: async (output: LLMResult, runId: string): Promise<void> => {
961
2842
  const callInfo = callInfoByCallId.get(runId);
962
2843
  callInfoByCallId.delete(runId);
963
- const model = callInfo?.model ?? fallbackModel;
964
- const callProvider = callInfo?.provider ?? provider;
2844
+ const observedMemberAgentId = callInfo?.memberAgentId;
2845
+ const memberAgentId =
2846
+ observedMemberAgentId != null && memberInputs.has(observedMemberAgentId)
2847
+ ? observedMemberAgentId
2848
+ : defaultMember?.[0];
2849
+ const memberInput =
2850
+ (memberAgentId == null ? undefined : memberInputs.get(memberAgentId)) ??
2851
+ defaultMember?.[1];
2852
+ const model =
2853
+ callInfo?.model ??
2854
+ (memberInput == null ? undefined : extractConfiguredModel(memberInput));
2855
+ const callProvider = callInfo?.provider ?? memberInput?.provider;
965
2856
  for (const generationGroup of output.generations) {
966
2857
  /**
967
2858
  * At most ONE event per generation group: each group is one
@@ -991,9 +2882,14 @@ function createUsageCaptureHandler(args: {
991
2882
  model,
992
2883
  provider: callProvider,
993
2884
  subagentType,
2885
+ subagentKind,
994
2886
  subagentRunId,
995
2887
  subagentAgentId,
996
- runId: parentRunId,
2888
+ memberAgentId,
2889
+ parentRunId,
2890
+ depth: executionContext.depth,
2891
+ ancestry: executionContext.ancestry,
2892
+ runId: executionContext.rootRunId,
997
2893
  });
998
2894
  } catch {
999
2895
  /* observational — a throwing/rejecting host sink must not break the child run */
@@ -1019,6 +2915,19 @@ function asNonEmptyString(value: unknown): string | undefined {
1019
2915
  return typeof value === 'string' && value !== '' ? value : undefined;
1020
2916
  }
1021
2917
 
2918
+ function getEventAgentId(data: unknown): string | undefined {
2919
+ if (data == null || typeof data !== 'object') {
2920
+ return undefined;
2921
+ }
2922
+ const event = data as {
2923
+ agentId?: unknown;
2924
+ result?: { agentId?: unknown };
2925
+ };
2926
+ return (
2927
+ asNonEmptyString(event.agentId) ?? asNonEmptyString(event.result?.agentId)
2928
+ );
2929
+ }
2930
+
1022
2931
  /**
1023
2932
  * Best-effort read of the configured model from a subagent's client
1024
2933
  * options. Providers disagree on the key (`model` vs `modelName`), and the
@@ -1040,6 +2949,63 @@ function extractConfiguredModel(agentInputs: AgentInputs): string | undefined {
1040
2949
  return undefined;
1041
2950
  }
1042
2951
 
2952
+ function sanitizeResolverConfigurable(
2953
+ parentConfigurable: Record<string, unknown>
2954
+ ): SubagentResolveConfigurable | undefined {
2955
+ const configurable = sanitizeChildConfigurable(parentConfigurable);
2956
+ const requestBody = createResolveRequestContext(configurable.requestBody);
2957
+ const user = createResolveUserContext(configurable.user);
2958
+ const userId = asNonEmptyString(configurable.user_id) ?? user?.id;
2959
+ if (requestBody == null && user == null && userId == null) {
2960
+ return undefined;
2961
+ }
2962
+ return {
2963
+ ...(requestBody == null ? {} : { requestBody }),
2964
+ ...(user == null ? {} : { user }),
2965
+ ...(userId == null ? {} : { user_id: userId }),
2966
+ };
2967
+ }
2968
+
2969
+ function createResolveRequestContext(
2970
+ value: unknown
2971
+ ): SubagentResolveRequestContext | undefined {
2972
+ if (!isObjectLike(value)) {
2973
+ return undefined;
2974
+ }
2975
+ const source = value as SubagentResolveRequestContext;
2976
+ const conversationId = asNonEmptyString(source.conversationId);
2977
+ const messageId = asNonEmptyString(source.messageId);
2978
+ const parentMessageId = asNonEmptyString(source.parentMessageId);
2979
+ if (conversationId == null && messageId == null && parentMessageId == null) {
2980
+ return undefined;
2981
+ }
2982
+ return {
2983
+ ...(conversationId == null ? {} : { conversationId }),
2984
+ ...(messageId == null ? {} : { messageId }),
2985
+ ...(parentMessageId == null ? {} : { parentMessageId }),
2986
+ };
2987
+ }
2988
+
2989
+ function createResolveUserContext(
2990
+ value: unknown
2991
+ ): SubagentResolveUserContext | undefined {
2992
+ if (!isObjectLike(value)) {
2993
+ return undefined;
2994
+ }
2995
+ const source = value as SubagentResolveUserContext;
2996
+ const id = asNonEmptyString(source.id);
2997
+ const role = asNonEmptyString(source.role);
2998
+ const tenantId = asNonEmptyString(source.tenantId);
2999
+ if (id == null && role == null && tenantId == null) {
3000
+ return undefined;
3001
+ }
3002
+ return {
3003
+ ...(id == null ? {} : { id }),
3004
+ ...(role == null ? {} : { role }),
3005
+ ...(tenantId == null ? {} : { tenantId }),
3006
+ };
3007
+ }
3008
+
1043
3009
  function sanitizeChildConfigurable(
1044
3010
  parentConfigurable: Record<string, unknown> | undefined
1045
3011
  ): Record<string, unknown> {
@@ -1057,6 +3023,10 @@ function isLangGraphRuntimeConfigKey(key: string): boolean {
1057
3023
  return (
1058
3024
  key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) ||
1059
3025
  LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key) ||
3026
+ key === SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY ||
3027
+ key === SUBAGENT_RESUME_MANIFEST_CONFIG_KEY ||
3028
+ key === SUBAGENT_PARENT_BATCH_CONFIG_KEY ||
3029
+ key === TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY ||
1060
3030
  /** The parent batch's breaker scope must not leak into the child
1061
3031
  * workflow's configurable — children own separate controllers. */
1062
3032
  key === RUN_BREAKER_SCOPE_CONFIG_KEY
@@ -1088,7 +3058,7 @@ export function sanitizeForwardedSubagentUpdateData(
1088
3058
  return undefined;
1089
3059
  }
1090
3060
 
1091
- function isDroppableSubagentUpdatePhase(phase: SubagentUpdatePhase): boolean {
3061
+ function isLowPrioritySubagentUpdatePhase(phase: SubagentUpdatePhase): boolean {
1092
3062
  return (
1093
3063
  phase === 'message_delta' ||
1094
3064
  phase === 'reasoning_delta' ||
@@ -1404,6 +3374,24 @@ export function summarizeEvent(eventName: string, data: unknown): string {
1404
3374
  return eventName;
1405
3375
  }
1406
3376
 
3377
+ /**
3378
+ * Reads only the result member captured by MultiAgentGraph. Worker output is
3379
+ * never used as a fallback when the designated result member is textless.
3380
+ */
3381
+ export function filterGraphSubagentResult(
3382
+ state: MultiAgentGraphState,
3383
+ resultAgentId: string
3384
+ ): string {
3385
+ const result = state.subagentResult;
3386
+ if (result == null || result.agentId !== resultAgentId) {
3387
+ return `Error: Graph subagent result agent "${resultAgentId}" did not produce a result.`;
3388
+ }
3389
+ if (result.message == null) {
3390
+ return 'Task completed';
3391
+ }
3392
+ return filterSubagentResult([result.message]);
3393
+ }
3394
+
1407
3395
  /**
1408
3396
  * Walk messages from last to first, returning the text content of the most
1409
3397
  * recent AIMessage that has any. Non-text blocks (tool_use, thinking,
@@ -1502,114 +3490,46 @@ export function filterSubagentResult(messages: BaseMessage[]): string {
1502
3490
  return 'Task completed';
1503
3491
  }
1504
3492
 
1505
- /**
1506
- * Resolve self-spawn configs by filling in agentInputs from the parent context.
1507
- * Returns configs with agentInputs guaranteed present. Throws on duplicate
1508
- * `type` values to prevent silent config shadowing.
1509
- */
1510
- export function resolveSubagentConfigs(
1511
- configs: SubagentConfig[],
1512
- parentContext: AgentContext
1513
- ): ResolvedSubagentConfig[] {
1514
- const resolved = configs
1515
- .map((config) => {
1516
- if (config.agentInputs != null) {
1517
- return config as ResolvedSubagentConfig;
1518
- }
1519
- if (config.self !== true || parentContext._sourceInputs == null) {
1520
- return null;
1521
- }
1522
- return {
1523
- ...config,
1524
- agentInputs: { ...parentContext._sourceInputs },
1525
- } as ResolvedSubagentConfig;
1526
- })
1527
- .filter((c): c is ResolvedSubagentConfig => c != null);
1528
-
1529
- const seenTypes = new Set<string>();
1530
- for (const config of resolved) {
1531
- if (seenTypes.has(config.type)) {
1532
- throw new Error(
1533
- `Duplicate subagent type "${config.type}". Each SubagentConfig must have a unique "type" field.`
1534
- );
1535
- }
1536
- seenTypes.add(config.type);
3493
+ function truncateErrorMessage(error: unknown): string {
3494
+ const message = error instanceof Error ? error.message : String(error);
3495
+ if (message.length <= ERROR_MESSAGE_MAX_CHARS) {
3496
+ return message;
1537
3497
  }
1538
-
1539
- return resolved;
3498
+ return `${message.slice(0, ERROR_MESSAGE_MAX_CHARS)}...`;
1540
3499
  }
1541
3500
 
1542
- /**
1543
- * Build child AgentInputs from a resolved config, stripping nesting and
1544
- * (optionally) event-driven fields. When `allowNested: true`, the child's
1545
- * `maxSubagentDepth` is decremented so that depth is consumed as the call
1546
- * chain deepens across graph boundaries — the parent's executor-level check
1547
- * alone cannot see into the child graph's separate executor.
1548
- *
1549
- * When `keepToolDefinitions` is `true`, the child retains the parent's
1550
- * `toolDefinitions` so event-driven tools remain usable. This is only safe
1551
- * when the caller has wired a forwarder for `ON_TOOL_EXECUTE` to a
1552
- * registered handler — otherwise the child will hang on tool dispatch.
1553
- *
1554
- * @remarks Advanced utility: exported primarily for testing and by
1555
- * {@link SubagentExecutor}. Host applications configuring subagents should
1556
- * not need to call this directly — it is invoked internally when a subagent
1557
- * tool is dispatched. The depth-countdown contract (parent's `maxDepth` in,
1558
- * child's decremented `maxSubagentDepth` on the returned inputs) is the
1559
- * mechanism that bounds nesting across graph boundaries; callers must
1560
- * respect it.
1561
- */
1562
- export function buildChildInputs(
1563
- config: ResolvedSubagentConfig,
1564
- childAgentId: string,
1565
- parentMaxDepth: number,
1566
- keepToolDefinitions: boolean = false
1567
- ): AgentInputs {
1568
- const { agentInputs } = config;
1569
- const childInputs: AgentInputs = {
1570
- ...agentInputs,
1571
- agentId: childAgentId,
1572
- toolDefinitions: keepToolDefinitions
1573
- ? agentInputs.toolDefinitions
1574
- : undefined,
1575
- /**
1576
- * Subagents run in an isolated context by contract. Parent-run-scoped
1577
- * fields that would otherwise survive the shallow-spread clone — the
1578
- * cross-run conversation summary and the prior-turn tool-discovery
1579
- * set — are cleared here so the child starts fresh. Host applications
1580
- * that want a subagent to see parent context must thread it in
1581
- * explicitly (e.g. via the `description` argument to the subagent
1582
- * tool), not via inherited state.
1583
- */
1584
- initialSummary: undefined,
1585
- discoveredTools: undefined,
1586
- /**
1587
- * Host-supplied direct tools are scrubbed from INHERITED configs only.
1588
- * A self-spawn config's `agentInputs` is a shallow spread of the parent's
1589
- * `_sourceInputs`, so without this a parent-scoped graph tool (e.g. an
1590
- * interrupt-raising ask_user_question, which needs the parent's
1591
- * checkpointer — child graphs compile without one) would silently leak
1592
- * into the child and deterministically throw `No checkpointer set`. An
1593
- * EXPLICIT child config that lists its own `graphTools` is a deliberate
1594
- * host choice and keeps them (Codex #289 P2).
1595
- */
1596
- graphTools: config.self === true ? undefined : agentInputs.graphTools,
1597
- };
1598
-
1599
- if (config.allowNested === true) {
1600
- childInputs.maxSubagentDepth = Math.max(0, parentMaxDepth - 1);
1601
- } else {
1602
- childInputs.subagentConfigs = undefined;
1603
- childInputs.maxSubagentDepth = undefined;
3501
+ function resolveAgentInputsWithSignal(
3502
+ resolve: () => Promise<AgentInputs>,
3503
+ signal: AbortSignal
3504
+ ): Promise<AgentInputs> {
3505
+ if (signal.aborted) {
3506
+ return Promise.reject(getAbortReason(signal));
1604
3507
  }
1605
3508
 
1606
- return childInputs;
3509
+ return new Promise<AgentInputs>((resolvePromise, rejectPromise) => {
3510
+ const cleanup = (): void => signal.removeEventListener('abort', onAbort);
3511
+ const onAbort = (): void => {
3512
+ cleanup();
3513
+ rejectPromise(getAbortReason(signal));
3514
+ };
3515
+ signal.addEventListener('abort', onAbort, { once: true });
3516
+ Promise.resolve()
3517
+ .then(resolve)
3518
+ .then(
3519
+ (agentInputs) => {
3520
+ cleanup();
3521
+ resolvePromise(agentInputs);
3522
+ },
3523
+ (error: unknown) => {
3524
+ cleanup();
3525
+ rejectPromise(error);
3526
+ }
3527
+ );
3528
+ });
1607
3529
  }
1608
3530
 
1609
- function truncateErrorMessage(error: unknown): string {
1610
- const message = error instanceof Error ? error.message : String(error);
1611
- if (message.length <= ERROR_MESSAGE_MAX_CHARS) {
1612
- return message;
1613
- }
1614
- return `${message.slice(0, ERROR_MESSAGE_MAX_CHARS)}...`;
3531
+ function getAbortReason(signal: AbortSignal): Error {
3532
+ return signal.reason instanceof Error
3533
+ ? signal.reason
3534
+ : new Error('Subagent resolution aborted.');
1615
3535
  }