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