@librechat/agents 3.4.1 → 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 (128) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +57 -33
  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/createWorkspacePolicyHook.cjs +1 -1
  12. package/dist/cjs/main.cjs +20 -11
  13. package/dist/cjs/run.cjs +48 -38
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  16. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  17. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  18. package/dist/cjs/tools/ToolNode.cjs +13 -9
  19. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  20. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  21. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  22. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  23. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  24. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  25. package/dist/cjs/tools/search/tool.cjs +4 -1
  26. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  27. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  28. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +672 -343
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs +3 -1
  32. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  33. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  34. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  35. package/dist/cjs/tools/subagent/index.cjs +1 -0
  36. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  37. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  38. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  39. package/dist/esm/graphs/Graph.mjs +57 -33
  40. package/dist/esm/graphs/Graph.mjs.map +1 -1
  41. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  42. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  43. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  44. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  45. package/dist/esm/graphs/createGraph.mjs +15 -0
  46. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  47. package/dist/esm/graphs/index.mjs +1 -0
  48. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  49. package/dist/esm/main.mjs +12 -10
  50. package/dist/esm/run.mjs +48 -38
  51. package/dist/esm/run.mjs.map +1 -1
  52. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  53. package/dist/esm/tools/SubagentTool.mjs +4 -4
  54. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  55. package/dist/esm/tools/ToolNode.mjs +13 -9
  56. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  57. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  58. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  59. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  60. package/dist/esm/tools/search/rerankers.mjs +154 -1
  61. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  62. package/dist/esm/tools/search/tool.mjs +4 -1
  63. package/dist/esm/tools/search/tool.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  65. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  66. package/dist/esm/tools/subagent/SubagentExecutor.mjs +672 -343
  67. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  68. package/dist/esm/tools/subagent/SubagentReplay.mjs +3 -1
  69. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  70. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  71. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  72. package/dist/esm/tools/subagent/index.mjs +1 -0
  73. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  74. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  75. package/dist/types/agents/AgentContext.d.ts +1 -1
  76. package/dist/types/graphs/Graph.d.ts +7 -2
  77. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  78. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  79. package/dist/types/graphs/createGraph.d.ts +13 -0
  80. package/dist/types/graphs/graphFactory.d.ts +13 -0
  81. package/dist/types/graphs/index.d.ts +2 -0
  82. package/dist/types/run.d.ts +2 -2
  83. package/dist/types/tools/SubagentTool.d.ts +1 -1
  84. package/dist/types/tools/search/rerankers.d.ts +28 -0
  85. package/dist/types/tools/search/types.d.ts +38 -1
  86. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  87. package/dist/types/tools/subagent/SubagentExecutor.d.ts +32 -37
  88. package/dist/types/tools/subagent/SubagentReplay.d.ts +5 -1
  89. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  90. package/dist/types/tools/subagent/index.d.ts +1 -1
  91. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  92. package/dist/types/types/graph.d.ts +184 -19
  93. package/package.json +3 -1
  94. package/src/agents/AgentContext.ts +1 -1
  95. package/src/graphs/Graph.ts +111 -60
  96. package/src/graphs/MultiAgentGraph.ts +149 -47
  97. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  98. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  99. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  100. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  101. package/src/graphs/createGraph.ts +32 -0
  102. package/src/graphs/graphFactory.ts +12 -0
  103. package/src/graphs/index.ts +2 -0
  104. package/src/run.ts +50 -38
  105. package/src/specs/graph-subagent.live.test.ts +242 -0
  106. package/src/specs/graph-subagent.test.ts +322 -0
  107. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  108. package/src/specs/subagent.test.ts +130 -4
  109. package/src/tools/SubagentTool.ts +4 -4
  110. package/src/tools/ToolNode.ts +22 -10
  111. package/src/tools/__tests__/SubagentExecutor.test.ts +198 -9
  112. package/src/tools/__tests__/SubagentReplay.test.ts +29 -0
  113. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +28 -2
  114. package/src/tools/__tests__/subagentHooks.test.ts +90 -3
  115. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  116. package/src/tools/search/rerankers.ts +260 -0
  117. package/src/tools/search/tool.ts +6 -0
  118. package/src/tools/search/types.ts +45 -1
  119. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  120. package/src/tools/subagent/SubagentExecutor.ts +1245 -563
  121. package/src/tools/subagent/SubagentReplay.ts +10 -1
  122. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  123. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  124. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  125. package/src/tools/subagent/childGraphConfig.ts +694 -0
  126. package/src/tools/subagent/index.ts +6 -0
  127. package/src/tools/subagent/runtimeLimits.ts +7 -0
  128. package/src/types/graph.ts +217 -19
@@ -1,4 +1,5 @@
1
1
  import { nanoid } from 'nanoid';
2
+ import { createHash } from 'crypto';
2
3
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
3
4
  import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
4
5
  import {
@@ -34,6 +35,8 @@ import type {
34
35
  AgentInputs,
35
36
  BaseGraphState,
36
37
  CompiledStateWorkflow,
38
+ EventHandler,
39
+ MultiAgentGraphState,
37
40
  HumanInTheLoopConfig,
38
41
  InjectedMessage,
39
42
  MessageDeltaEvent,
@@ -42,9 +45,14 @@ import type {
42
45
  RunStep,
43
46
  RunStepDeltaEvent,
44
47
  StandardGraphInput,
48
+ ExecutableSubagentConfigEntry,
45
49
  ResolvedSubagentConfig,
50
+ ResolvedSubagentConfigEntry,
51
+ SubagentExecutionContext,
52
+ SubagentResolveConfigurable,
53
+ SubagentResolveRequestContext,
54
+ SubagentResolveUserContext,
46
55
  StepCompleted,
47
- SubagentConfig,
48
56
  SubagentUpdateEvent,
49
57
  SubagentUpdatePhase,
50
58
  SubagentUsageSink,
@@ -55,16 +63,34 @@ import type {
55
63
  ToolApprovalDecision,
56
64
  ToolApprovalDecisionMap,
57
65
  } from '@/types';
66
+ import type {
67
+ PreparedSubagentExecutionIdentity,
68
+ SubagentDefinitionBinding,
69
+ SubagentExecutionIdentity,
70
+ SubagentExecutionPreparationLease,
71
+ SubagentExecutionRecord,
72
+ } from './SubagentExecutionRegistry';
58
73
  import type {
59
74
  SubagentResumeExecution,
60
75
  SubagentResumeManifest,
61
76
  SubagentCheckpointReference,
62
77
  SettledSubagentToolOutput,
63
78
  } from './SubagentReplay';
64
- import type { AggregatedHookResult, HookRegistry } from '@/hooks';
65
- import type { AgentContext } from '@/agents/AgentContext';
79
+ import type {
80
+ AggregatedHookResult,
81
+ HookRegistry,
82
+ ToolApprovalReplaySnapshot,
83
+ } from '@/hooks';
84
+ import type { GraphFactory } from '@/graphs/graphFactory';
66
85
  import type { StandardGraph } from '@/graphs/Graph';
67
86
  import type { HandlerRegistry } from '@/events';
87
+ import {
88
+ getSubagentApprovalExecutionScope,
89
+ SubagentDefinitionBindingError,
90
+ SubagentExecutionRegistry,
91
+ SubagentInvocationBindingError,
92
+ SubagentSettlementBindingError,
93
+ } from './SubagentExecutionRegistry';
68
94
  import {
69
95
  getSubagentResumeManifest,
70
96
  attachSubagentResumeManifest,
@@ -76,6 +102,10 @@ import {
76
102
  StreamLimitExceededError,
77
103
  RUN_BREAKER_SCOPE_CONFIG_KEY,
78
104
  } from '@/llm/streamLimits';
105
+ import {
106
+ DEFAULT_SUBAGENT_MAX_TURNS,
107
+ SUBAGENT_RECURSION_MULTIPLIER,
108
+ } from './runtimeLimits';
79
109
  import {
80
110
  ContentTypes,
81
111
  Constants,
@@ -87,12 +117,59 @@ import {
87
117
  executeHooks,
88
118
  TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
89
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';
90
135
 
91
- const DEFAULT_MAX_TURNS = 25;
92
- const RECURSION_MULTIPLIER = 3;
93
136
  const ERROR_MESSAGE_MAX_CHARS = 200;
94
- const MAX_PENDING_SUBAGENT_UPDATES = 64;
137
+ const MAX_QUEUED_SUBAGENT_UPDATES = 64;
138
+ const SUBAGENT_UPDATE_HANDLER_TIMEOUT_MS = 5_000;
95
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
+ }
96
173
 
97
174
  const HOOK_FALLBACK: AggregatedHookResult = Object.freeze({
98
175
  additionalContexts: [] as string[],
@@ -201,6 +278,7 @@ type QueuedSubagentUpdate = {
201
278
  eventName: string;
202
279
  phase: SubagentUpdatePhase;
203
280
  data: unknown;
281
+ memberAgentId?: string;
204
282
  };
205
283
 
206
284
  type ForwarderCallback = {
@@ -212,7 +290,7 @@ type StatefulCompiledWorkflow = Omit<CompiledStateWorkflow, 'invoke'> & {
212
290
  invoke(
213
291
  input: BaseGraphState | Command | null,
214
292
  config?: RunnableConfig
215
- ): Promise<BaseGraphState>;
293
+ ): Promise<MultiAgentGraphState>;
216
294
  getState(config: RunnableConfig): Promise<StateSnapshot>;
217
295
  updateState?(
218
296
  config: RunnableConfig,
@@ -235,6 +313,7 @@ type ActiveChildRun = {
235
313
  pendingInterrupts: Interrupt[];
236
314
  invokeConfig?: RunnableConfig;
237
315
  childAgentId: string;
316
+ checkpointNodeId: string;
238
317
  childRunId: string;
239
318
  };
240
319
 
@@ -261,18 +340,54 @@ type SubagentCheckpointMarker = {
261
340
  settledOutput?: PersistedToolOutput;
262
341
  };
263
342
 
264
- type ChildExecutionIdentity = {
265
- childRunId: string;
266
- childThreadId: string;
267
- approvalExecutionScope: string;
268
- resumeExecution?: SubagentResumeExecution;
269
- };
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
+ }
270
370
 
271
- type ChildExecutionIdentityParams = {
272
- threadId?: string;
273
- parentToolCallId: string;
274
- parentConfigurable?: Record<string, unknown>;
275
- };
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
+ }
276
391
 
277
392
  const LANGGRAPH_RUNTIME_CONFIG_PREFIX = '__pregel_';
278
393
  const LANGGRAPH_RESUME_MAP_CONFIG_KEY = '__pregel_resume_map';
@@ -285,6 +400,7 @@ const SUBAGENT_CHECKPOINT_MARKER_KEY = '__librechat_subagent_checkpoint';
285
400
  const SUBAGENT_HOOK_SESSION_KEY = '__librechat_subagent_hook_session';
286
401
  const SUBAGENT_RUN_ID_KEY = '__librechat_subagent_run_id';
287
402
  const SUBAGENT_REPLAY_NODE = 'subagent-replay';
403
+ export const DEFAULT_SUBAGENT_DESCRIPTION = 'No task description provided';
288
404
 
289
405
  function isCheckpointSaver(value: unknown): value is BaseCheckpointSaver {
290
406
  if (value == null || typeof value !== 'object') {
@@ -480,6 +596,10 @@ function serializeToolOutput(
480
596
  };
481
597
  }
482
598
 
599
+ function getSettlementFingerprint(output: PersistedToolOutput): string {
600
+ return createHash('sha256').update(stableStringify(output)).digest('hex');
601
+ }
602
+
483
603
  function deserializeToolOutput(
484
604
  output: PersistedToolOutput
485
605
  ): SettledSubagentToolOutput {
@@ -504,61 +624,6 @@ function deserializeToolOutput(
504
624
  };
505
625
  }
506
626
 
507
- function getParentCheckpointFork(
508
- configurable: Record<string, unknown> | undefined
509
- ): string {
510
- const checkpointId = configurable?.checkpoint_id;
511
- return typeof checkpointId === 'string' && checkpointId.length > 0
512
- ? checkpointId
513
- : 'root';
514
- }
515
-
516
- function getResumeAttemptId(
517
- configurable: Record<string, unknown> | undefined,
518
- fallback: string
519
- ): string {
520
- const attemptId = configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
521
- return typeof attemptId === 'string' && attemptId.length > 0
522
- ? attemptId
523
- : fallback;
524
- }
525
-
526
- function getApprovalExecutionScope(
527
- childRunId: string,
528
- resumeAttemptId: string
529
- ): string {
530
- return `subagent-approval:${Buffer.from(
531
- JSON.stringify([childRunId, resumeAttemptId])
532
- ).toString('base64url')}`;
533
- }
534
-
535
- function getChildThreadId(args: {
536
- parentRunId: string;
537
- parentAgentId?: string;
538
- threadId?: string;
539
- parentToolCallId: string;
540
- parentConfigurable?: Record<string, unknown>;
541
- branchId?: string;
542
- }): string {
543
- const durableParentId = args.threadId ?? args.parentRunId;
544
- const parentFork = getParentCheckpointFork(args.parentConfigurable);
545
- const parentBatch =
546
- args.parentConfigurable?.[SUBAGENT_PARENT_BATCH_CONFIG_KEY];
547
- const identity = [
548
- durableParentId,
549
- parentFork,
550
- args.parentAgentId ?? 'agent',
551
- args.parentToolCallId,
552
- typeof parentBatch === 'string' && parentBatch.length > 0
553
- ? parentBatch
554
- : 'batch',
555
- ];
556
- if (args.branchId != null) {
557
- identity.push(args.branchId);
558
- }
559
- return `subagent:${Buffer.from(JSON.stringify(identity)).toString('base64url')}`;
560
- }
561
-
562
627
  function isToolApprovalPayload(
563
628
  value: unknown
564
629
  ): value is ToolApprovalInterruptPayload {
@@ -575,12 +640,13 @@ function addSubagentScope(
575
640
  resumeManifest?: SubagentResumeManifest
576
641
  ): Interrupt[] {
577
642
  return interrupts.map((childInterrupt) => {
578
- const payload = isToolApprovalPayload(childInterrupt.value)
579
- ? {
643
+ let payload = childInterrupt.value;
644
+ if (isToolApprovalPayload(payload)) {
645
+ payload = {
580
646
  ...childInterrupt.value,
581
647
  subagent: childInterrupt.value.subagent ?? scope,
582
- }
583
- : childInterrupt.value;
648
+ };
649
+ }
584
650
  return {
585
651
  ...childInterrupt,
586
652
  value:
@@ -656,6 +722,8 @@ export type SubagentExecuteParams = {
656
722
  description: string;
657
723
  subagentType: string;
658
724
  threadId?: string;
725
+ /** Signal attached to this specific parent tool invocation. */
726
+ signal?: AbortSignal;
659
727
  /**
660
728
  * Breaker controller captured at the parent TOOL BATCH's entry, before
661
729
  * PreToolUse hooks. Preferred over the live scope accessor: a graph reset
@@ -720,7 +788,7 @@ export type SubagentExecuteResult = {
720
788
  export type ChildGraphFactory = (input: StandardGraphInput) => StandardGraph;
721
789
 
722
790
  export type SubagentExecutorOptions = {
723
- configs: Map<string, ResolvedSubagentConfig>;
791
+ configs: ReadonlyMap<string, ExecutableSubagentConfigEntry>;
724
792
  parentSignal?: AbortSignal;
725
793
  /** Run-scoped breaker abort shared by every executor of one graph, so a
726
794
  * child tripping a stream limit stops subagents running under OTHER
@@ -736,6 +804,8 @@ export type SubagentExecutorOptions = {
736
804
  hookRegistry?: HookRegistry;
737
805
  parentRunId: string;
738
806
  parentAgentId?: string;
807
+ /** Root identity, policy session, and lineage inherited by a child graph. */
808
+ executionContext?: SubagentExecutionContext;
739
809
  langfuse?: StandardGraphInput['langfuse'];
740
810
  tokenCounter?: TokenCounter;
741
811
  /**
@@ -760,6 +830,9 @@ export type SubagentExecutorOptions = {
760
830
  * module dependency.
761
831
  */
762
832
  createChildGraph: ChildGraphFactory;
833
+ /** Preferred polymorphic child constructor. The legacy standard-only
834
+ * factory remains required for source compatibility. */
835
+ createChildGraphByKind?: GraphFactory;
763
836
  /**
764
837
  * Parent's event handler registry. When provided, child-graph events are
765
838
  * forwarded through this registry so hosts can:
@@ -784,8 +857,15 @@ export type SubagentExecutorOptions = {
784
857
  usageSink?: SubagentUsageSink;
785
858
  };
786
859
 
860
+ type DurableExecutionRecord = SubagentExecutionRecord<
861
+ SubagentExecuteResult,
862
+ ResolvedSubagentConfig,
863
+ ActiveChildRun,
864
+ PersistedToolOutput
865
+ >;
866
+
787
867
  export class SubagentExecutor {
788
- private readonly configs: Map<string, ResolvedSubagentConfig>;
868
+ private readonly configs: ReadonlyMap<string, ExecutableSubagentConfigEntry>;
789
869
  private readonly parentSignal?: AbortSignal;
790
870
  /** Aborted when a child trips a stream circuit breaker: parallel sibling
791
871
  * subagents run concurrently on the parent's signal, and rejecting the
@@ -798,6 +878,7 @@ export class SubagentExecutor {
798
878
  private readonly hookRegistry?: HookRegistry;
799
879
  private readonly parentRunId: string;
800
880
  private readonly parentAgentId?: string;
881
+ private readonly executionContext: SubagentExecutionContext;
801
882
  private readonly langfuse?: StandardGraphInput['langfuse'];
802
883
  private readonly tokenCounter?: TokenCounter;
803
884
  private readonly streamLimits?: StandardGraphInput['streamLimits'];
@@ -805,22 +886,14 @@ export class SubagentExecutor {
805
886
  private readonly checkpointer?: BaseCheckpointSaver;
806
887
  private readonly maxDepth: number;
807
888
  private readonly createChildGraph: ChildGraphFactory;
889
+ private readonly createChildGraphByKind?: GraphFactory;
808
890
  private readonly usageSink?: SubagentUsageSink;
809
- private readonly checkpointThreadIds = new Set<string>();
810
- private readonly startedChildRuns = new Set<string>();
811
- private readonly completedChildRuns = new Set<string>();
812
- private readonly completedChildResults = new Map<
813
- string,
814
- SubagentExecuteResult
815
- >();
816
- private readonly childExecutionIdentities = new Map<
817
- string,
818
- Pick<
819
- ChildExecutionIdentity,
820
- 'childRunId' | 'childThreadId' | 'approvalExecutionScope'
821
- >
822
- >();
823
- private readonly activeChildRuns = new Map<string, ActiveChildRun>();
891
+ private readonly executions: SubagentExecutionRegistry<
892
+ SubagentExecuteResult,
893
+ ResolvedSubagentConfig,
894
+ ActiveChildRun,
895
+ PersistedToolOutput
896
+ >;
824
897
  private replayCheckpointWorkflow?: ReplayCheckpointWorkflow;
825
898
  private readonly resolveParentHandlerRegistry?: () =>
826
899
  | HandlerRegistry
@@ -833,6 +906,12 @@ export class SubagentExecutor {
833
906
  this.hookRegistry = options.hookRegistry;
834
907
  this.parentRunId = options.parentRunId;
835
908
  this.parentAgentId = options.parentAgentId;
909
+ this.executionContext = options.executionContext ?? {
910
+ rootRunId: options.parentRunId,
911
+ hookSessionId: options.parentRunId,
912
+ depth: 0,
913
+ ancestry: [],
914
+ };
836
915
  this.langfuse = options.langfuse;
837
916
  this.tokenCounter = options.tokenCounter;
838
917
  this.streamLimits = options.streamLimits;
@@ -840,8 +919,15 @@ export class SubagentExecutor {
840
919
  this.checkpointer = isCheckpointSaver(options.checkpointer)
841
920
  ? options.checkpointer
842
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
+ });
843
928
  this.maxDepth = options.maxDepth ?? 1;
844
929
  this.createChildGraph = options.createChildGraph;
930
+ this.createChildGraphByKind = options.createChildGraphByKind;
845
931
  this.usageSink = options.usageSink;
846
932
  const rawRegistry = options.parentHandlerRegistry;
847
933
  if (typeof rawRegistry === 'function') {
@@ -860,12 +946,17 @@ export class SubagentExecutor {
860
946
  /** One signal that fires on the parent's abort or the breaker abort,
861
947
  * collapsed to a single signal when possible (mirrors
862
948
  * `composeAbortSignals` in Graph.ts). */
863
- private composeChildSignal(breaker: AbortController): AbortSignal {
864
- const child = breaker.signal;
865
- if (this.parentSignal == null || this.parentSignal === child) {
866
- return child;
867
- }
868
- return AbortSignal.any([this.parentSignal, 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
+ );
869
960
  }
870
961
 
871
962
  /** Snapshot of the parent's registry at the moment a subagent is dispatched. */
@@ -873,6 +964,74 @@ export class SubagentExecutor {
873
964
  return this.resolveParentHandlerRegistry?.();
874
965
  }
875
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
+
876
1035
  /**
877
1036
  * Keeps the original child thread as an immutable resume source once a
878
1037
  * different parent Run reconstructs it. Each rebuilt parent gets a private
@@ -880,60 +1039,138 @@ export class SubagentExecutor {
880
1039
  * activity and usage correlation across the interrupt boundary.
881
1040
  */
882
1041
  private async resolveChildExecutionIdentity(
883
- params: ChildExecutionIdentityParams
884
- ): Promise<ChildExecutionIdentity> {
885
- const currentChildRunId = `${this.parentRunId}_sub_${params.parentToolCallId}`;
886
- const resumeAttemptId = getResumeAttemptId(
887
- params.parentConfigurable,
888
- this.parentRunId
889
- );
890
- const baseChildThreadId = getChildThreadId({
891
- parentRunId: this.parentRunId,
892
- parentAgentId: this.parentAgentId,
893
- threadId: params.threadId,
894
- parentToolCallId: params.parentToolCallId,
895
- parentConfigurable: params.parentConfigurable,
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
+ };
896
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;
897
1097
  if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null) {
898
- return {
1098
+ return this.prepareIdentity(execution, {
899
1099
  childRunId: currentChildRunId,
900
1100
  childThreadId: baseChildThreadId,
901
1101
  approvalExecutionScope: currentChildRunId,
902
- };
1102
+ });
903
1103
  }
904
1104
 
905
- const resumeManifest = getSubagentResumeManifest(params.parentConfigurable);
906
- const resumeExecution = resumeManifest?.executions.find(
907
- (execution) => execution.parentToolCallId === params.parentToolCallId
908
- );
909
- const branchChildThreadId = getChildThreadId({
910
- parentRunId: this.parentRunId,
911
- parentAgentId: this.parentAgentId,
912
- threadId: params.threadId,
913
- parentToolCallId: params.parentToolCallId,
914
- parentConfigurable: params.parentConfigurable,
915
- branchId: resumeAttemptId,
916
- });
1105
+ const { resumeExecution } = execution;
917
1106
  if (resumeExecution != null) {
918
- const approvalExecutionScope = getApprovalExecutionScope(
1107
+ const approvalExecutionScope = getSubagentApprovalExecutionScope(
919
1108
  resumeExecution.childRunId,
920
1109
  resumeAttemptId
921
1110
  );
922
- await this.forkCheckpointSnapshot(
923
- resumeExecution.checkpoints,
924
- branchChildThreadId
925
- );
926
- this.hookRegistry?.restorePendingToolApprovals(
927
- approvalExecutionScope,
928
- approvalExecutionScope,
929
- resumeExecution.approvalReplays
930
- );
931
- this.checkpointThreadIds.add(branchChildThreadId);
932
- return {
1111
+ const identity = {
933
1112
  childRunId: resumeExecution.childRunId,
934
1113
  childThreadId: branchChildThreadId,
935
1114
  approvalExecutionScope,
936
- resumeExecution,
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
+ },
937
1174
  };
938
1175
  }
939
1176
 
@@ -943,37 +1180,46 @@ export class SubagentExecutor {
943
1180
  if (branchTuple != null) {
944
1181
  const childRunId =
945
1182
  getSubagentRunId(getTupleMessages(branchTuple)) ?? currentChildRunId;
946
- this.checkpointThreadIds.add(branchChildThreadId);
947
- return {
1183
+ return this.prepareIdentity(execution, {
948
1184
  childRunId,
949
1185
  childThreadId: branchChildThreadId,
950
- approvalExecutionScope: getApprovalExecutionScope(
1186
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
951
1187
  childRunId,
952
1188
  resumeAttemptId
953
1189
  ),
954
- };
1190
+ });
955
1191
  }
956
1192
 
957
1193
  const baseTuple = await this.checkpointer.getTuple({
958
1194
  configurable: { thread_id: baseChildThreadId },
959
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
+ }
960
1208
  const persistedChildRunId = getSubagentRunId(getTupleMessages(baseTuple));
961
1209
  if (
962
- baseTuple == null ||
963
- persistedChildRunId == null ||
964
- persistedChildRunId === currentChildRunId
1210
+ !explicitResumeAttempt &&
1211
+ (persistedChildRunId == null || persistedChildRunId === currentChildRunId)
965
1212
  ) {
966
- this.checkpointThreadIds.add(baseChildThreadId);
967
- return {
968
- childRunId: persistedChildRunId ?? currentChildRunId,
1213
+ const childRunId = persistedChildRunId ?? currentChildRunId;
1214
+ return this.prepareIdentity(execution, {
1215
+ childRunId,
969
1216
  childThreadId: baseChildThreadId,
970
- approvalExecutionScope: getApprovalExecutionScope(
971
- persistedChildRunId ?? currentChildRunId,
1217
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1218
+ childRunId,
972
1219
  resumeAttemptId
973
1220
  ),
974
- };
1221
+ });
975
1222
  }
976
-
977
1223
  const sourceCheckpoints =
978
1224
  await this.getLatestCheckpointSnapshot(baseChildThreadId);
979
1225
  if (sourceCheckpoints.length === 0) {
@@ -981,16 +1227,77 @@ export class SubagentExecutor {
981
1227
  `Cannot fork subagent checkpoint thread "${baseChildThreadId}" without a checkpoint ID.`
982
1228
  );
983
1229
  }
984
- await this.forkCheckpointSnapshot(sourceCheckpoints, branchChildThreadId);
985
- this.checkpointThreadIds.add(branchChildThreadId);
986
- return {
987
- childRunId: persistedChildRunId,
1230
+ const childRunId = persistedChildRunId ?? currentChildRunId;
1231
+ const identity = {
1232
+ childRunId,
988
1233
  childThreadId: branchChildThreadId,
989
- approvalExecutionScope: getApprovalExecutionScope(
990
- persistedChildRunId,
1234
+ approvalExecutionScope: getSubagentApprovalExecutionScope(
1235
+ childRunId,
991
1236
  resumeAttemptId
992
1237
  ),
993
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;
994
1301
  }
995
1302
 
996
1303
  /** Captures one exact checkpoint head per namespace for a child thread. */
@@ -1130,28 +1437,26 @@ export class SubagentExecutor {
1130
1437
  }
1131
1438
 
1132
1439
  private async createResumeManifest(
1133
- parentToolCallIds?: ReadonlySet<string>
1440
+ records: ReadonlyArray<DurableExecutionRecord>
1134
1441
  ): Promise<SubagentResumeManifest | undefined> {
1135
1442
  if (this.checkpointer == null) {
1136
1443
  return undefined;
1137
1444
  }
1138
1445
  const executions: SubagentResumeExecution[] = [];
1139
- for (const [parentToolCallId, identity] of this.childExecutionIdentities) {
1140
- if (
1141
- parentToolCallIds != null &&
1142
- !parentToolCallIds.has(parentToolCallId)
1143
- ) {
1446
+ for (const record of records) {
1447
+ const { identity, binding, activeRun } = record.snapshot;
1448
+ if (identity == null) {
1144
1449
  continue;
1145
1450
  }
1451
+ const { parentToolCallId } = record.address;
1146
1452
  const checkpoints = await this.getLatestCheckpointSnapshot(
1147
1453
  identity.childThreadId
1148
1454
  );
1149
1455
  if (checkpoints.length === 0) {
1150
1456
  continue;
1151
1457
  }
1152
- const activeChildRun = this.activeChildRuns.get(identity.childThreadId);
1153
1458
  const configuredHookSessionId =
1154
- activeChildRun?.invokeConfig?.configurable?.run_id;
1459
+ activeRun?.invokeConfig?.configurable?.run_id;
1155
1460
  const hookSessionId =
1156
1461
  typeof configuredHookSessionId === 'string' &&
1157
1462
  configuredHookSessionId.length > 0
@@ -1162,12 +1467,12 @@ export class SubagentExecutor {
1162
1467
  identity.approvalExecutionScope,
1163
1468
  identity.approvalExecutionScope
1164
1469
  ) ?? [];
1165
- const descendant = activeChildRun?.pendingInterrupts
1470
+ const descendant = activeRun?.pendingInterrupts
1166
1471
  .map((pendingInterrupt) =>
1167
1472
  getSubagentResumeManifest(pendingInterrupt.value)
1168
1473
  )
1169
1474
  .find((manifest) => manifest != null);
1170
- const graphState = activeChildRun?.graph.createSubagentResumeState(
1475
+ const graphState = activeRun?.graph.createSubagentResumeState(
1171
1476
  hookSessionId
1172
1477
  ) ?? {
1173
1478
  toolCallSteps: [],
@@ -1179,6 +1484,10 @@ export class SubagentExecutor {
1179
1484
  executions.push({
1180
1485
  parentToolCallId,
1181
1486
  childRunId: identity.childRunId,
1487
+ ...(binding?.subagentType == null
1488
+ ? {}
1489
+ : { subagentType: binding.subagentType }),
1490
+ ...(binding?.configId == null ? {} : { configId: binding.configId }),
1182
1491
  approvalExecutionScope: identity.approvalExecutionScope,
1183
1492
  checkpoints,
1184
1493
  graphState,
@@ -1196,25 +1505,22 @@ export class SubagentExecutor {
1196
1505
  }
1197
1506
 
1198
1507
  getResumeManifest(
1199
- parentToolCallIds?: ReadonlySet<string>
1508
+ parentToolCallIds?: ReadonlySet<string>,
1509
+ config?: RunnableConfig
1200
1510
  ): Promise<SubagentResumeManifest | undefined> {
1201
- return this.createResumeManifest(parentToolCallIds);
1511
+ return this.createResumeManifest(
1512
+ this.executions.selectForResume({ parentToolCallIds, config })
1513
+ );
1202
1514
  }
1203
1515
 
1204
1516
  getChildCheckpointThreadIds(): string[] {
1205
- const threadIds = new Set(this.checkpointThreadIds);
1206
- for (const activeChildRun of this.activeChildRuns.values()) {
1207
- for (const threadId of this.getGraphChildCheckpointThreadIds(
1208
- activeChildRun.graph
1209
- )) {
1210
- threadIds.add(threadId);
1211
- }
1212
- }
1213
- return [...threadIds];
1517
+ return this.executions.getCheckpointThreadIds((activeRun) =>
1518
+ this.getGraphChildCheckpointThreadIds(activeRun.graph)
1519
+ );
1214
1520
  }
1215
1521
 
1216
1522
  resetCheckpointThreadIds(): void {
1217
- this.checkpointThreadIds.clear();
1523
+ this.executions.resetCheckpointThreadIds();
1218
1524
  }
1219
1525
 
1220
1526
  private getGraphChildCheckpointThreadIds(graph: StandardGraph): string[] {
@@ -1225,25 +1531,54 @@ export class SubagentExecutor {
1225
1531
  }
1226
1532
 
1227
1533
  private clearChildGraph(graph: StandardGraph): void {
1228
- for (const threadId of this.getGraphChildCheckpointThreadIds(graph)) {
1229
- this.checkpointThreadIds.add(threadId);
1230
- }
1534
+ this.executions.rememberCheckpointThread(
1535
+ ...this.getGraphChildCheckpointThreadIds(graph)
1536
+ );
1231
1537
  graph.clearHeavyState();
1232
1538
  }
1233
1539
 
1234
1540
  clearHeavyState(): void {
1235
- for (const activeChildRun of this.activeChildRuns.values()) {
1236
- this.clearChildGraph(activeChildRun.graph);
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;
1237
1560
  }
1238
- this.activeChildRuns.clear();
1239
- this.completedChildResults.clear();
1240
- for (const identity of this.childExecutionIdentities.values()) {
1241
- this.hookRegistry?.clearSession(identity.approvalExecutionScope);
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
+ }
1242
1580
  }
1243
- this.childExecutionIdentities.clear();
1244
- this.startedChildRuns.clear();
1245
- this.completedChildRuns.clear();
1246
- this.replayCheckpointWorkflow = undefined;
1581
+ return fallback;
1247
1582
  }
1248
1583
 
1249
1584
  async getSettledToolOutput(
@@ -1263,25 +1598,109 @@ export class SubagentExecutor {
1263
1598
  | Record<string, unknown>
1264
1599
  | undefined;
1265
1600
  const threadId = parentConfigurable?.thread_id;
1266
- const { childRunId, childThreadId, approvalExecutionScope } =
1267
- await this.resolveChildExecutionIdentity({
1268
- threadId: typeof threadId === 'string' ? threadId : undefined,
1269
- parentToolCallId,
1270
- parentConfigurable,
1271
- });
1272
- this.childExecutionIdentities.set(parentToolCallId, {
1273
- childRunId,
1274
- childThreadId,
1275
- approvalExecutionScope,
1276
- });
1277
- this.checkpointThreadIds.add(childThreadId);
1278
- const checkpoint = await this.checkpointer.getTuple({
1279
- configurable: { thread_id: childThreadId },
1601
+ const execution = this.executions.open({
1602
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1603
+ parentToolCallId,
1604
+ parentConfigurable,
1280
1605
  });
1281
- const messages = getCheckpointMessages(
1282
- checkpoint?.checkpoint.channel_values.messages
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
1283
1643
  );
1284
- const marker = getSubagentCheckpointMarker(messages, parentToolCallId);
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
+ }
1285
1704
  const persistedHookSessionId =
1286
1705
  marker?.hookSessionId ?? getSubagentHookSessionId(messages);
1287
1706
  const currentHookSessionId = parentConfigurable?.run_id;
@@ -1306,9 +1725,10 @@ export class SubagentExecutor {
1306
1725
  settled: SettledSubagentToolOutput
1307
1726
  ): Promise<void> {
1308
1727
  const parentToolCallId = call.id;
1728
+ const checkpointer = this.checkpointer;
1309
1729
  if (
1310
1730
  this.humanInTheLoop?.enabled !== true ||
1311
- this.checkpointer == null ||
1731
+ checkpointer == null ||
1312
1732
  parentToolCallId == null ||
1313
1733
  parentToolCallId === ''
1314
1734
  ) {
@@ -1318,52 +1738,117 @@ export class SubagentExecutor {
1318
1738
  | Record<string, unknown>
1319
1739
  | undefined;
1320
1740
  const threadId = parentConfigurable?.thread_id;
1321
- const { childRunId, childThreadId, approvalExecutionScope } =
1322
- await this.resolveChildExecutionIdentity({
1323
- threadId: typeof threadId === 'string' ? threadId : undefined,
1324
- parentToolCallId,
1325
- parentConfigurable,
1326
- });
1327
- this.childExecutionIdentities.set(parentToolCallId, {
1328
- childRunId,
1329
- childThreadId,
1330
- approvalExecutionScope,
1741
+ const execution = this.executions.open({
1742
+ threadId: typeof threadId === 'string' ? threadId : undefined,
1743
+ parentToolCallId,
1744
+ parentConfigurable,
1331
1745
  });
1332
- this.checkpointThreadIds.add(childThreadId);
1333
- const activeChildRun = this.activeChildRuns.get(childThreadId);
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
+ }
1334
1789
  const persistedOutput = serializeToolOutput(settled);
1335
- if (activeChildRun != null) {
1336
- await this.persistChildCheckpointMarker(
1337
- activeChildRun,
1338
- parentToolCallId,
1339
- persistedOutput
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
+ }
1340
1841
  );
1341
- this.clearChildGraph(activeChildRun.graph);
1342
- this.activeChildRuns.delete(childThreadId);
1343
- return;
1842
+ } catch (error) {
1843
+ if (
1844
+ error instanceof SubagentDefinitionBindingError ||
1845
+ error instanceof SubagentInvocationBindingError ||
1846
+ error instanceof SubagentSettlementBindingError
1847
+ ) {
1848
+ return;
1849
+ }
1850
+ throw error;
1344
1851
  }
1345
- this.replayCheckpointWorkflow ??= createReplayCheckpointWorkflow(
1346
- this.checkpointer
1347
- );
1348
- await this.replayCheckpointWorkflow.updateState(
1349
- { configurable: { thread_id: childThreadId } },
1350
- {
1351
- messages: [
1352
- createSubagentCheckpointMarkerMessage({
1353
- version: 1,
1354
- parentToolCallId,
1355
- lifecycleComplete: true,
1356
- hookSessionId:
1357
- typeof parentConfigurable?.run_id === 'string'
1358
- ? parentConfigurable.run_id
1359
- : this.parentRunId,
1360
- childRunId,
1361
- settledOutput: persistedOutput,
1362
- }),
1363
- ],
1364
- },
1365
- SUBAGENT_REPLAY_NODE
1366
- );
1367
1852
  }
1368
1853
 
1369
1854
  private async persistChildCheckpointMarker(
@@ -1396,11 +1881,83 @@ export class SubagentExecutor {
1396
1881
  }),
1397
1882
  ],
1398
1883
  },
1399
- activeChildRun.childAgentId
1884
+ activeChildRun.checkpointNodeId
1400
1885
  );
1401
1886
  }
1402
1887
 
1403
- async execute(params: SubagentExecuteParams): Promise<SubagentExecuteResult> {
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
+ }
1954
+ }
1955
+
1956
+ private async executeOnce(
1957
+ params: SubagentExecuteParams,
1958
+ execution: DurableExecutionRecord,
1959
+ executableConfig: ExecutableSubagentConfigEntry
1960
+ ): Promise<SubagentExecuteResult> {
1404
1961
  const { description, subagentType, threadId, parentToolCallId } = params;
1405
1962
  /** Captured ONCE per execution, preferring the controller the parent
1406
1963
  * tool batch captured at ITS entry (before PreToolUse hooks): a failed
@@ -1410,61 +1967,75 @@ export class SubagentExecutor {
1410
1967
  * new run's breaker from an old child's stream-limit breach. Signal and
1411
1968
  * trip target both bind to this capture. */
1412
1969
  const childBreaker = params.breaker ?? this.resolveBreakerController();
1413
- const childSignal = this.composeChildSignal(childBreaker);
1414
- const config = this.configs.get(subagentType);
1970
+ const childSignal = this.composeChildSignal(childBreaker, params.signal);
1415
1971
 
1416
- if (!config) {
1417
- 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);
1418
1982
  return {
1419
- content: `Error: Unknown subagent type "${subagentType}". Available types: ${available}`,
1983
+ content: SUBAGENT_CONFIG_CHANGED_MESSAGE,
1420
1984
  messages: [],
1421
1985
  };
1422
1986
  }
1423
-
1424
- 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
+ }
1425
1995
  return {
1426
- content: 'Error: Maximum subagent nesting depth exceeded.',
1996
+ content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
1427
1997
  messages: [],
1428
1998
  };
1429
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
+ }
1430
2018
 
1431
- if (
1432
- this.humanInTheLoop?.enabled === true &&
1433
- (parentToolCallId == null || parentToolCallId === '')
1434
- ) {
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;
2032
+ }
1435
2033
  return {
1436
- content:
1437
- 'Error: Resumable subagent execution requires a parent tool call ID.',
2034
+ content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
1438
2035
  messages: [],
1439
2036
  };
1440
2037
  }
1441
2038
 
1442
- const executionSuffix = parentToolCallId ?? nanoid(8);
1443
- const {
1444
- childRunId,
1445
- childThreadId,
1446
- approvalExecutionScope,
1447
- resumeExecution,
1448
- } = await this.resolveChildExecutionIdentity({
1449
- threadId,
1450
- parentToolCallId: executionSuffix,
1451
- parentConfigurable: params.parentConfigurable,
1452
- });
1453
- this.childExecutionIdentities.set(executionSuffix, {
1454
- childRunId,
1455
- childThreadId,
1456
- approvalExecutionScope,
1457
- });
1458
- const childExecutionKey = childThreadId;
1459
- const childAgentId =
1460
- config.agentInputs.agentId ||
1461
- `${this.parentAgentId ?? 'agent'}_sub_${executionSuffix}`;
1462
- const completedChildResult =
1463
- this.completedChildResults.get(childExecutionKey);
1464
- if (completedChildResult != null) {
1465
- return completedChildResult;
1466
- }
1467
-
1468
2039
  const parentRegistry = this.getParentHandlerRegistry();
1469
2040
  const forwardingEnabled = parentRegistry != null;
1470
2041
  /**
@@ -1478,79 +2049,121 @@ export class SubagentExecutor {
1478
2049
  */
1479
2050
  const hasToolExecuteHandler =
1480
2051
  parentRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) != null;
1481
- const childInputs = buildChildInputs(
2052
+ const childPlan = createChildGraphPlan({
1482
2053
  config,
1483
- childAgentId,
1484
- this.maxDepth,
1485
- /* keepToolDefinitions */ hasToolExecuteHandler
1486
- );
1487
- 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);
1488
2085
 
1489
2086
  const hostUsageSink = this.usageSink;
1490
- const cachedChildRun = this.activeChildRuns.get(childExecutionKey);
1491
- const childGraph =
1492
- cachedChildRun?.graph ??
1493
- this.createChildGraph({
1494
- runId: childRunId,
1495
- signal: childSignal,
1496
- agents: [childInputs],
1497
- langfuse: this.langfuse,
1498
- tokenCounter: this.tokenCounter,
1499
- streamLimits: this.streamLimits,
1500
- subagentScope: true,
1501
- /**
1502
- * Forwarded so the child graph's own `SubagentExecutor` (created in
1503
- * its `createAgentNode` when `allowNested` keeps subagentConfigs)
1504
- * reports nested-child usage through the same host sink. Each nesting
1505
- * level attaches its own capture callback — `workflow.invoke` replaces
1506
- * the inherited callback chain, so a single top-level handler would
1507
- * never see grandchild model calls.
1508
- *
1509
- * The wrapper rewrites `runId` to THIS executor's parent run: nested
1510
- * executors emit with their own `parentRunId` (a `*_sub_*` child id),
1511
- * and each wrapper layer rewrites upward, so by the time an event
1512
- * reaches the host sink its `runId` is the ROOT run hosts keying
1513
- * billing by run id never see intermediate child run ids there
1514
- * (`subagentRunId` still identifies the emitting child).
1515
- */
1516
- subagentUsageSink:
1517
- hostUsageSink == null
1518
- ? undefined
1519
- : /** Returns the host sink's result so async sinks stay awaited
1520
- * through every wrapper layer. */
1521
- (event): void | Promise<void> =>
1522
- hostUsageSink({ ...event, runId: this.parentRunId }),
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 = {
2097
+ runId: childRunId,
2098
+ signal: childSignal,
2099
+ agents: childPlan.agents,
2100
+ langfuse: this.langfuse,
2101
+ tokenCounter: this.tokenCounter,
2102
+ streamLimits: this.streamLimits,
2103
+ subagentScope: true,
2104
+ subagentExecutionContext: childExecutionContext,
2105
+ /**
2106
+ * Forwarded so the child graph's own `SubagentExecutor` (created in
2107
+ * its `createAgentNode` when `allowNested` keeps subagentConfigs)
2108
+ * reports nested-child usage through the same host sink. Each nesting
2109
+ * level attaches its own capture callback`workflow.invoke` replaces
2110
+ * the inherited callback chain, so a single top-level handler would
2111
+ * never see grandchild model calls.
2112
+ */
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
+ },
1523
2132
  });
2133
+ }
2134
+ childGraph ??= this.createChildGraph(childGraphInput);
1524
2135
  let forwarding: ForwarderCallback | undefined;
1525
2136
  if (forwardingEnabled) {
1526
2137
  forwarding = this.createForwarderCallback({
1527
2138
  parentRegistry: parentRegistry!,
1528
2139
  subagentType,
2140
+ subagentKind: childPlan.kind,
1529
2141
  subagentAgentId: childAgentId,
1530
2142
  childRunId,
1531
2143
  parentToolCallId,
2144
+ executionContext: childExecutionContext,
1532
2145
  });
1533
2146
  }
1534
2147
  const forwarder = forwarding?.handler;
1535
- let childAlreadyStarted = this.startedChildRuns.has(childExecutionKey);
1536
- let childAlreadyCompleted = this.completedChildRuns.has(childExecutionKey);
2148
+ let childAlreadyStarted = execution.started;
2149
+ let childAlreadyCompleted = execution.completed;
1537
2150
 
1538
- let result: { messages: BaseMessage[] } | undefined;
2151
+ let result: MultiAgentGraphState | undefined;
1539
2152
  let recoveredComplete = false;
1540
2153
  let recoveredInProgress = false;
1541
2154
  try {
1542
2155
  const workflow = (cachedChildRun?.workflow ??
1543
2156
  childGraph.createWorkflow()) as StatefulCompiledWorkflow;
1544
- const activeChildRun = cachedChildRun ?? {
1545
- graph: childGraph,
1546
- workflow,
1547
- pendingInterrupts: [],
1548
- childAgentId,
1549
- childRunId,
1550
- };
1551
- if (cachedChildRun == null) {
1552
- this.activeChildRuns.set(childExecutionKey, activeChildRun);
1553
- }
2157
+ const activeChildRun = execution.activate(
2158
+ cachedChildRun ?? {
2159
+ graph: childGraph,
2160
+ workflow,
2161
+ pendingInterrupts: [],
2162
+ childAgentId,
2163
+ checkpointNodeId: childPlan.checkpointNodeId,
2164
+ childRunId,
2165
+ }
2166
+ );
1554
2167
  /**
1555
2168
  * When `parentHandlerRegistry` is provided (forwarding mode), attach a
1556
2169
  * lightweight callback that intercepts the child's `on_custom_event`
@@ -1584,11 +2197,12 @@ export class SubagentExecutor {
1584
2197
  createUsageCaptureHandler({
1585
2198
  sink: this.usageSink,
1586
2199
  subagentType,
2200
+ subagentKind: childPlan.kind,
1587
2201
  subagentRunId: childRunId,
1588
2202
  subagentAgentId: childAgentId,
1589
2203
  parentRunId: this.parentRunId,
1590
- provider: config.agentInputs.provider,
1591
- fallbackModel: extractConfiguredModel(config.agentInputs),
2204
+ executionContext: childExecutionContext,
2205
+ memberInputs: childPlan.memberInputs,
1592
2206
  })
1593
2207
  );
1594
2208
  }
@@ -1608,46 +2222,35 @@ export class SubagentExecutor {
1608
2222
  */
1609
2223
  const inheritedConfigurable: Record<string, unknown> =
1610
2224
  sanitizeChildConfigurable(params.parentConfigurable);
1611
- const resumeAttemptId = getResumeAttemptId(
1612
- params.parentConfigurable,
1613
- this.parentRunId
1614
- );
1615
- const currentHookSessionId =
1616
- typeof inheritedConfigurable.run_id === 'string' &&
1617
- inheritedConfigurable.run_id.length > 0
1618
- ? inheritedConfigurable.run_id
1619
- : this.parentRunId;
2225
+ const { resumeAttemptId } = execution.address;
1620
2226
  if (cachedChildRun == null && resumeExecution != null) {
1621
2227
  childGraph.restoreSubagentResumeState(
1622
2228
  resumeExecution.graphState,
1623
2229
  currentHookSessionId
1624
2230
  );
1625
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);
1626
2248
  const childInvokeConfig = {
1627
- recursionLimit: maxTurns * RECURSION_MULTIPLIER,
2249
+ recursionLimit,
1628
2250
  signal: childSignal,
1629
2251
  callbacks,
1630
2252
  runName: `subagent:${subagentType}`,
1631
- configurable: {
1632
- ...inheritedConfigurable,
1633
- ...(this.humanInTheLoop?.enabled === true
1634
- ? {
1635
- [TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY]:
1636
- approvalExecutionScope,
1637
- }
1638
- : {}),
1639
- ...(resumeExecution?.descendant == null
1640
- ? {}
1641
- : {
1642
- [SUBAGENT_RESUME_MANIFEST_CONFIG_KEY]:
1643
- resumeExecution.descendant,
1644
- [SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: resumeAttemptId,
1645
- }),
1646
- thread_id:
1647
- this.humanInTheLoop?.enabled === true
1648
- ? childThreadId
1649
- : (inheritedConfigurable.thread_id ?? childRunId),
1650
- },
2253
+ configurable: childConfigurable,
1651
2254
  };
1652
2255
  activeChildRun.invokeConfig = childInvokeConfig;
1653
2256
  if (cachedChildRun == null && this.humanInTheLoop?.enabled === true) {
@@ -1669,12 +2272,12 @@ export class SubagentExecutor {
1669
2272
  const persistedInterrupts = getPersistedInterrupts(persistedState);
1670
2273
  if (persistedInterrupts.length > 0) {
1671
2274
  activeChildRun.pendingInterrupts = persistedInterrupts;
1672
- this.startedChildRuns.add(childExecutionKey);
2275
+ execution.markStarted();
1673
2276
  childAlreadyStarted = true;
1674
2277
  } else if (persistedState.next.length > 0) {
1675
2278
  recoveredInProgress = true;
1676
2279
  childAlreadyStarted = true;
1677
- this.startedChildRuns.add(childExecutionKey);
2280
+ execution.markStarted();
1678
2281
  } else if (persistedState.next.length === 0) {
1679
2282
  const persistedMessages = getPersistedMessages(persistedState);
1680
2283
  if (persistedMessages != null) {
@@ -1686,7 +2289,7 @@ export class SubagentExecutor {
1686
2289
  recoveredComplete = true;
1687
2290
  childAlreadyStarted = true;
1688
2291
  childAlreadyCompleted = marker?.lifecycleComplete === true;
1689
- this.startedChildRuns.add(childExecutionKey);
2292
+ execution.markStarted();
1690
2293
  }
1691
2294
  }
1692
2295
  }
@@ -1722,54 +2325,59 @@ export class SubagentExecutor {
1722
2325
 
1723
2326
  if (
1724
2327
  !childAlreadyStarted &&
1725
- this.hookRegistry?.hasHookFor('SubagentStart', this.parentRunId) ===
1726
- true
2328
+ this.hookRegistry?.hasHookFor(
2329
+ 'SubagentStart',
2330
+ currentHookSessionId
2331
+ ) === true
1727
2332
  ) {
1728
2333
  const hookResult = await executeHooks({
1729
2334
  registry: this.hookRegistry,
1730
2335
  input: {
1731
2336
  hook_event_name: 'SubagentStart',
1732
- runId: this.parentRunId,
2337
+ runId: currentHookSessionId,
1733
2338
  threadId,
1734
2339
  parentAgentId: this.parentAgentId,
1735
2340
  agentId: childAgentId,
1736
2341
  agentType: subagentType,
1737
2342
  inputs: [new HumanMessage(description)],
1738
2343
  },
1739
- sessionId: this.parentRunId,
2344
+ sessionId: currentHookSessionId,
1740
2345
  matchQuery: subagentType,
1741
2346
  }).catch((): AggregatedHookResult => HOOK_FALLBACK);
1742
2347
 
1743
2348
  if (hookResult.decision === 'deny' || hookResult.decision === 'ask') {
1744
2349
  this.clearChildGraph(childGraph);
1745
- this.activeChildRuns.delete(childExecutionKey);
2350
+ execution.releaseActiveRun();
2351
+ this.executions.remove(execution);
1746
2352
  return {
1747
2353
  content: `Blocked: ${hookResult.reason ?? 'Blocked by hook'}`,
1748
2354
  messages: [],
1749
2355
  };
1750
2356
  }
1751
2357
  }
1752
- this.startedChildRuns.add(childExecutionKey);
2358
+ execution.markStarted();
1753
2359
 
1754
2360
  if (forwarder && !childAlreadyStarted) {
1755
2361
  await this.emitSubagentUpdate(parentRegistry!, {
1756
2362
  childRunId,
1757
2363
  subagentType,
2364
+ subagentKind: childPlan.kind,
1758
2365
  subagentAgentId: childAgentId,
1759
2366
  parentToolCallId,
2367
+ executionContext: childExecutionContext,
1760
2368
  phase: 'start',
1761
2369
  label: `Subagent "${subagentType}" started`,
1762
2370
  });
1763
2371
  }
1764
2372
 
1765
- let childResult: BaseGraphState;
2373
+ let childResult: MultiAgentGraphState;
1766
2374
  if (this.humanInTheLoop?.enabled === true) {
1767
2375
  /** Execute as an independently checkpointed root instead of inheriting
1768
2376
  * the parent's Pregel namespace. Parent decisions are routed explicitly
1769
2377
  * by interrupt id, so concurrent children keep isolated resume state. */
1770
2378
  childResult = await AsyncLocalStorageProviderSingleton.runWithConfig(
1771
2379
  childInvokeConfig,
1772
- (): Promise<BaseGraphState> =>
2380
+ (): Promise<MultiAgentGraphState> =>
1773
2381
  workflow.invoke(childInput, childInvokeConfig)
1774
2382
  );
1775
2383
  } else {
@@ -1781,18 +2389,19 @@ export class SubagentExecutor {
1781
2389
  if (childInterrupts != null && childInterrupts.length > 0) {
1782
2390
  throw new GraphInterrupt(childInterrupts);
1783
2391
  }
1784
- result = { messages: childResult.messages };
2392
+ result = childResult;
1785
2393
  }
1786
2394
  } catch (error) {
1787
2395
  if (isGraphInterrupt(error)) {
1788
- const activeChildRun = this.activeChildRuns.get(childExecutionKey);
2396
+ const activeChildRun = execution.activeRun;
1789
2397
  if (activeChildRun != null) {
1790
2398
  activeChildRun.pendingInterrupts = error.interrupts;
2399
+ execution.markInterrupted();
1791
2400
  }
1792
2401
  const resumeManifest =
1793
2402
  activeChildRun == null || parentToolCallId == null
1794
2403
  ? undefined
1795
- : await this.createResumeManifest();
2404
+ : await this.createResumeManifest([execution]);
1796
2405
  await forwarding?.drain();
1797
2406
  throw new GraphInterrupt(
1798
2407
  addSubagentScope(
@@ -1823,15 +2432,17 @@ export class SubagentExecutor {
1823
2432
  await this.emitSubagentUpdate(parentRegistry!, {
1824
2433
  childRunId,
1825
2434
  subagentType,
2435
+ subagentKind: childPlan.kind,
1826
2436
  subagentAgentId: childAgentId,
1827
2437
  parentToolCallId,
2438
+ executionContext: childExecutionContext,
1828
2439
  phase: 'error',
1829
2440
  label: `Subagent "${subagentType}" errored: ${errorMessage}`,
1830
2441
  data: { message: errorMessage },
1831
2442
  });
1832
2443
  }
1833
2444
  this.clearChildGraph(childGraph);
1834
- this.activeChildRuns.delete(childExecutionKey);
2445
+ execution.markFailed();
1835
2446
  /**
1836
2447
  * A tripped stream circuit breaker is a safety abort, not a recoverable
1837
2448
  * subagent failure: converting it into a tool result would let the
@@ -1851,11 +2462,15 @@ export class SubagentExecutor {
1851
2462
  if (result == null) {
1852
2463
  throw new Error('Subagent completed without producing graph state.');
1853
2464
  }
1854
- const filteredContent = filterSubagentResult(result.messages);
2465
+ const filteredContent =
2466
+ childPlan.kind === 'graph'
2467
+ ? filterGraphSubagentResult(result, childPlan.resultAgentId)
2468
+ : filterSubagentResult(result.messages);
1855
2469
 
1856
2470
  if (
1857
2471
  !childAlreadyCompleted &&
1858
- this.hookRegistry?.hasHookFor('SubagentStop', this.parentRunId) === true
2472
+ this.hookRegistry?.hasHookFor('SubagentStop', currentHookSessionId) ===
2473
+ true
1859
2474
  ) {
1860
2475
  /**
1861
2476
  * Awaited (not fire-and-forget) for deterministic test synchronization
@@ -1867,13 +2482,13 @@ export class SubagentExecutor {
1867
2482
  registry: this.hookRegistry,
1868
2483
  input: {
1869
2484
  hook_event_name: 'SubagentStop',
1870
- runId: this.parentRunId,
2485
+ runId: currentHookSessionId,
1871
2486
  threadId,
1872
2487
  agentId: childAgentId,
1873
2488
  agentType: subagentType,
1874
2489
  messages: result.messages,
1875
2490
  },
1876
- sessionId: this.parentRunId,
2491
+ sessionId: currentHookSessionId,
1877
2492
  matchQuery: subagentType,
1878
2493
  }).catch(() => {
1879
2494
  /* SubagentStop is observational — swallow errors */
@@ -1885,14 +2500,16 @@ export class SubagentExecutor {
1885
2500
  await this.emitSubagentUpdate(parentRegistry!, {
1886
2501
  childRunId,
1887
2502
  subagentType,
2503
+ subagentKind: childPlan.kind,
1888
2504
  subagentAgentId: childAgentId,
1889
2505
  parentToolCallId,
2506
+ executionContext: childExecutionContext,
1890
2507
  phase: 'stop',
1891
2508
  label: `Subagent "${subagentType}" finished`,
1892
2509
  });
1893
2510
  }
1894
2511
  if (!childAlreadyCompleted) {
1895
- const activeChildRun = this.activeChildRuns.get(childExecutionKey);
2512
+ const activeChildRun = execution.activeRun;
1896
2513
  if (activeChildRun != null && parentToolCallId != null) {
1897
2514
  await this.persistChildCheckpointMarker(
1898
2515
  activeChildRun,
@@ -1900,7 +2517,6 @@ export class SubagentExecutor {
1900
2517
  );
1901
2518
  }
1902
2519
  }
1903
- this.completedChildRuns.add(childExecutionKey);
1904
2520
 
1905
2521
  this.clearChildGraph(childGraph);
1906
2522
 
@@ -1908,7 +2524,7 @@ export class SubagentExecutor {
1908
2524
  content: filteredContent,
1909
2525
  messages: result.messages,
1910
2526
  };
1911
- this.completedChildResults.set(childExecutionKey, completedResult);
2527
+ execution.markCompleted(completedResult);
1912
2528
  return completedResult;
1913
2529
  }
1914
2530
 
@@ -1922,8 +2538,11 @@ export class SubagentExecutor {
1922
2538
  args: {
1923
2539
  childRunId: string;
1924
2540
  subagentType: string;
2541
+ subagentKind: 'agent' | 'graph';
1925
2542
  subagentAgentId: string;
2543
+ memberAgentId?: string;
1926
2544
  parentToolCallId?: string;
2545
+ executionContext: SubagentExecutionContext;
1927
2546
  phase: SubagentUpdatePhase;
1928
2547
  data?: unknown;
1929
2548
  label?: string;
@@ -1934,22 +2553,23 @@ export class SubagentExecutor {
1934
2553
  return;
1935
2554
  }
1936
2555
  const event: SubagentUpdateEvent = {
1937
- runId: this.parentRunId,
2556
+ runId: args.executionContext.rootRunId,
2557
+ parentRunId: this.parentRunId,
1938
2558
  subagentRunId: args.childRunId,
1939
2559
  subagentType: args.subagentType,
2560
+ subagentKind: args.subagentKind,
1940
2561
  subagentAgentId: args.subagentAgentId,
2562
+ memberAgentId: args.memberAgentId,
1941
2563
  parentAgentId: this.parentAgentId,
1942
2564
  parentToolCallId: args.parentToolCallId,
2565
+ depth: args.executionContext.depth,
2566
+ ancestry: args.executionContext.ancestry,
1943
2567
  phase: args.phase,
1944
2568
  data: args.data,
1945
2569
  label: args.label,
1946
2570
  timestamp: new Date().toISOString(),
1947
2571
  };
1948
- try {
1949
- await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
1950
- } catch {
1951
- /* observational — swallow */
1952
- }
2572
+ await dispatchObservationalSubagentUpdate(handler, event);
1953
2573
  }
1954
2574
 
1955
2575
  /**
@@ -1967,60 +2587,72 @@ export class SubagentExecutor {
1967
2587
  private createForwarderCallback(args: {
1968
2588
  parentRegistry: HandlerRegistry;
1969
2589
  subagentType: string;
2590
+ subagentKind: 'agent' | 'graph';
1970
2591
  subagentAgentId: string;
1971
2592
  childRunId: string;
1972
2593
  parentToolCallId?: string;
2594
+ executionContext: SubagentExecutionContext;
1973
2595
  }): ForwarderCallback {
1974
2596
  const {
1975
2597
  parentRegistry,
1976
2598
  subagentType,
2599
+ subagentKind,
1977
2600
  subagentAgentId,
1978
2601
  childRunId,
1979
2602
  parentToolCallId,
2603
+ executionContext,
1980
2604
  } = args;
1981
- const parentRunId = this.parentRunId;
2605
+ const immediateParentRunId = this.parentRunId;
1982
2606
  const parentAgentId = this.parentAgentId;
1983
2607
 
1984
2608
  const wrap = async (
1985
2609
  eventName: string,
1986
2610
  phase: SubagentUpdatePhase,
1987
- data: unknown
1988
- ): Promise<void> => {
2611
+ data: unknown,
2612
+ memberAgentId?: string
2613
+ ): Promise<boolean> => {
1989
2614
  const handler = parentRegistry.getHandler(GraphEvents.ON_SUBAGENT_UPDATE);
1990
2615
  if (!handler) {
1991
- return;
1992
- }
1993
- try {
1994
- const event: SubagentUpdateEvent = {
1995
- runId: parentRunId,
1996
- subagentRunId: childRunId,
1997
- subagentType,
1998
- subagentAgentId,
1999
- parentAgentId,
2000
- parentToolCallId,
2001
- phase,
2002
- data: sanitizeForwardedSubagentUpdateData(eventName, data),
2003
- label: summarizeEvent(eventName, data),
2004
- timestamp: new Date().toISOString(),
2005
- };
2006
- await handler.handle(GraphEvents.ON_SUBAGENT_UPDATE, event);
2007
- } catch {
2008
- /* observational — swallow */
2616
+ return true;
2009
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);
2010
2636
  };
2011
2637
 
2012
2638
  const queuedUpdates: QueuedSubagentUpdate[] = [];
2013
2639
  let drainPromise: Promise<void> | undefined;
2640
+ let queueOpen = true;
2014
2641
 
2015
2642
  const enqueue = (update: QueuedSubagentUpdate): void => {
2016
- if (queuedUpdates.length >= MAX_PENDING_SUBAGENT_UPDATES) {
2643
+ if (!queueOpen) {
2644
+ return;
2645
+ }
2646
+ if (queuedUpdates.length >= MAX_QUEUED_SUBAGENT_UPDATES) {
2017
2647
  const dropIndex = queuedUpdates.findIndex((queued) =>
2018
- isDroppableSubagentUpdatePhase(queued.phase)
2648
+ isLowPrioritySubagentUpdatePhase(queued.phase)
2019
2649
  );
2020
2650
  if (dropIndex >= 0) {
2021
2651
  queuedUpdates.splice(dropIndex, 1);
2022
- } else if (isDroppableSubagentUpdatePhase(update.phase)) {
2652
+ } else if (isLowPrioritySubagentUpdatePhase(update.phase)) {
2023
2653
  return;
2654
+ } else {
2655
+ queuedUpdates.shift();
2024
2656
  }
2025
2657
  }
2026
2658
  queuedUpdates.push(update);
@@ -2037,7 +2669,16 @@ export class SubagentExecutor {
2037
2669
  if (update == null) {
2038
2670
  continue;
2039
2671
  }
2040
- 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
+ }
2041
2682
  }
2042
2683
  })();
2043
2684
  try {
@@ -2053,17 +2694,23 @@ export class SubagentExecutor {
2053
2694
  const scheduleWrap = (
2054
2695
  eventName: string,
2055
2696
  phase: SubagentUpdatePhase,
2056
- data: unknown
2697
+ data: unknown,
2698
+ memberAgentId?: string
2057
2699
  ): void => {
2058
- enqueue({ eventName, phase, data });
2700
+ enqueue({ eventName, phase, data, memberAgentId });
2059
2701
  void drain();
2060
2702
  };
2061
2703
 
2062
2704
  const handler = BaseCallbackHandler.fromMethods({
2063
2705
  [Callback.CUSTOM_EVENT]: async (
2064
2706
  eventName: string,
2065
- data: unknown
2707
+ data: unknown,
2708
+ _runId?: string,
2709
+ _tags?: string[],
2710
+ metadata?: Record<string, unknown>
2066
2711
  ): Promise<void> => {
2712
+ const memberAgentId =
2713
+ asNonEmptyString(metadata?.agentId) ?? getEventAgentId(data);
2067
2714
  if (eventName === GraphEvents.ON_TOOL_EXECUTE) {
2068
2715
  const toolHandler = parentRegistry.getHandler(
2069
2716
  GraphEvents.ON_TOOL_EXECUTE
@@ -2078,28 +2725,28 @@ export class SubagentExecutor {
2078
2725
  * We also surface a short notice in the subagent-update stream so
2079
2726
  * the UI can show "calling <tool>" for each tool the child spawns.
2080
2727
  */
2081
- scheduleWrap(eventName, 'run_step', data);
2728
+ scheduleWrap(eventName, 'run_step', data, memberAgentId);
2082
2729
  return;
2083
2730
  }
2084
2731
 
2085
2732
  if (eventName === GraphEvents.ON_RUN_STEP) {
2086
- scheduleWrap(eventName, 'run_step', data);
2733
+ scheduleWrap(eventName, 'run_step', data, memberAgentId);
2087
2734
  return;
2088
2735
  }
2089
2736
  if (eventName === GraphEvents.ON_RUN_STEP_DELTA) {
2090
- scheduleWrap(eventName, 'run_step_delta', data);
2737
+ scheduleWrap(eventName, 'run_step_delta', data, memberAgentId);
2091
2738
  return;
2092
2739
  }
2093
2740
  if (eventName === GraphEvents.ON_RUN_STEP_COMPLETED) {
2094
- scheduleWrap(eventName, 'run_step_completed', data);
2741
+ scheduleWrap(eventName, 'run_step_completed', data, memberAgentId);
2095
2742
  return;
2096
2743
  }
2097
2744
  if (eventName === GraphEvents.ON_MESSAGE_DELTA) {
2098
- scheduleWrap(eventName, 'message_delta', data);
2745
+ scheduleWrap(eventName, 'message_delta', data, memberAgentId);
2099
2746
  return;
2100
2747
  }
2101
2748
  if (eventName === GraphEvents.ON_REASONING_DELTA) {
2102
- scheduleWrap(eventName, 'reasoning_delta', data);
2749
+ scheduleWrap(eventName, 'reasoning_delta', data, memberAgentId);
2103
2750
  return;
2104
2751
  }
2105
2752
  },
@@ -2134,29 +2781,25 @@ export class SubagentExecutor {
2134
2781
  function createUsageCaptureHandler(args: {
2135
2782
  sink: SubagentUsageSink;
2136
2783
  subagentType: string;
2784
+ subagentKind: 'agent' | 'graph';
2137
2785
  subagentRunId: string;
2138
2786
  subagentAgentId: string;
2139
2787
  parentRunId: string;
2140
- /**
2141
- * Child config's provider enum — the default tag when a call carries no
2142
- * `INVOKED_PROVIDER` metadata (hosts key pricing/cache semantics off it).
2143
- */
2144
- provider?: string;
2145
- /**
2146
- * Child config's model, used when a call carries neither `ls_model_name`
2147
- * nor `INVOKED_MODEL` metadata.
2148
- */
2149
- fallbackModel?: string;
2788
+ executionContext: SubagentExecutionContext;
2789
+ memberInputs: ReadonlyMap<string, AgentInputs>;
2150
2790
  }): BaseCallbackHandler {
2151
2791
  const {
2152
2792
  sink,
2153
2793
  subagentType,
2794
+ subagentKind,
2154
2795
  subagentRunId,
2155
2796
  subagentAgentId,
2156
2797
  parentRunId,
2157
- provider,
2158
- fallbackModel,
2798
+ executionContext,
2799
+ memberInputs,
2159
2800
  } = args;
2801
+ const defaultMember =
2802
+ memberInputs.size === 1 ? memberInputs.entries().next().value : undefined;
2160
2803
  /**
2161
2804
  * Per-call attribution keyed by LangChain callback runId. `model` joins
2162
2805
  * `ls_model_name` (provider-reported) with `INVOKED_MODEL` (stamped by
@@ -2168,7 +2811,7 @@ function createUsageCaptureHandler(args: {
2168
2811
  */
2169
2812
  const callInfoByCallId = new Map<
2170
2813
  string,
2171
- { model?: string; provider?: string }
2814
+ { model?: string; provider?: string; memberAgentId?: string }
2172
2815
  >();
2173
2816
  const handler = BaseCallbackHandler.fromMethods({
2174
2817
  handleChatModelStart: (
@@ -2186,18 +2829,30 @@ function createUsageCaptureHandler(args: {
2186
2829
  const callProvider = asNonEmptyString(
2187
2830
  metadata?.[Constants.INVOKED_PROVIDER]
2188
2831
  );
2189
- if (callModel != null || callProvider != null) {
2832
+ const memberAgentId = asNonEmptyString(metadata?.agentId);
2833
+ if (callModel != null || callProvider != null || memberAgentId != null) {
2190
2834
  callInfoByCallId.set(runId, {
2191
2835
  model: callModel,
2192
2836
  provider: callProvider,
2837
+ memberAgentId,
2193
2838
  });
2194
2839
  }
2195
2840
  },
2196
2841
  handleLLMEnd: async (output: LLMResult, runId: string): Promise<void> => {
2197
2842
  const callInfo = callInfoByCallId.get(runId);
2198
2843
  callInfoByCallId.delete(runId);
2199
- const model = callInfo?.model ?? fallbackModel;
2200
- 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;
2201
2856
  for (const generationGroup of output.generations) {
2202
2857
  /**
2203
2858
  * At most ONE event per generation group: each group is one
@@ -2227,9 +2882,14 @@ function createUsageCaptureHandler(args: {
2227
2882
  model,
2228
2883
  provider: callProvider,
2229
2884
  subagentType,
2885
+ subagentKind,
2230
2886
  subagentRunId,
2231
2887
  subagentAgentId,
2232
- runId: parentRunId,
2888
+ memberAgentId,
2889
+ parentRunId,
2890
+ depth: executionContext.depth,
2891
+ ancestry: executionContext.ancestry,
2892
+ runId: executionContext.rootRunId,
2233
2893
  });
2234
2894
  } catch {
2235
2895
  /* observational — a throwing/rejecting host sink must not break the child run */
@@ -2255,6 +2915,19 @@ function asNonEmptyString(value: unknown): string | undefined {
2255
2915
  return typeof value === 'string' && value !== '' ? value : undefined;
2256
2916
  }
2257
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
+
2258
2931
  /**
2259
2932
  * Best-effort read of the configured model from a subagent's client
2260
2933
  * options. Providers disagree on the key (`model` vs `modelName`), and the
@@ -2276,6 +2949,63 @@ function extractConfiguredModel(agentInputs: AgentInputs): string | undefined {
2276
2949
  return undefined;
2277
2950
  }
2278
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
+
2279
3009
  function sanitizeChildConfigurable(
2280
3010
  parentConfigurable: Record<string, unknown> | undefined
2281
3011
  ): Record<string, unknown> {
@@ -2296,6 +3026,7 @@ function isLangGraphRuntimeConfigKey(key: string): boolean {
2296
3026
  key === SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY ||
2297
3027
  key === SUBAGENT_RESUME_MANIFEST_CONFIG_KEY ||
2298
3028
  key === SUBAGENT_PARENT_BATCH_CONFIG_KEY ||
3029
+ key === TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY ||
2299
3030
  /** The parent batch's breaker scope must not leak into the child
2300
3031
  * workflow's configurable — children own separate controllers. */
2301
3032
  key === RUN_BREAKER_SCOPE_CONFIG_KEY
@@ -2327,7 +3058,7 @@ export function sanitizeForwardedSubagentUpdateData(
2327
3058
  return undefined;
2328
3059
  }
2329
3060
 
2330
- function isDroppableSubagentUpdatePhase(phase: SubagentUpdatePhase): boolean {
3061
+ function isLowPrioritySubagentUpdatePhase(phase: SubagentUpdatePhase): boolean {
2331
3062
  return (
2332
3063
  phase === 'message_delta' ||
2333
3064
  phase === 'reasoning_delta' ||
@@ -2643,6 +3374,24 @@ export function summarizeEvent(eventName: string, data: unknown): string {
2643
3374
  return eventName;
2644
3375
  }
2645
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
+
2646
3395
  /**
2647
3396
  * Walk messages from last to first, returning the text content of the most
2648
3397
  * recent AIMessage that has any. Non-text blocks (tool_use, thinking,
@@ -2741,113 +3490,46 @@ export function filterSubagentResult(messages: BaseMessage[]): string {
2741
3490
  return 'Task completed';
2742
3491
  }
2743
3492
 
2744
- /**
2745
- * Resolve self-spawn configs by filling in agentInputs from the parent context.
2746
- * Returns configs with agentInputs guaranteed present. Throws on duplicate
2747
- * `type` values to prevent silent config shadowing.
2748
- */
2749
- export function resolveSubagentConfigs(
2750
- configs: SubagentConfig[],
2751
- parentContext: AgentContext
2752
- ): ResolvedSubagentConfig[] {
2753
- const resolved = configs
2754
- .map((config) => {
2755
- if (config.agentInputs != null) {
2756
- return config as ResolvedSubagentConfig;
2757
- }
2758
- if (config.self !== true || parentContext._sourceInputs == null) {
2759
- return null;
2760
- }
2761
- return {
2762
- ...config,
2763
- agentInputs: { ...parentContext._sourceInputs },
2764
- } as ResolvedSubagentConfig;
2765
- })
2766
- .filter((c): c is ResolvedSubagentConfig => c != null);
2767
-
2768
- const seenTypes = new Set<string>();
2769
- for (const config of resolved) {
2770
- if (seenTypes.has(config.type)) {
2771
- throw new Error(
2772
- `Duplicate subagent type "${config.type}". Each SubagentConfig must have a unique "type" field.`
2773
- );
2774
- }
2775
- 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;
2776
3497
  }
2777
-
2778
- return resolved;
3498
+ return `${message.slice(0, ERROR_MESSAGE_MAX_CHARS)}...`;
2779
3499
  }
2780
3500
 
2781
- /**
2782
- * Build child AgentInputs from a resolved config, stripping nesting and
2783
- * (optionally) event-driven fields. When `allowNested: true`, the child's
2784
- * `maxSubagentDepth` is decremented so that depth is consumed as the call
2785
- * chain deepens across graph boundaries — the parent's executor-level check
2786
- * alone cannot see into the child graph's separate executor.
2787
- *
2788
- * When `keepToolDefinitions` is `true`, the child retains the parent's
2789
- * `toolDefinitions` so event-driven tools remain usable. This is only safe
2790
- * when the caller has wired a forwarder for `ON_TOOL_EXECUTE` to a
2791
- * registered handler — otherwise the child will hang on tool dispatch.
2792
- *
2793
- * @remarks Advanced utility: exported primarily for testing and by
2794
- * {@link SubagentExecutor}. Host applications configuring subagents should
2795
- * not need to call this directly — it is invoked internally when a subagent
2796
- * tool is dispatched. The depth-countdown contract (parent's `maxDepth` in,
2797
- * child's decremented `maxSubagentDepth` on the returned inputs) is the
2798
- * mechanism that bounds nesting across graph boundaries; callers must
2799
- * respect it.
2800
- */
2801
- export function buildChildInputs(
2802
- config: ResolvedSubagentConfig,
2803
- childAgentId: string,
2804
- parentMaxDepth: number,
2805
- keepToolDefinitions: boolean = false
2806
- ): AgentInputs {
2807
- const { agentInputs } = config;
2808
- const childInputs: AgentInputs = {
2809
- ...agentInputs,
2810
- agentId: childAgentId,
2811
- toolDefinitions: keepToolDefinitions
2812
- ? agentInputs.toolDefinitions
2813
- : undefined,
2814
- /**
2815
- * Subagents run in an isolated context by contract. Parent-run-scoped
2816
- * fields that would otherwise survive the shallow-spread clone — the
2817
- * cross-run conversation summary and the prior-turn tool-discovery
2818
- * set — are cleared here so the child starts fresh. Host applications
2819
- * that want a subagent to see parent context must thread it in
2820
- * explicitly (e.g. via the `description` argument to the subagent
2821
- * tool), not via inherited state.
2822
- */
2823
- initialSummary: undefined,
2824
- discoveredTools: undefined,
2825
- /**
2826
- * Host-supplied direct tools are scrubbed from INHERITED configs only.
2827
- * A self-spawn config's `agentInputs` is a shallow spread of the parent's
2828
- * `_sourceInputs`, so without this a parent-scoped graph tool (e.g. an
2829
- * interrupt-raising ask_user_question) would silently become available to
2830
- * the child. An EXPLICIT child config that lists its own `graphTools` is a
2831
- * deliberate host choice and keeps them (Codex #289 P2); with HITL enabled,
2832
- * those tools use the shared checkpointer and can pause and resume safely.
2833
- */
2834
- graphTools: config.self === true ? undefined : agentInputs.graphTools,
2835
- };
2836
-
2837
- if (config.allowNested === true) {
2838
- childInputs.maxSubagentDepth = Math.max(0, parentMaxDepth - 1);
2839
- } else {
2840
- childInputs.subagentConfigs = undefined;
2841
- 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));
2842
3507
  }
2843
3508
 
2844
- 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
+ });
2845
3529
  }
2846
3530
 
2847
- function truncateErrorMessage(error: unknown): string {
2848
- const message = error instanceof Error ? error.message : String(error);
2849
- if (message.length <= ERROR_MESSAGE_MAX_CHARS) {
2850
- return message;
2851
- }
2852
- 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.');
2853
3535
  }