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