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