@librechat/agents 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/cjs/graphs/Graph.cjs +111 -27
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs.map +1 -1
  8. package/dist/cjs/hooks/types.cjs +2 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/llm/init.cjs +3 -3
  11. package/dist/cjs/llm/invoke.cjs +2 -2
  12. package/dist/cjs/main.cjs +14 -13
  13. package/dist/cjs/messages/format.cjs +40 -23
  14. package/dist/cjs/messages/format.cjs.map +1 -1
  15. package/dist/cjs/run.cjs +82 -15
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/session/AgentSession.cjs +37 -2
  18. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  19. package/dist/cjs/stream.cjs +1 -1
  20. package/dist/cjs/summarization/node.cjs +6 -3
  21. package/dist/cjs/summarization/node.cjs.map +1 -1
  22. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  23. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  24. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  25. package/dist/cjs/tools/ToolNode.cjs +316 -94
  26. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  28. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +630 -78
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  30. package/dist/cjs/tools/subagent/SubagentReplay.cjs +175 -0
  31. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  32. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  33. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  34. package/dist/cjs/utils/index.cjs +2 -2
  35. package/dist/esm/graphs/Graph.mjs +111 -27
  36. package/dist/esm/graphs/Graph.mjs.map +1 -1
  37. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  38. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  39. package/dist/esm/hooks/executeHooks.mjs +60 -14
  40. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  41. package/dist/esm/hooks/index.mjs.map +1 -1
  42. package/dist/esm/hooks/types.mjs +2 -1
  43. package/dist/esm/hooks/types.mjs.map +1 -1
  44. package/dist/esm/llm/init.mjs +1 -1
  45. package/dist/esm/llm/invoke.mjs +2 -2
  46. package/dist/esm/main.mjs +11 -11
  47. package/dist/esm/messages/format.mjs +40 -23
  48. package/dist/esm/messages/format.mjs.map +1 -1
  49. package/dist/esm/run.mjs +83 -16
  50. package/dist/esm/run.mjs.map +1 -1
  51. package/dist/esm/session/AgentSession.mjs +37 -2
  52. package/dist/esm/session/AgentSession.mjs.map +1 -1
  53. package/dist/esm/stream.mjs +1 -1
  54. package/dist/esm/summarization/node.mjs +6 -3
  55. package/dist/esm/summarization/node.mjs.map +1 -1
  56. package/dist/esm/tools/BashExecutor.mjs +1 -1
  57. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  58. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +317 -95
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/ToolSearch.mjs +1 -1
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs +631 -79
  63. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  64. package/dist/esm/tools/subagent/SubagentReplay.mjs +168 -0
  65. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  66. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  67. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  68. package/dist/esm/utils/index.mjs +2 -2
  69. package/dist/types/graphs/Graph.d.ts +16 -1
  70. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  71. package/dist/types/hooks/executeHooks.d.ts +5 -1
  72. package/dist/types/hooks/index.d.ts +2 -2
  73. package/dist/types/hooks/types.d.ts +12 -0
  74. package/dist/types/run.d.ts +4 -1
  75. package/dist/types/session/AgentSession.d.ts +1 -0
  76. package/dist/types/tools/ToolNode.d.ts +23 -15
  77. package/dist/types/tools/subagent/SubagentExecutor.d.ts +46 -7
  78. package/dist/types/tools/subagent/SubagentReplay.d.ts +81 -0
  79. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  80. package/dist/types/types/graph.d.ts +5 -5
  81. package/dist/types/types/hitl.d.ts +15 -0
  82. package/package.json +6 -2
  83. package/src/graphs/Graph.ts +246 -52
  84. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  85. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  86. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  87. package/src/hooks/HookRegistry.ts +151 -1
  88. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  89. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  90. package/src/hooks/executeHooks.ts +133 -15
  91. package/src/hooks/index.ts +3 -1
  92. package/src/hooks/types.ts +16 -0
  93. package/src/messages/format.ts +54 -26
  94. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  95. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  96. package/src/messages/formatAgentMessages.test.ts +7 -2
  97. package/src/run.ts +164 -18
  98. package/src/session/AgentSession.ts +52 -2
  99. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  100. package/src/summarization/__tests__/node.test.ts +32 -14
  101. package/src/summarization/node.ts +15 -11
  102. package/src/tools/ToolNode.ts +631 -157
  103. package/src/tools/__tests__/SubagentExecutor.test.ts +542 -6
  104. package/src/tools/__tests__/SubagentReplay.test.ts +300 -0
  105. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  106. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +486 -2
  107. package/src/tools/__tests__/hitl.test.ts +234 -5
  108. package/src/tools/__tests__/subagentHooks.test.ts +1040 -62
  109. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  110. package/src/tools/subagent/SubagentExecutor.ts +1369 -131
  111. package/src/tools/subagent/SubagentReplay.ts +575 -0
  112. package/src/tools/toolOutputReferences.ts +40 -1
  113. package/src/types/graph.ts +5 -5
  114. package/src/types/hitl.ts +16 -0
@@ -1,11 +1,15 @@
1
+ import { SUBAGENT_PARENT_BATCH_CONFIG_KEY, SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY, SUBAGENT_RESUME_MANIFEST_CONFIG_KEY, attachSubagentResumeManifest, getSubagentResumeManifest } from "./SubagentReplay.mjs";
1
2
  import "../../common/enum.mjs";
2
3
  import "../../common/index.mjs";
3
4
  import { StreamLimitExceededError } from "../../llm/streamLimits.mjs";
4
5
  import { executeHooks } from "../../hooks/executeHooks.mjs";
6
+ import { TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from "../../hooks/types.mjs";
5
7
  import "../../hooks/index.mjs";
6
8
  import { nanoid } from "nanoid";
7
9
  import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
8
- import { HumanMessage } from "@langchain/core/messages";
10
+ import { AIMessage, BaseMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
11
+ import { Command, END, GraphInterrupt, INTERRUPT, MessagesAnnotation, START, StateGraph, copyCheckpoint, isGraphInterrupt, isInterrupted } from "@langchain/langgraph";
12
+ import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
9
13
  //#region src/tools/subagent/SubagentExecutor.ts
10
14
  const DEFAULT_MAX_TURNS = 25;
11
15
  const RECURSION_MULTIPLIER = 3;
@@ -18,11 +22,185 @@ const HOOK_FALLBACK = Object.freeze({
18
22
  errors: []
19
23
  });
20
24
  const LANGGRAPH_RUNTIME_CONFIG_PREFIX = "__pregel_";
25
+ const LANGGRAPH_RESUME_MAP_CONFIG_KEY = "__pregel_resume_map";
21
26
  const LANGGRAPH_CHECKPOINT_CONFIG_KEYS = new Set([
22
27
  "checkpoint_id",
23
28
  "checkpoint_map",
24
29
  "checkpoint_ns"
25
30
  ]);
31
+ const SUBAGENT_CHECKPOINT_MARKER_KEY = "__librechat_subagent_checkpoint";
32
+ const SUBAGENT_HOOK_SESSION_KEY = "__librechat_subagent_hook_session";
33
+ const SUBAGENT_RUN_ID_KEY = "__librechat_subagent_run_id";
34
+ const SUBAGENT_REPLAY_NODE = "subagent-replay";
35
+ function isCheckpointSaver(value) {
36
+ if (value == null || typeof value !== "object") return false;
37
+ const candidate = value;
38
+ return typeof candidate.getTuple === "function" && typeof candidate.list === "function" && typeof candidate.put === "function" && typeof candidate.putWrites === "function" && typeof candidate.deleteThread === "function";
39
+ }
40
+ function isSubagentCheckpointMarker(value) {
41
+ if (value == null || typeof value !== "object") return false;
42
+ const marker = value;
43
+ return marker.version === 1 && marker.lifecycleComplete === true && typeof marker.parentToolCallId === "string" && (marker.hookSessionId == null || typeof marker.hookSessionId === "string") && (marker.childRunId == null || typeof marker.childRunId === "string") && (marker.settledOutput == null || isPersistedToolOutput(marker.settledOutput));
44
+ }
45
+ function isPersistedToolOutput(value) {
46
+ if (value == null || typeof value !== "object") return false;
47
+ const output = value;
48
+ const contentIsValid = typeof output.content === "string" || Array.isArray(output.content);
49
+ const statusIsValid = output.status == null || output.status === "success" || output.status === "error";
50
+ const contextsAreValid = Array.isArray(output.additionalContexts) && output.additionalContexts.every((context) => typeof context === "string");
51
+ const resolvedArgsAreValid = output.resolvedArgs == null || typeof output.resolvedArgs === "object" && !Array.isArray(output.resolvedArgs);
52
+ return contentIsValid && typeof output.toolCallId === "string" && output.additionalKwargs != null && typeof output.additionalKwargs === "object" && output.responseMetadata != null && typeof output.responseMetadata === "object" && statusIsValid && contextsAreValid && resolvedArgsAreValid && (output.referenceContent == null || typeof output.referenceContent === "string");
53
+ }
54
+ function getSubagentCheckpointMarker(messages, parentToolCallId) {
55
+ for (let i = messages.length - 1; i >= 0; i--) {
56
+ const marker = messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
57
+ if (isSubagentCheckpointMarker(marker) && marker.parentToolCallId === parentToolCallId) return marker;
58
+ }
59
+ }
60
+ function getSubagentHookSessionId(messages) {
61
+ for (let i = messages.length - 1; i >= 0; i--) {
62
+ const sessionId = messages[i].additional_kwargs[SUBAGENT_HOOK_SESSION_KEY];
63
+ if (typeof sessionId === "string" && sessionId.length > 0) return sessionId;
64
+ }
65
+ }
66
+ function getSubagentRunId(messages) {
67
+ for (let i = messages.length - 1; i >= 0; i--) {
68
+ const runId = messages[i].additional_kwargs[SUBAGENT_RUN_ID_KEY];
69
+ if (typeof runId === "string" && runId.length > 0) return runId;
70
+ const marker = messages[i].additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY];
71
+ if (isSubagentCheckpointMarker(marker) && marker.childRunId != null && marker.childRunId.length > 0) return marker.childRunId;
72
+ }
73
+ }
74
+ function isSubagentCheckpointMarkerMessage(message) {
75
+ return isSubagentCheckpointMarker(message.additional_kwargs[SUBAGENT_CHECKPOINT_MARKER_KEY]);
76
+ }
77
+ function createSubagentCheckpointMarkerMessage(marker) {
78
+ return new AIMessage({
79
+ content: "",
80
+ additional_kwargs: { [SUBAGENT_CHECKPOINT_MARKER_KEY]: marker }
81
+ });
82
+ }
83
+ function getCheckpointMessages(value) {
84
+ return Array.isArray(value) ? value.filter(BaseMessage.isInstance) : [];
85
+ }
86
+ function getConfigurableString(config, key) {
87
+ const value = config?.configurable?.[key];
88
+ return typeof value === "string" && value.length > 0 ? value : void 0;
89
+ }
90
+ function getTupleMessages(tuple) {
91
+ return getCheckpointMessages(tuple?.checkpoint.channel_values.messages);
92
+ }
93
+ function getCheckpointReference(tuple) {
94
+ const configurable = tuple?.config.configurable;
95
+ const threadId = configurable?.thread_id;
96
+ const checkpointId = configurable?.checkpoint_id;
97
+ const checkpointNs = configurable?.checkpoint_ns ?? "";
98
+ if (typeof threadId !== "string" || threadId.length === 0 || typeof checkpointId !== "string" || checkpointId.length === 0 || typeof checkpointNs !== "string") return;
99
+ return {
100
+ threadId,
101
+ checkpointId,
102
+ checkpointNs
103
+ };
104
+ }
105
+ function serializeToolOutput(settled) {
106
+ const { output } = settled;
107
+ return {
108
+ content: output.content,
109
+ toolCallId: output.tool_call_id,
110
+ ...output.id == null ? {} : { id: output.id },
111
+ ...output.name == null ? {} : { name: output.name },
112
+ ...output.status == null ? {} : { status: output.status },
113
+ additionalKwargs: output.additional_kwargs,
114
+ responseMetadata: output.response_metadata,
115
+ ...output.metadata == null ? {} : { metadata: output.metadata },
116
+ additionalContexts: settled.additionalContexts,
117
+ ...settled.resolvedArgs == null ? {} : { resolvedArgs: settled.resolvedArgs },
118
+ ...settled.referenceContent == null ? {} : { referenceContent: settled.referenceContent }
119
+ };
120
+ }
121
+ function deserializeToolOutput(output) {
122
+ return {
123
+ output: new ToolMessage({
124
+ content: output.content,
125
+ tool_call_id: output.toolCallId,
126
+ ...output.id == null ? {} : { id: output.id },
127
+ ...output.name == null ? {} : { name: output.name },
128
+ ...output.status == null ? {} : { status: output.status },
129
+ additional_kwargs: output.additionalKwargs,
130
+ response_metadata: output.responseMetadata,
131
+ ...output.metadata == null ? {} : { metadata: output.metadata }
132
+ }),
133
+ additionalContexts: output.additionalContexts,
134
+ ...output.resolvedArgs == null ? {} : { resolvedArgs: output.resolvedArgs },
135
+ ...output.referenceContent == null ? {} : { referenceContent: output.referenceContent }
136
+ };
137
+ }
138
+ function getParentCheckpointFork(configurable) {
139
+ const checkpointId = configurable?.checkpoint_id;
140
+ return typeof checkpointId === "string" && checkpointId.length > 0 ? checkpointId : "root";
141
+ }
142
+ function getResumeAttemptId(configurable, fallback) {
143
+ const attemptId = configurable?.[SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY];
144
+ return typeof attemptId === "string" && attemptId.length > 0 ? attemptId : fallback;
145
+ }
146
+ function getApprovalExecutionScope(childRunId, resumeAttemptId) {
147
+ return `subagent-approval:${Buffer.from(JSON.stringify([childRunId, resumeAttemptId])).toString("base64url")}`;
148
+ }
149
+ function getChildThreadId(args) {
150
+ const durableParentId = args.threadId ?? args.parentRunId;
151
+ const parentFork = getParentCheckpointFork(args.parentConfigurable);
152
+ const parentBatch = args.parentConfigurable?.[SUBAGENT_PARENT_BATCH_CONFIG_KEY];
153
+ const identity = [
154
+ durableParentId,
155
+ parentFork,
156
+ args.parentAgentId ?? "agent",
157
+ args.parentToolCallId,
158
+ typeof parentBatch === "string" && parentBatch.length > 0 ? parentBatch : "batch"
159
+ ];
160
+ if (args.branchId != null) identity.push(args.branchId);
161
+ return `subagent:${Buffer.from(JSON.stringify(identity)).toString("base64url")}`;
162
+ }
163
+ function isToolApprovalPayload(value) {
164
+ return value != null && typeof value === "object" && value.type === "tool_approval";
165
+ }
166
+ function addSubagentScope(interrupts, scope, resumeManifest) {
167
+ return interrupts.map((childInterrupt) => {
168
+ const payload = isToolApprovalPayload(childInterrupt.value) ? {
169
+ ...childInterrupt.value,
170
+ subagent: childInterrupt.value.subagent ?? scope
171
+ } : childInterrupt.value;
172
+ return {
173
+ ...childInterrupt,
174
+ value: resumeManifest == null ? payload : attachSubagentResumeManifest(payload, resumeManifest)
175
+ };
176
+ });
177
+ }
178
+ function getChildResumeMap(pendingInterrupts, parentConfigurable) {
179
+ const resumeMap = parentConfigurable?.[LANGGRAPH_RESUME_MAP_CONFIG_KEY];
180
+ if (resumeMap == null || typeof resumeMap !== "object") return;
181
+ const parentResumeMap = resumeMap;
182
+ const childResumeMap = {};
183
+ for (const childInterrupt of pendingInterrupts) {
184
+ const interruptId = childInterrupt.id;
185
+ if (typeof interruptId === "string" && Object.prototype.hasOwnProperty.call(parentResumeMap, interruptId)) childResumeMap[interruptId] = parentResumeMap[interruptId];
186
+ }
187
+ return Object.keys(childResumeMap).length > 0 ? childResumeMap : void 0;
188
+ }
189
+ function getPersistedInterrupts(snapshot) {
190
+ const interrupts = [];
191
+ for (const task of snapshot.tasks) for (const pendingInterrupt of task.interrupts) interrupts.push(pendingInterrupt);
192
+ return interrupts;
193
+ }
194
+ function getPersistedMessages(snapshot) {
195
+ if (snapshot.values == null || typeof snapshot.values !== "object") return;
196
+ const values = snapshot.values;
197
+ if (!Array.isArray(values.messages) || values.messages.length === 0) return;
198
+ const messages = values.messages.filter((message) => !isSubagentCheckpointMarkerMessage(message));
199
+ return messages.length > 0 ? messages : void 0;
200
+ }
201
+ function createReplayCheckpointWorkflow(checkpointer) {
202
+ return new StateGraph(MessagesAnnotation).addNode(SUBAGENT_REPLAY_NODE, (state) => state).addEdge(START, SUBAGENT_REPLAY_NODE).addEdge(SUBAGENT_REPLAY_NODE, END).compile({ checkpointer });
203
+ }
26
204
  var SubagentExecutor = class {
27
205
  configs;
28
206
  parentSignal;
@@ -40,9 +218,18 @@ var SubagentExecutor = class {
40
218
  langfuse;
41
219
  tokenCounter;
42
220
  streamLimits;
221
+ humanInTheLoop;
222
+ checkpointer;
43
223
  maxDepth;
44
224
  createChildGraph;
45
225
  usageSink;
226
+ checkpointThreadIds = /* @__PURE__ */ new Set();
227
+ startedChildRuns = /* @__PURE__ */ new Set();
228
+ completedChildRuns = /* @__PURE__ */ new Set();
229
+ completedChildResults = /* @__PURE__ */ new Map();
230
+ childExecutionIdentities = /* @__PURE__ */ new Map();
231
+ activeChildRuns = /* @__PURE__ */ new Map();
232
+ replayCheckpointWorkflow;
46
233
  resolveParentHandlerRegistry;
47
234
  constructor(options) {
48
235
  this.configs = options.configs;
@@ -54,6 +241,8 @@ var SubagentExecutor = class {
54
241
  this.langfuse = options.langfuse;
55
242
  this.tokenCounter = options.tokenCounter;
56
243
  this.streamLimits = options.streamLimits;
244
+ this.humanInTheLoop = options.humanInTheLoop;
245
+ this.checkpointer = isCheckpointSaver(options.checkpointer) ? options.checkpointer : void 0;
57
246
  this.maxDepth = options.maxDepth ?? 1;
58
247
  this.createChildGraph = options.createChildGraph;
59
248
  this.usageSink = options.usageSink;
@@ -78,6 +267,274 @@ var SubagentExecutor = class {
78
267
  getParentHandlerRegistry() {
79
268
  return this.resolveParentHandlerRegistry?.();
80
269
  }
270
+ /**
271
+ * Keeps the original child thread as an immutable resume source once a
272
+ * different parent Run reconstructs it. Each rebuilt parent gets a private
273
+ * checkpoint fork, while the persisted child run ID remains stable for
274
+ * activity and usage correlation across the interrupt boundary.
275
+ */
276
+ async resolveChildExecutionIdentity(params) {
277
+ const currentChildRunId = `${this.parentRunId}_sub_${params.parentToolCallId}`;
278
+ const resumeAttemptId = getResumeAttemptId(params.parentConfigurable, this.parentRunId);
279
+ const baseChildThreadId = getChildThreadId({
280
+ parentRunId: this.parentRunId,
281
+ parentAgentId: this.parentAgentId,
282
+ threadId: params.threadId,
283
+ parentToolCallId: params.parentToolCallId,
284
+ parentConfigurable: params.parentConfigurable
285
+ });
286
+ if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null) return {
287
+ childRunId: currentChildRunId,
288
+ childThreadId: baseChildThreadId,
289
+ approvalExecutionScope: currentChildRunId
290
+ };
291
+ const resumeExecution = getSubagentResumeManifest(params.parentConfigurable)?.executions.find((execution) => execution.parentToolCallId === params.parentToolCallId);
292
+ const branchChildThreadId = getChildThreadId({
293
+ parentRunId: this.parentRunId,
294
+ parentAgentId: this.parentAgentId,
295
+ threadId: params.threadId,
296
+ parentToolCallId: params.parentToolCallId,
297
+ parentConfigurable: params.parentConfigurable,
298
+ branchId: resumeAttemptId
299
+ });
300
+ if (resumeExecution != null) {
301
+ const approvalExecutionScope = getApprovalExecutionScope(resumeExecution.childRunId, resumeAttemptId);
302
+ await this.forkCheckpointSnapshot(resumeExecution.checkpoints, branchChildThreadId);
303
+ this.hookRegistry?.restorePendingToolApprovals(approvalExecutionScope, approvalExecutionScope, resumeExecution.approvalReplays);
304
+ this.checkpointThreadIds.add(branchChildThreadId);
305
+ return {
306
+ childRunId: resumeExecution.childRunId,
307
+ childThreadId: branchChildThreadId,
308
+ approvalExecutionScope,
309
+ resumeExecution
310
+ };
311
+ }
312
+ const branchTuple = await this.checkpointer.getTuple({ configurable: { thread_id: branchChildThreadId } });
313
+ if (branchTuple != null) {
314
+ const childRunId = getSubagentRunId(getTupleMessages(branchTuple)) ?? currentChildRunId;
315
+ this.checkpointThreadIds.add(branchChildThreadId);
316
+ return {
317
+ childRunId,
318
+ childThreadId: branchChildThreadId,
319
+ approvalExecutionScope: getApprovalExecutionScope(childRunId, resumeAttemptId)
320
+ };
321
+ }
322
+ const baseTuple = await this.checkpointer.getTuple({ configurable: { thread_id: baseChildThreadId } });
323
+ const persistedChildRunId = getSubagentRunId(getTupleMessages(baseTuple));
324
+ if (baseTuple == null || persistedChildRunId == null || persistedChildRunId === currentChildRunId) {
325
+ this.checkpointThreadIds.add(baseChildThreadId);
326
+ return {
327
+ childRunId: persistedChildRunId ?? currentChildRunId,
328
+ childThreadId: baseChildThreadId,
329
+ approvalExecutionScope: getApprovalExecutionScope(persistedChildRunId ?? currentChildRunId, resumeAttemptId)
330
+ };
331
+ }
332
+ const sourceCheckpoints = await this.getLatestCheckpointSnapshot(baseChildThreadId);
333
+ if (sourceCheckpoints.length === 0) throw new Error(`Cannot fork subagent checkpoint thread "${baseChildThreadId}" without a checkpoint ID.`);
334
+ await this.forkCheckpointSnapshot(sourceCheckpoints, branchChildThreadId);
335
+ this.checkpointThreadIds.add(branchChildThreadId);
336
+ return {
337
+ childRunId: persistedChildRunId,
338
+ childThreadId: branchChildThreadId,
339
+ approvalExecutionScope: getApprovalExecutionScope(persistedChildRunId, resumeAttemptId)
340
+ };
341
+ }
342
+ /** Captures one exact checkpoint head per namespace for a child thread. */
343
+ async getLatestCheckpointSnapshot(threadId) {
344
+ if (this.checkpointer == null) return [];
345
+ const checkpointsByNamespace = /* @__PURE__ */ new Map();
346
+ for await (const tuple of this.checkpointer.list({ configurable: { thread_id: threadId } })) {
347
+ const checkpoint = getCheckpointReference(tuple);
348
+ const current = checkpoint == null ? void 0 : checkpointsByNamespace.get(checkpoint.checkpointNs);
349
+ if (checkpoint != null && (current == null || checkpoint.checkpointId.localeCompare(current.checkpointId) > 0)) checkpointsByNamespace.set(checkpoint.checkpointNs, checkpoint);
350
+ }
351
+ return [...checkpointsByNamespace.values()].sort((left, right) => left.checkpointNs.localeCompare(right.checkpointNs));
352
+ }
353
+ /** Copies exact checkpoint lineages, including pending task writes. */
354
+ async forkCheckpointSnapshot(sources, targetThreadId) {
355
+ if (this.checkpointer == null || sources.length === 0 || sources.every((source) => source.threadId === targetThreadId)) return;
356
+ for (const source of sources) {
357
+ const tuples = [];
358
+ const visited = /* @__PURE__ */ new Set();
359
+ let tuple = await this.checkpointer.getTuple({ configurable: {
360
+ thread_id: source.threadId,
361
+ checkpoint_ns: source.checkpointNs,
362
+ checkpoint_id: source.checkpointId
363
+ } });
364
+ if (tuple == null) throw new Error(`Subagent checkpoint "${source.checkpointId}" is unavailable.`);
365
+ for (;;) {
366
+ const reference = getCheckpointReference(tuple);
367
+ if (reference == null) throw new Error("Subagent checkpoint lineage contains an invalid tuple.");
368
+ if (reference.threadId !== source.threadId || reference.checkpointNs !== source.checkpointNs) throw new Error("Subagent checkpoint lineage escapes its source namespace.");
369
+ const lineageKey = JSON.stringify([
370
+ reference.threadId,
371
+ reference.checkpointNs,
372
+ reference.checkpointId
373
+ ]);
374
+ if (visited.has(lineageKey)) throw new Error("Subagent checkpoint lineage contains a cycle.");
375
+ visited.add(lineageKey);
376
+ tuples.push(tuple);
377
+ if (tuple.parentConfig == null) break;
378
+ tuple = await this.checkpointer.getTuple(tuple.parentConfig);
379
+ if (tuple == null) throw new Error("Subagent checkpoint lineage is incomplete.");
380
+ }
381
+ tuples.reverse();
382
+ for (const lineageTuple of tuples) {
383
+ const checkpointNs = getConfigurableString(lineageTuple.config, "checkpoint_ns") ?? "";
384
+ const parentCheckpointId = getConfigurableString(lineageTuple.parentConfig, "checkpoint_id");
385
+ const storedConfig = await this.checkpointer.put({ configurable: {
386
+ thread_id: targetThreadId,
387
+ checkpoint_ns: checkpointNs,
388
+ ...parentCheckpointId == null ? {} : { checkpoint_id: parentCheckpointId }
389
+ } }, copyCheckpoint(lineageTuple.checkpoint), lineageTuple.metadata ?? {
390
+ source: "fork",
391
+ step: -1,
392
+ parents: {}
393
+ }, lineageTuple.checkpoint.channel_versions);
394
+ const writesByTask = /* @__PURE__ */ new Map();
395
+ for (const [taskId, channel, value] of lineageTuple.pendingWrites ?? []) {
396
+ let writes = writesByTask.get(taskId);
397
+ if (writes == null) {
398
+ writes = [];
399
+ writesByTask.set(taskId, writes);
400
+ }
401
+ writes.push([channel, value]);
402
+ }
403
+ for (const [taskId, writes] of writesByTask) await this.checkpointer.putWrites(storedConfig, writes, taskId);
404
+ }
405
+ }
406
+ }
407
+ async createResumeManifest(parentToolCallIds) {
408
+ if (this.checkpointer == null) return;
409
+ const executions = [];
410
+ for (const [parentToolCallId, identity] of this.childExecutionIdentities) {
411
+ if (parentToolCallIds != null && !parentToolCallIds.has(parentToolCallId)) continue;
412
+ const checkpoints = await this.getLatestCheckpointSnapshot(identity.childThreadId);
413
+ if (checkpoints.length === 0) continue;
414
+ const activeChildRun = this.activeChildRuns.get(identity.childThreadId);
415
+ const configuredHookSessionId = activeChildRun?.invokeConfig?.configurable?.run_id;
416
+ const hookSessionId = typeof configuredHookSessionId === "string" && configuredHookSessionId.length > 0 ? configuredHookSessionId : this.parentRunId;
417
+ const approvalReplays = this.hookRegistry?.snapshotPendingToolApprovals(identity.approvalExecutionScope, identity.approvalExecutionScope) ?? [];
418
+ const descendant = activeChildRun?.pendingInterrupts.map((pendingInterrupt) => getSubagentResumeManifest(pendingInterrupt.value)).find((manifest) => manifest != null);
419
+ const graphState = activeChildRun?.graph.createSubagentResumeState(hookSessionId) ?? {
420
+ toolCallSteps: [],
421
+ toolSessions: [],
422
+ toolNodes: [],
423
+ eagerToolUsage: [],
424
+ eagerToolSuppressions: []
425
+ };
426
+ executions.push({
427
+ parentToolCallId,
428
+ childRunId: identity.childRunId,
429
+ approvalExecutionScope: identity.approvalExecutionScope,
430
+ checkpoints,
431
+ graphState,
432
+ approvalReplays,
433
+ ...descendant == null ? {} : { descendant }
434
+ });
435
+ }
436
+ if (executions.length === 0) return;
437
+ return {
438
+ version: 1,
439
+ executions
440
+ };
441
+ }
442
+ getResumeManifest(parentToolCallIds) {
443
+ return this.createResumeManifest(parentToolCallIds);
444
+ }
445
+ getChildCheckpointThreadIds() {
446
+ const threadIds = new Set(this.checkpointThreadIds);
447
+ for (const activeChildRun of this.activeChildRuns.values()) for (const threadId of this.getGraphChildCheckpointThreadIds(activeChildRun.graph)) threadIds.add(threadId);
448
+ return [...threadIds];
449
+ }
450
+ resetCheckpointThreadIds() {
451
+ this.checkpointThreadIds.clear();
452
+ }
453
+ getGraphChildCheckpointThreadIds(graph) {
454
+ return graph.getChildCheckpointThreadIds?.() ?? [];
455
+ }
456
+ clearChildGraph(graph) {
457
+ for (const threadId of this.getGraphChildCheckpointThreadIds(graph)) this.checkpointThreadIds.add(threadId);
458
+ graph.clearHeavyState();
459
+ }
460
+ clearHeavyState() {
461
+ for (const activeChildRun of this.activeChildRuns.values()) this.clearChildGraph(activeChildRun.graph);
462
+ this.activeChildRuns.clear();
463
+ this.completedChildResults.clear();
464
+ for (const identity of this.childExecutionIdentities.values()) this.hookRegistry?.clearSession(identity.approvalExecutionScope);
465
+ this.childExecutionIdentities.clear();
466
+ this.startedChildRuns.clear();
467
+ this.completedChildRuns.clear();
468
+ this.replayCheckpointWorkflow = void 0;
469
+ }
470
+ async getSettledToolOutput(call, config) {
471
+ const parentToolCallId = call.id;
472
+ if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null || parentToolCallId == null || parentToolCallId === "") return;
473
+ const parentConfigurable = config.configurable;
474
+ const threadId = parentConfigurable?.thread_id;
475
+ const { childRunId, childThreadId, approvalExecutionScope } = await this.resolveChildExecutionIdentity({
476
+ threadId: typeof threadId === "string" ? threadId : void 0,
477
+ parentToolCallId,
478
+ parentConfigurable
479
+ });
480
+ this.childExecutionIdentities.set(parentToolCallId, {
481
+ childRunId,
482
+ childThreadId,
483
+ approvalExecutionScope
484
+ });
485
+ this.checkpointThreadIds.add(childThreadId);
486
+ const messages = getCheckpointMessages((await this.checkpointer.getTuple({ configurable: { thread_id: childThreadId } }))?.checkpoint.channel_values.messages);
487
+ const marker = getSubagentCheckpointMarker(messages, parentToolCallId);
488
+ const persistedHookSessionId = marker?.hookSessionId ?? getSubagentHookSessionId(messages);
489
+ const currentHookSessionId = parentConfigurable?.run_id;
490
+ if (persistedHookSessionId != null && typeof currentHookSessionId === "string" && currentHookSessionId.length > 0) this.hookRegistry?.copySession(persistedHookSessionId, currentHookSessionId);
491
+ return marker?.settledOutput == null ? void 0 : deserializeToolOutput(marker.settledOutput);
492
+ }
493
+ async persistSettledToolOutput(call, config, settled) {
494
+ const parentToolCallId = call.id;
495
+ if (this.humanInTheLoop?.enabled !== true || this.checkpointer == null || parentToolCallId == null || parentToolCallId === "") return;
496
+ const parentConfigurable = config.configurable;
497
+ const threadId = parentConfigurable?.thread_id;
498
+ const { childRunId, childThreadId, approvalExecutionScope } = await this.resolveChildExecutionIdentity({
499
+ threadId: typeof threadId === "string" ? threadId : void 0,
500
+ parentToolCallId,
501
+ parentConfigurable
502
+ });
503
+ this.childExecutionIdentities.set(parentToolCallId, {
504
+ childRunId,
505
+ childThreadId,
506
+ approvalExecutionScope
507
+ });
508
+ this.checkpointThreadIds.add(childThreadId);
509
+ const activeChildRun = this.activeChildRuns.get(childThreadId);
510
+ const persistedOutput = serializeToolOutput(settled);
511
+ if (activeChildRun != null) {
512
+ await this.persistChildCheckpointMarker(activeChildRun, parentToolCallId, persistedOutput);
513
+ this.clearChildGraph(activeChildRun.graph);
514
+ this.activeChildRuns.delete(childThreadId);
515
+ return;
516
+ }
517
+ this.replayCheckpointWorkflow ??= createReplayCheckpointWorkflow(this.checkpointer);
518
+ await this.replayCheckpointWorkflow.updateState({ configurable: { thread_id: childThreadId } }, { messages: [createSubagentCheckpointMarkerMessage({
519
+ version: 1,
520
+ parentToolCallId,
521
+ lifecycleComplete: true,
522
+ hookSessionId: typeof parentConfigurable?.run_id === "string" ? parentConfigurable.run_id : this.parentRunId,
523
+ childRunId,
524
+ settledOutput: persistedOutput
525
+ })] }, SUBAGENT_REPLAY_NODE);
526
+ }
527
+ async persistChildCheckpointMarker(activeChildRun, parentToolCallId, settledOutput) {
528
+ if (this.humanInTheLoop?.enabled !== true || activeChildRun.workflow.updateState == null || activeChildRun.invokeConfig == null) return;
529
+ await activeChildRun.workflow.updateState(activeChildRun.invokeConfig, { messages: [createSubagentCheckpointMarkerMessage({
530
+ version: 1,
531
+ parentToolCallId,
532
+ lifecycleComplete: true,
533
+ hookSessionId: typeof activeChildRun.invokeConfig.configurable?.run_id === "string" ? activeChildRun.invokeConfig.configurable.run_id : this.parentRunId,
534
+ childRunId: activeChildRun.childRunId,
535
+ ...settledOutput == null ? {} : { settledOutput }
536
+ })] }, activeChildRun.childAgentId);
537
+ }
81
538
  async execute(params) {
82
539
  const { description, subagentType, threadId, parentToolCallId } = params;
83
540
  /** Captured ONCE per execution, preferring the controller the parent
@@ -98,32 +555,25 @@ var SubagentExecutor = class {
98
555
  content: "Error: Maximum subagent nesting depth exceeded.",
99
556
  messages: []
100
557
  };
101
- const childAgentId = config.agentInputs.agentId || `${this.parentAgentId ?? "agent"}_sub_${nanoid(8)}`;
102
- if (this.hookRegistry?.hasHookFor("SubagentStart", this.parentRunId) === true) {
103
- const hookResult = await executeHooks({
104
- registry: this.hookRegistry,
105
- input: {
106
- hook_event_name: "SubagentStart",
107
- runId: this.parentRunId,
108
- threadId,
109
- parentAgentId: this.parentAgentId,
110
- agentId: childAgentId,
111
- agentType: subagentType,
112
- inputs: [new HumanMessage(description)]
113
- },
114
- sessionId: this.parentRunId,
115
- matchQuery: subagentType
116
- }).catch(() => HOOK_FALLBACK);
117
- /**
118
- * `ask` is treated identically to `deny` in the subagent context:
119
- * subagents are non-interactive, so there is no prompt path for `ask`.
120
- * Both decisions block execution and return a "Blocked" tool result.
121
- */
122
- if (hookResult.decision === "deny" || hookResult.decision === "ask") return {
123
- content: `Blocked: ${hookResult.reason ?? "Blocked by hook"}`,
124
- messages: []
125
- };
126
- }
558
+ if (this.humanInTheLoop?.enabled === true && (parentToolCallId == null || parentToolCallId === "")) return {
559
+ content: "Error: Resumable subagent execution requires a parent tool call ID.",
560
+ messages: []
561
+ };
562
+ const executionSuffix = parentToolCallId ?? nanoid(8);
563
+ const { childRunId, childThreadId, approvalExecutionScope, resumeExecution } = await this.resolveChildExecutionIdentity({
564
+ threadId,
565
+ parentToolCallId: executionSuffix,
566
+ parentConfigurable: params.parentConfigurable
567
+ });
568
+ this.childExecutionIdentities.set(executionSuffix, {
569
+ childRunId,
570
+ childThreadId,
571
+ approvalExecutionScope
572
+ });
573
+ const childExecutionKey = childThreadId;
574
+ const childAgentId = config.agentInputs.agentId || `${this.parentAgentId ?? "agent"}_sub_${executionSuffix}`;
575
+ const completedChildResult = this.completedChildResults.get(childExecutionKey);
576
+ if (completedChildResult != null) return completedChildResult;
127
577
  const parentRegistry = this.getParentHandlerRegistry();
128
578
  const forwardingEnabled = parentRegistry != null;
129
579
  /**
@@ -137,10 +587,10 @@ var SubagentExecutor = class {
137
587
  */
138
588
  const hasToolExecuteHandler = parentRegistry?.getHandler("on_tool_execute") != null;
139
589
  const childInputs = buildChildInputs(config, childAgentId, this.maxDepth, hasToolExecuteHandler);
140
- const childRunId = `${this.parentRunId}_sub_${nanoid(8)}`;
141
590
  const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
142
591
  const hostUsageSink = this.usageSink;
143
- const childGraph = this.createChildGraph({
592
+ const cachedChildRun = this.activeChildRuns.get(childExecutionKey);
593
+ const childGraph = cachedChildRun?.graph ?? this.createChildGraph({
144
594
  runId: childRunId,
145
595
  signal: childSignal,
146
596
  agents: [childInputs],
@@ -177,17 +627,21 @@ var SubagentExecutor = class {
177
627
  parentToolCallId
178
628
  });
179
629
  const forwarder = forwarding?.handler;
180
- if (forwarder) await this.emitSubagentUpdate(parentRegistry, {
181
- childRunId,
182
- subagentType,
183
- subagentAgentId: childAgentId,
184
- parentToolCallId,
185
- phase: "start",
186
- label: `Subagent "${subagentType}" started`
187
- });
630
+ let childAlreadyStarted = this.startedChildRuns.has(childExecutionKey);
631
+ let childAlreadyCompleted = this.completedChildRuns.has(childExecutionKey);
188
632
  let result;
633
+ let recoveredComplete = false;
634
+ let recoveredInProgress = false;
189
635
  try {
190
- const workflow = childGraph.createWorkflow();
636
+ const workflow = cachedChildRun?.workflow ?? childGraph.createWorkflow();
637
+ const activeChildRun = cachedChildRun ?? {
638
+ graph: childGraph,
639
+ workflow,
640
+ pendingInterrupts: [],
641
+ childAgentId,
642
+ childRunId
643
+ };
644
+ if (cachedChildRun == null) this.activeChildRuns.set(childExecutionKey, activeChildRun);
191
645
  /**
192
646
  * When `parentHandlerRegistry` is provided (forwarding mode), attach a
193
647
  * lightweight callback that intercepts the child's `on_custom_event`
@@ -225,46 +679,136 @@ var SubagentExecutor = class {
225
679
  }));
226
680
  const callbacks = callbackHandlers;
227
681
  /**
228
- * Inherit the parent's host `configurable` host-set fields
229
- * (`requestBody`, `user`, `userMCPAuthMap`, etc.) AND the run-
230
- * identity fields (`run_id`, `parent_run_id`, `thread_id`) all
231
- * propagate. LangGraph's own runtime keys are excluded because the
232
- * child graph creates its own scratchpad/checkpoint/abort plumbing.
682
+ * Inherit the parent's host `configurable` while binding LangGraph's
683
+ * checkpoint identity to a stable child id derived from the durable
684
+ * parent thread and checkpoint fork. The parent thread id cannot be
685
+ * reused here: parent and child share one checkpointer when nested HITL
686
+ * is enabled, and root checkpoint namespaces are normalized by
687
+ * LangGraph, so a shared `thread_id` would collide with the parent.
233
688
  *
234
- * Run-identity propagation is intentional and matches the
235
- * convention this executor itself already uses for `SubagentStart`
236
- * / `SubagentStop` hooks (`sessionId: this.parentRunId`): the
237
- * subagent runs under the parent's session scope, not its own.
238
- * Forwarding `run_id` / `parent_run_id` / `thread_id` makes
239
- * `ToolNode`'s hook lookups (`hasHookFor(eventName, runId)`),
240
- * `ToolOutputReferenceRegistry` keying, and trace lineage all
241
- * resolve to the parent's session for tools dispatched from the
242
- * subagent — so `PreToolUse` / `PostToolUse` hooks the host
243
- * registered against the parent's run fire for subagent tool
244
- * calls too. "Same run" matches the user-perceptual mental model.
245
- *
246
- * `thread_id` falls back to `childRunId` only when the parent
247
- * didn't supply one (legacy behavior preserved for hosts that
248
- * never set thread_id).
249
- *
250
- * NOTE: a future revision will likely make this configurable per
251
- * spawn type — e.g. a background / async subagent that runs after
252
- * the parent's run completes wants isolation, not inheritance.
253
- * For now the inheritance path matches LibreChat's primary use
254
- * case (synchronous subagents within a single user turn).
689
+ * `run_id` still propagates as the parent run id, which is the key used
690
+ * for session-scoped hook lookup. Child hook inputs therefore retain
691
+ * the parent policy scope while their `threadId` truthfully identifies
692
+ * the independently checkpointed child execution.
255
693
  */
256
694
  const inheritedConfigurable = sanitizeChildConfigurable(params.parentConfigurable);
257
- result = await workflow.invoke({ messages: [new HumanMessage(description)] }, {
695
+ const resumeAttemptId = getResumeAttemptId(params.parentConfigurable, this.parentRunId);
696
+ const currentHookSessionId = typeof inheritedConfigurable.run_id === "string" && inheritedConfigurable.run_id.length > 0 ? inheritedConfigurable.run_id : this.parentRunId;
697
+ if (cachedChildRun == null && resumeExecution != null) childGraph.restoreSubagentResumeState(resumeExecution.graphState, currentHookSessionId);
698
+ const childInvokeConfig = {
258
699
  recursionLimit: maxTurns * RECURSION_MULTIPLIER,
259
700
  signal: childSignal,
260
701
  callbacks,
261
702
  runName: `subagent:${subagentType}`,
262
703
  configurable: {
263
- thread_id: childRunId,
264
- ...inheritedConfigurable
704
+ ...inheritedConfigurable,
705
+ ...this.humanInTheLoop?.enabled === true ? { [TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY]: approvalExecutionScope } : {},
706
+ ...resumeExecution?.descendant == null ? {} : {
707
+ [SUBAGENT_RESUME_MANIFEST_CONFIG_KEY]: resumeExecution.descendant,
708
+ [SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY]: resumeAttemptId
709
+ },
710
+ thread_id: this.humanInTheLoop?.enabled === true ? childThreadId : inheritedConfigurable.thread_id ?? childRunId
265
711
  }
266
- });
712
+ };
713
+ activeChildRun.invokeConfig = childInvokeConfig;
714
+ if (cachedChildRun == null && this.humanInTheLoop?.enabled === true) {
715
+ /** Rehydrate child-owned interrupt state when a host rebuilds Run
716
+ * around the same durable checkpointer after a process boundary. */
717
+ const persistedState = await workflow.getState(childInvokeConfig);
718
+ const checkpointMessages = getCheckpointMessages(persistedState.values?.messages);
719
+ const persistedHookSessionId = getSubagentHookSessionId(checkpointMessages);
720
+ if (persistedHookSessionId != null) this.hookRegistry?.copySession(persistedHookSessionId, currentHookSessionId);
721
+ const persistedInterrupts = getPersistedInterrupts(persistedState);
722
+ if (persistedInterrupts.length > 0) {
723
+ activeChildRun.pendingInterrupts = persistedInterrupts;
724
+ this.startedChildRuns.add(childExecutionKey);
725
+ childAlreadyStarted = true;
726
+ } else if (persistedState.next.length > 0) {
727
+ recoveredInProgress = true;
728
+ childAlreadyStarted = true;
729
+ this.startedChildRuns.add(childExecutionKey);
730
+ } else if (persistedState.next.length === 0) {
731
+ const persistedMessages = getPersistedMessages(persistedState);
732
+ if (persistedMessages != null) {
733
+ const marker = getSubagentCheckpointMarker(checkpointMessages, executionSuffix);
734
+ result = { messages: persistedMessages };
735
+ recoveredComplete = true;
736
+ childAlreadyStarted = true;
737
+ childAlreadyCompleted = marker?.lifecycleComplete === true;
738
+ this.startedChildRuns.add(childExecutionKey);
739
+ }
740
+ }
741
+ }
742
+ if (!recoveredComplete) {
743
+ const childResumeMap = getChildResumeMap(activeChildRun.pendingInterrupts, params.parentConfigurable);
744
+ if (activeChildRun.pendingInterrupts.length > 0 && childResumeMap == null) throw new GraphInterrupt(activeChildRun.pendingInterrupts);
745
+ let childInput;
746
+ if (childResumeMap != null) childInput = new Command({ resume: childResumeMap });
747
+ else if (recoveredInProgress) childInput = null;
748
+ else childInput = { messages: [new HumanMessage({
749
+ content: description,
750
+ additional_kwargs: {
751
+ [SUBAGENT_HOOK_SESSION_KEY]: currentHookSessionId,
752
+ [SUBAGENT_RUN_ID_KEY]: childRunId
753
+ }
754
+ })] };
755
+ if (!childAlreadyStarted && this.hookRegistry?.hasHookFor("SubagentStart", this.parentRunId) === true) {
756
+ const hookResult = await executeHooks({
757
+ registry: this.hookRegistry,
758
+ input: {
759
+ hook_event_name: "SubagentStart",
760
+ runId: this.parentRunId,
761
+ threadId,
762
+ parentAgentId: this.parentAgentId,
763
+ agentId: childAgentId,
764
+ agentType: subagentType,
765
+ inputs: [new HumanMessage(description)]
766
+ },
767
+ sessionId: this.parentRunId,
768
+ matchQuery: subagentType
769
+ }).catch(() => HOOK_FALLBACK);
770
+ if (hookResult.decision === "deny" || hookResult.decision === "ask") {
771
+ this.clearChildGraph(childGraph);
772
+ this.activeChildRuns.delete(childExecutionKey);
773
+ return {
774
+ content: `Blocked: ${hookResult.reason ?? "Blocked by hook"}`,
775
+ messages: []
776
+ };
777
+ }
778
+ }
779
+ this.startedChildRuns.add(childExecutionKey);
780
+ if (forwarder && !childAlreadyStarted) await this.emitSubagentUpdate(parentRegistry, {
781
+ childRunId,
782
+ subagentType,
783
+ subagentAgentId: childAgentId,
784
+ parentToolCallId,
785
+ phase: "start",
786
+ label: `Subagent "${subagentType}" started`
787
+ });
788
+ let childResult;
789
+ if (this.humanInTheLoop?.enabled === true)
790
+ /** Execute as an independently checkpointed root instead of inheriting
791
+ * the parent's Pregel namespace. Parent decisions are routed explicitly
792
+ * by interrupt id, so concurrent children keep isolated resume state. */
793
+ childResult = await AsyncLocalStorageProviderSingleton.runWithConfig(childInvokeConfig, () => workflow.invoke(childInput, childInvokeConfig));
794
+ else childResult = await workflow.invoke(childInput, childInvokeConfig);
795
+ const childInterrupts = isInterrupted(childResult) ? childResult[INTERRUPT] : void 0;
796
+ if (childInterrupts != null && childInterrupts.length > 0) throw new GraphInterrupt(childInterrupts);
797
+ result = { messages: childResult.messages };
798
+ }
267
799
  } catch (error) {
800
+ if (isGraphInterrupt(error)) {
801
+ const activeChildRun = this.activeChildRuns.get(childExecutionKey);
802
+ if (activeChildRun != null) activeChildRun.pendingInterrupts = error.interrupts;
803
+ const resumeManifest = activeChildRun == null || parentToolCallId == null ? void 0 : await this.createResumeManifest();
804
+ await forwarding?.drain();
805
+ throw new GraphInterrupt(addSubagentScope(error.interrupts, {
806
+ run_id: childRunId,
807
+ agent_id: childAgentId,
808
+ subagent_type: subagentType,
809
+ parent_tool_call_id: parentToolCallId
810
+ }, resumeManifest));
811
+ }
268
812
  /** Aborted before any observational work below: parallel siblings — in
269
813
  * this executor and, via the graph-scoped breaker, under other
270
814
  * parallel agent nodes — stream on the composed child signal, and
@@ -286,7 +830,8 @@ var SubagentExecutor = class {
286
830
  data: { message: errorMessage }
287
831
  });
288
832
  }
289
- childGraph.clearHeavyState();
833
+ this.clearChildGraph(childGraph);
834
+ this.activeChildRuns.delete(childExecutionKey);
290
835
  /**
291
836
  * A tripped stream circuit breaker is a safety abort, not a recoverable
292
837
  * subagent failure: converting it into a tool result would let the
@@ -300,8 +845,9 @@ var SubagentExecutor = class {
300
845
  messages: []
301
846
  };
302
847
  }
848
+ if (result == null) throw new Error("Subagent completed without producing graph state.");
303
849
  const filteredContent = filterSubagentResult(result.messages);
304
- if (this.hookRegistry?.hasHookFor("SubagentStop", this.parentRunId) === true)
850
+ if (!childAlreadyCompleted && this.hookRegistry?.hasHookFor("SubagentStop", this.parentRunId) === true)
305
851
  /**
306
852
  * Awaited (not fire-and-forget) for deterministic test synchronization
307
853
  * and consistency with PostCompact. The parent is already waiting on the
@@ -321,7 +867,7 @@ var SubagentExecutor = class {
321
867
  sessionId: this.parentRunId,
322
868
  matchQuery: subagentType
323
869
  }).catch(() => {});
324
- if (forwarding) {
870
+ if (forwarding && !childAlreadyCompleted) {
325
871
  await forwarding.drain();
326
872
  await this.emitSubagentUpdate(parentRegistry, {
327
873
  childRunId,
@@ -332,11 +878,18 @@ var SubagentExecutor = class {
332
878
  label: `Subagent "${subagentType}" finished`
333
879
  });
334
880
  }
335
- childGraph.clearHeavyState();
336
- return {
881
+ if (!childAlreadyCompleted) {
882
+ const activeChildRun = this.activeChildRuns.get(childExecutionKey);
883
+ if (activeChildRun != null && parentToolCallId != null) await this.persistChildCheckpointMarker(activeChildRun, parentToolCallId);
884
+ }
885
+ this.completedChildRuns.add(childExecutionKey);
886
+ this.clearChildGraph(childGraph);
887
+ const completedResult = {
337
888
  content: filteredContent,
338
889
  messages: result.messages
339
890
  };
891
+ this.completedChildResults.set(childExecutionKey, completedResult);
892
+ return completedResult;
340
893
  }
341
894
  /**
342
895
  * Emits a single {@link GraphEvents.ON_SUBAGENT_UPDATE} envelope through the
@@ -583,7 +1136,7 @@ function sanitizeChildConfigurable(parentConfigurable) {
583
1136
  return Object.fromEntries(Object.entries(parentConfigurable).filter(([key]) => !isLangGraphRuntimeConfigKey(key)));
584
1137
  }
585
1138
  function isLangGraphRuntimeConfigKey(key) {
586
- return key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) || LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key) || key === "lc_run_breaker_scope";
1139
+ return key.startsWith(LANGGRAPH_RUNTIME_CONFIG_PREFIX) || LANGGRAPH_CHECKPOINT_CONFIG_KEYS.has(key) || key === "__librechat_subagent_resume_attempt" || key === "__librechat_subagent_resume_manifest" || key === "__librechat_subagent_parent_batch" || key === "lc_run_breaker_scope";
587
1140
  }
588
1141
  function sanitizeForwardedSubagentUpdateData(eventName, data) {
589
1142
  if (eventName === "on_tool_execute") return sanitizeToolExecuteUpdateData(data);
@@ -880,11 +1433,10 @@ function buildChildInputs(config, childAgentId, parentMaxDepth, keepToolDefiniti
880
1433
  * Host-supplied direct tools are scrubbed from INHERITED configs only.
881
1434
  * A self-spawn config's `agentInputs` is a shallow spread of the parent's
882
1435
  * `_sourceInputs`, so without this a parent-scoped graph tool (e.g. an
883
- * interrupt-raising ask_user_question, which needs the parent's
884
- * checkpointer child graphs compile without one) would silently leak
885
- * into the child and deterministically throw `No checkpointer set`. An
886
- * EXPLICIT child config that lists its own `graphTools` is a deliberate
887
- * host choice and keeps them (Codex #289 P2).
1436
+ * interrupt-raising ask_user_question) would silently become available to
1437
+ * the child. An EXPLICIT child config that lists its own `graphTools` is a
1438
+ * deliberate host choice and keeps them (Codex #289 P2); with HITL enabled,
1439
+ * those tools use the shared checkpointer and can pause and resume safely.
888
1440
  */
889
1441
  graphTools: config.self === true ? void 0 : agentInputs.graphTools
890
1442
  };