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