@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
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
18
18
|
import type { LangGraphRunnableConfig } from '@langchain/langgraph';
|
|
19
19
|
import type { ToolRuntime } from '@langchain/core/tools';
|
|
20
|
+
import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
|
|
20
21
|
import type * as t from '@/types';
|
|
21
22
|
import { serializeToolContentBounded } from '@/utils/toolContent';
|
|
22
23
|
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
|
|
@@ -124,6 +125,30 @@ function isValidHandoffGroupId(value: unknown): value is number {
|
|
|
124
125
|
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function getLastNewAiMessage(
|
|
129
|
+
messages: BaseMessage[],
|
|
130
|
+
previousMessages: BaseMessage[]
|
|
131
|
+
): BaseMessage | undefined {
|
|
132
|
+
const previousMessageObjects = new Set(previousMessages);
|
|
133
|
+
const previousMessageIds = new Set<string>();
|
|
134
|
+
for (const message of previousMessages) {
|
|
135
|
+
if (message.id != null) {
|
|
136
|
+
previousMessageIds.add(message.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
140
|
+
const message = messages[index];
|
|
141
|
+
if (
|
|
142
|
+
message.getType() === 'ai' &&
|
|
143
|
+
!previousMessageObjects.has(message) &&
|
|
144
|
+
(message.id == null || !previousMessageIds.has(message.id))
|
|
145
|
+
) {
|
|
146
|
+
return message;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
127
152
|
function withHandoffGroupMetadata(
|
|
128
153
|
config: LangGraphRunnableConfig | undefined,
|
|
129
154
|
groupId: number | undefined
|
|
@@ -156,6 +181,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
156
181
|
private startingNodes: Set<string> = new Set();
|
|
157
182
|
private directEdges: t.GraphEdge[] = [];
|
|
158
183
|
private handoffEdges: t.GraphEdge[] = [];
|
|
184
|
+
private handoffSourceIds = new Set<string>();
|
|
185
|
+
private readonly resultAgentId?: string;
|
|
186
|
+
private readonly memberRecursionLimit?: number;
|
|
159
187
|
private handoffPromptLabels: Map<string, Set<string>> = new Map();
|
|
160
188
|
/**
|
|
161
189
|
* Map of agentId to parallel group info.
|
|
@@ -169,11 +197,34 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
169
197
|
*/
|
|
170
198
|
private agentParallelGroups: Map<string, number> = new Map();
|
|
171
199
|
|
|
172
|
-
constructor(
|
|
173
|
-
|
|
200
|
+
constructor(
|
|
201
|
+
input: t.MultiAgentGraphInput,
|
|
202
|
+
dependencies?: GraphFactoryDependencies
|
|
203
|
+
) {
|
|
204
|
+
super(input, dependencies);
|
|
174
205
|
this.edges = input.edges;
|
|
206
|
+
this.resultAgentId = input.resultAgentId;
|
|
207
|
+
this.memberRecursionLimit = input.memberRecursionLimit;
|
|
208
|
+
if (
|
|
209
|
+
this.memberRecursionLimit != null &&
|
|
210
|
+
(!Number.isSafeInteger(this.memberRecursionLimit) ||
|
|
211
|
+
this.memberRecursionLimit <= 0)
|
|
212
|
+
) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
'MultiAgentGraph: memberRecursionLimit must be a positive safe integer.'
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
this.resultAgentId != null &&
|
|
219
|
+
!this.agentContexts.has(this.resultAgentId)
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`MultiAgentGraph: resultAgentId "${this.resultAgentId}" is not present in agents.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
175
225
|
this.validateEdgeAgents();
|
|
176
226
|
this.categorizeEdges();
|
|
227
|
+
this.validateCommandRoutedDirectEdges();
|
|
177
228
|
this.analyzeGraph();
|
|
178
229
|
this.createHandoffTools();
|
|
179
230
|
}
|
|
@@ -221,26 +272,61 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
221
272
|
*/
|
|
222
273
|
private categorizeEdges(): void {
|
|
223
274
|
for (const edge of this.edges) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
275
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
276
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
277
|
+
const isDefaultDirect =
|
|
278
|
+
edge.edgeType == null &&
|
|
279
|
+
edge.condition == null &&
|
|
280
|
+
sources.length === 1 &&
|
|
281
|
+
destinations.length > 1;
|
|
282
|
+
if (edge.edgeType === 'direct' || isDefaultDirect) {
|
|
227
283
|
this.directEdges.push(edge);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
this.handoffEdges.push(edge);
|
|
287
|
+
for (const source of sources) {
|
|
288
|
+
this.handoffSourceIds.add(source);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
234
292
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
293
|
+
/** Static waiting/prompt edges cannot also be driven by Command routing. */
|
|
294
|
+
private validateCommandRoutedDirectEdges(): void {
|
|
295
|
+
const destinationGroups = new Map<
|
|
296
|
+
string,
|
|
297
|
+
{ hasPrompt: boolean; commandSource?: string }
|
|
298
|
+
>();
|
|
299
|
+
for (const edge of this.directEdges) {
|
|
300
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
301
|
+
const commandSource = sources.find((source) =>
|
|
302
|
+
this.handoffSourceIds.has(source)
|
|
303
|
+
);
|
|
304
|
+
if (commandSource != null && sources.length > 1) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
'MultiAgentGraph: grouped direct edge cannot include command-routed ' +
|
|
307
|
+
`source "${commandSource}". Split handoff routing from all-of fan-in.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
311
|
+
const hasPrompt = edge.prompt != null && edge.prompt !== '';
|
|
312
|
+
for (const destination of destinations) {
|
|
313
|
+
const group = destinationGroups.get(destination) ?? {
|
|
314
|
+
hasPrompt: false,
|
|
315
|
+
};
|
|
316
|
+
group.hasPrompt ||= hasPrompt;
|
|
317
|
+
group.commandSource ??= commandSource;
|
|
318
|
+
destinationGroups.set(destination, group);
|
|
242
319
|
}
|
|
243
320
|
}
|
|
321
|
+
for (const { hasPrompt, commandSource } of destinationGroups.values()) {
|
|
322
|
+
if (!hasPrompt || commandSource == null) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
throw new Error(
|
|
326
|
+
'MultiAgentGraph: prompted direct edge cannot include command-routed ' +
|
|
327
|
+
`source "${commandSource}". Move the prompt into the routed node.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
244
330
|
}
|
|
245
331
|
|
|
246
332
|
/**
|
|
@@ -582,7 +668,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
582
668
|
* 3. Include all messages before the AIMessage plus the filtered pair
|
|
583
669
|
*/
|
|
584
670
|
const messages = state.messages;
|
|
585
|
-
let filteredMessages
|
|
671
|
+
let filteredMessages: BaseMessage[];
|
|
586
672
|
let aiMessageIndex = -1;
|
|
587
673
|
|
|
588
674
|
/** Find the AIMessage containing this tool call */
|
|
@@ -946,9 +1032,22 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
946
1032
|
reducer: (a, b) => b,
|
|
947
1033
|
default: () => [],
|
|
948
1034
|
}),
|
|
1035
|
+
subagentResult: Annotation<t.SubagentGraphResult | undefined>({
|
|
1036
|
+
reducer: (_current, update) => update,
|
|
1037
|
+
default: () => undefined,
|
|
1038
|
+
}),
|
|
949
1039
|
});
|
|
950
1040
|
|
|
951
1041
|
const builder = new StateGraph(StateAnnotation);
|
|
1042
|
+
const addDirectEdge = (sources: string[], destination: string): void => {
|
|
1043
|
+
if (sources.length === 0) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const source = sources.length === 1 ? sources[0] : sources;
|
|
1047
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1048
|
+
/** @ts-ignore */
|
|
1049
|
+
builder.addEdge(source, destination);
|
|
1050
|
+
};
|
|
952
1051
|
|
|
953
1052
|
// Add all agents as complete subgraphs
|
|
954
1053
|
for (const [agentId] of this.agentContexts) {
|
|
@@ -997,6 +1096,11 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
997
1096
|
config?: LangGraphRunnableConfig
|
|
998
1097
|
): Promise<t.MultiAgentGraphState | Command> => {
|
|
999
1098
|
let result: t.MultiAgentGraphState;
|
|
1099
|
+
let inputMessages = state.messages;
|
|
1100
|
+
const memberConfig =
|
|
1101
|
+
this.memberRecursionLimit == null
|
|
1102
|
+
? config
|
|
1103
|
+
: { ...config, recursionLimit: this.memberRecursionLimit };
|
|
1000
1104
|
|
|
1001
1105
|
/**
|
|
1002
1106
|
* Check if this agent is receiving a handoff.
|
|
@@ -1092,9 +1196,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1092
1196
|
...state,
|
|
1093
1197
|
messages: messagesForAgent,
|
|
1094
1198
|
};
|
|
1199
|
+
inputMessages = messagesForAgent;
|
|
1095
1200
|
result = await agentSubgraph.invoke(
|
|
1096
1201
|
transformedState,
|
|
1097
|
-
withHandoffGroupMetadata(
|
|
1202
|
+
withHandoffGroupMetadata(memberConfig, parallelGroupId)
|
|
1098
1203
|
);
|
|
1099
1204
|
result = {
|
|
1100
1205
|
...result,
|
|
@@ -1140,14 +1245,25 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1140
1245
|
...state,
|
|
1141
1246
|
messages: state.agentMessages,
|
|
1142
1247
|
};
|
|
1143
|
-
|
|
1248
|
+
inputMessages = state.agentMessages;
|
|
1249
|
+
result = await agentSubgraph.invoke(transformedState, memberConfig);
|
|
1144
1250
|
result = {
|
|
1145
1251
|
...result,
|
|
1146
1252
|
/** Clear agentMessages for next agent */
|
|
1147
1253
|
agentMessages: [],
|
|
1148
1254
|
};
|
|
1149
1255
|
} else {
|
|
1150
|
-
result = await agentSubgraph.invoke(state,
|
|
1256
|
+
result = await agentSubgraph.invoke(state, memberConfig);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (this.resultAgentId === agentId) {
|
|
1260
|
+
result = {
|
|
1261
|
+
...result,
|
|
1262
|
+
subagentResult: {
|
|
1263
|
+
agentId,
|
|
1264
|
+
message: getLastNewAiMessage(result.messages, inputMessages),
|
|
1265
|
+
},
|
|
1266
|
+
};
|
|
1151
1267
|
}
|
|
1152
1268
|
|
|
1153
1269
|
/** If agent has both handoff and direct edges, use Command for exclusive routing */
|
|
@@ -1296,11 +1412,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1296
1412
|
/** Add edges from all sources to the wrapper, then wrapper to destination */
|
|
1297
1413
|
for (const edge of edges) {
|
|
1298
1414
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1299
|
-
|
|
1300
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1301
|
-
/** @ts-ignore */
|
|
1302
|
-
builder.addEdge(source, wrapperNodeId);
|
|
1303
|
-
}
|
|
1415
|
+
addDirectEdge(sources, wrapperNodeId);
|
|
1304
1416
|
}
|
|
1305
1417
|
|
|
1306
1418
|
/** Single edge from wrapper to destination */
|
|
@@ -1311,26 +1423,16 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1311
1423
|
/** No prompt instructions, add direct edges (skip if source uses Command routing) */
|
|
1312
1424
|
for (const edge of edges) {
|
|
1313
1425
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1326
|
-
if (sourceHandoffEdges.length > 0 && sourceDirectEdges.length > 0) {
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1331
|
-
/** @ts-ignore */
|
|
1332
|
-
builder.addEdge(source, destination);
|
|
1333
|
-
}
|
|
1426
|
+
const staticSources = sources.filter(
|
|
1427
|
+
(source) =>
|
|
1428
|
+
!this.handoffEdges.some((handoffEdge) => {
|
|
1429
|
+
const handoffSources = Array.isArray(handoffEdge.from)
|
|
1430
|
+
? handoffEdge.from
|
|
1431
|
+
: [handoffEdge.from];
|
|
1432
|
+
return handoffSources.includes(source);
|
|
1433
|
+
})
|
|
1434
|
+
);
|
|
1435
|
+
addDirectEdge(staticSources, destination);
|
|
1334
1436
|
}
|
|
1335
1437
|
}
|
|
1336
1438
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect } from '@jest/globals';
|
|
1
|
+
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
2
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
3
3
|
import type * as t from '@/types';
|
|
4
4
|
import { StreamLimitExceededError } from '@/llm/streamLimits';
|
|
5
|
-
import { Providers } from '@/common';
|
|
6
5
|
import { StandardGraph } from '../Graph';
|
|
6
|
+
import { Providers } from '@/common';
|
|
7
7
|
|
|
8
8
|
const makeAgent = (agentId: string): t.AgentInputs => ({
|
|
9
9
|
agentId,
|
|
@@ -25,7 +25,51 @@ const makeTrip = (): StreamLimitExceededError =>
|
|
|
25
25
|
toolName: 'db_query',
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
type CompiledRegistrationInternals = {
|
|
29
|
+
_compiledToolNodes: Set<{ clearDirectPathTurns(): void }>;
|
|
30
|
+
_subagentExecutors: Set<{
|
|
31
|
+
clearHeavyState(): void;
|
|
32
|
+
resetCheckpointThreadIds(): void;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
28
36
|
describe('run breaker lifecycle', () => {
|
|
37
|
+
it('retains compiled cleanup registrations across graph reuse', () => {
|
|
38
|
+
const graph = makeGraph();
|
|
39
|
+
const internals = graph as unknown as CompiledRegistrationInternals;
|
|
40
|
+
const clearDirectPathTurns = jest.fn();
|
|
41
|
+
const clearSubagentState = jest.fn();
|
|
42
|
+
const resetCheckpointThreadIds = jest.fn();
|
|
43
|
+
internals._compiledToolNodes.add({ clearDirectPathTurns });
|
|
44
|
+
internals._subagentExecutors.add({
|
|
45
|
+
clearHeavyState: clearSubagentState,
|
|
46
|
+
resetCheckpointThreadIds,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
graph.clearHeavyState();
|
|
50
|
+
graph.clearHeavyState();
|
|
51
|
+
|
|
52
|
+
expect(clearDirectPathTurns).toHaveBeenCalledTimes(2);
|
|
53
|
+
expect(clearSubagentState).toHaveBeenCalledTimes(2);
|
|
54
|
+
expect(internals._compiledToolNodes.size).toBe(1);
|
|
55
|
+
expect(internals._subagentExecutors.size).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('resets subagent checkpoint ownership at each fresh run start', () => {
|
|
59
|
+
const graph = makeGraph();
|
|
60
|
+
const internals = graph as unknown as CompiledRegistrationInternals;
|
|
61
|
+
const resetCheckpointThreadIds = jest.fn();
|
|
62
|
+
internals._subagentExecutors.add({
|
|
63
|
+
clearHeavyState: jest.fn(),
|
|
64
|
+
resetCheckpointThreadIds,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
graph.resetValues();
|
|
68
|
+
graph.resetValues();
|
|
69
|
+
|
|
70
|
+
expect(resetCheckpointThreadIds).toHaveBeenCalledTimes(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
29
73
|
it('replaces the breaker at every run start, even when un-aborted', () => {
|
|
30
74
|
const graph = makeGraph();
|
|
31
75
|
const previous = graph.breakerAbort;
|
|
@@ -115,9 +159,7 @@ describe('run breaker lifecycle', () => {
|
|
|
115
159
|
model: model as unknown as t.ChatModel,
|
|
116
160
|
messages: [new HumanMessage('hi')],
|
|
117
161
|
provider: Providers.OPENAI,
|
|
118
|
-
context: graph as Parameters<
|
|
119
|
-
typeof attemptInvoke
|
|
120
|
-
>[0]['context'],
|
|
162
|
+
context: graph as Parameters<typeof attemptInvoke>[0]['context'],
|
|
121
163
|
onChunk: () => {
|
|
122
164
|
/* producer loop enforcement is under test */
|
|
123
165
|
},
|
|
@@ -225,10 +267,7 @@ describe('run breaker lifecycle', () => {
|
|
|
225
267
|
/** With a live breaker the entry guard must not fire; the node then
|
|
226
268
|
* fails later, on the missing config, proving it got past the guard. */
|
|
227
269
|
await expect(
|
|
228
|
-
node(
|
|
229
|
-
{ messages: [] } as unknown as t.AgentSubgraphState,
|
|
230
|
-
undefined
|
|
231
|
-
)
|
|
270
|
+
node({ messages: [] } as unknown as t.AgentSubgraphState, undefined)
|
|
232
271
|
).rejects.toThrow('No config provided');
|
|
233
272
|
});
|
|
234
273
|
});
|
|
@@ -1338,7 +1338,7 @@ describe('context overflow recovery', () => {
|
|
|
1338
1338
|
if (agentContext == null) {
|
|
1339
1339
|
throw new Error('Expected default agent context');
|
|
1340
1340
|
}
|
|
1341
|
-
expect(run.Graph.compileOptions?.checkpointer).
|
|
1341
|
+
expect(run.Graph.compileOptions?.checkpointer).toBeInstanceOf(MemorySaver);
|
|
1342
1342
|
expect(run.Graph.hasCompiledCheckpointer).toBe(true);
|
|
1343
1343
|
run.Graph.resetValues(undefined, 'checkpoint-scope');
|
|
1344
1344
|
agentContext.preserveOriginalToolContent(new Map([[2, 'full output']]));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
|
|
3
|
+
import { StandardGraph } from '../Graph';
|
|
4
|
+
import { Providers } from '@/common';
|
|
5
|
+
|
|
6
|
+
type ResumeToolNode = {
|
|
7
|
+
createSubagentResumeState(): SubagentToolNodeResumeState;
|
|
8
|
+
restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const makeGraph = (runId: string): StandardGraph =>
|
|
12
|
+
new StandardGraph({
|
|
13
|
+
runId,
|
|
14
|
+
agents: [
|
|
15
|
+
{
|
|
16
|
+
agentId: 'child-agent',
|
|
17
|
+
provider: Providers.OPENAI,
|
|
18
|
+
instructions: 'Test child state restoration.',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const initializeToolNode = (graph: StandardGraph): ResumeToolNode => {
|
|
24
|
+
const agentContext = graph.agentContexts.get('child-agent');
|
|
25
|
+
return graph.initializeTools({
|
|
26
|
+
currentTools: [],
|
|
27
|
+
agentContext,
|
|
28
|
+
}) as unknown as ResumeToolNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('subagent graph resume state', () => {
|
|
32
|
+
it('restores tool identity, turns, references, and sessions into a branch', () => {
|
|
33
|
+
const source = makeGraph('source');
|
|
34
|
+
source.toolOutputReferences = { enabled: true };
|
|
35
|
+
const sourceNode = initializeToolNode(source);
|
|
36
|
+
source.toolCallStepIds.set('call_tool', 'step_tool');
|
|
37
|
+
source.sessions.set('execute_code', {
|
|
38
|
+
session_id: 'sandbox-session',
|
|
39
|
+
lastUpdated: 42,
|
|
40
|
+
});
|
|
41
|
+
source
|
|
42
|
+
.getOrCreateToolOutputRegistry()
|
|
43
|
+
?.set('source-scope', 'tool0turn0', 'raw output');
|
|
44
|
+
source.getEagerEventToolUsageCount('child-agent').set('calculator', 3);
|
|
45
|
+
source.eagerEventToolSuppressions.add('unstable_search');
|
|
46
|
+
const sourceNodeState = sourceNode.createSubagentResumeState();
|
|
47
|
+
sourceNode.restoreSubagentResumeState({
|
|
48
|
+
...sourceNodeState,
|
|
49
|
+
toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
|
|
50
|
+
directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const target = makeGraph('target');
|
|
54
|
+
target.toolOutputReferences = { enabled: true };
|
|
55
|
+
const targetNode = initializeToolNode(target);
|
|
56
|
+
target.restoreSubagentResumeState(
|
|
57
|
+
source.createSubagentResumeState('source-scope'),
|
|
58
|
+
'target-scope'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(target.toolCallStepIds.get('call_tool')).toBe('step_tool');
|
|
62
|
+
expect(target.sessions.get('execute_code')).toMatchObject({
|
|
63
|
+
session_id: 'sandbox-session',
|
|
64
|
+
lastUpdated: 42,
|
|
65
|
+
});
|
|
66
|
+
expect(
|
|
67
|
+
target.getOrCreateToolOutputRegistry()?.get('target-scope', 'tool0turn0')
|
|
68
|
+
).toBe('raw output');
|
|
69
|
+
expect(targetNode.createSubagentResumeState()).toMatchObject({
|
|
70
|
+
toolUsageCounts: [{ toolName: 'calculator', count: 3 }],
|
|
71
|
+
directPathTurns: [{ toolCallId: 'call_tool', turn: 2 }],
|
|
72
|
+
});
|
|
73
|
+
expect(
|
|
74
|
+
target.getEagerEventToolUsageCount('child-agent').get('calculator')
|
|
75
|
+
).toBe(3);
|
|
76
|
+
expect(target.eagerEventToolSuppressions).toEqual(
|
|
77
|
+
new Set(['unstable_search'])
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -88,4 +88,90 @@ describe('MultiAgentGraph.validateEdgeAgents', () => {
|
|
|
88
88
|
};
|
|
89
89
|
expect(() => new MultiAgentGraph(input)).not.toThrow();
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
it('rejects grouped fan-in containing a command-routed source', () => {
|
|
93
|
+
const input: t.MultiAgentGraphInput = {
|
|
94
|
+
runId: 'hybrid-fan-in',
|
|
95
|
+
agents: [
|
|
96
|
+
makeAgent('router'),
|
|
97
|
+
makeAgent('worker'),
|
|
98
|
+
makeAgent('result'),
|
|
99
|
+
makeAgent('alternate'),
|
|
100
|
+
],
|
|
101
|
+
edges: [
|
|
102
|
+
{
|
|
103
|
+
from: ['router', 'worker'],
|
|
104
|
+
to: 'result',
|
|
105
|
+
edgeType: 'direct',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
from: 'router',
|
|
109
|
+
to: 'alternate',
|
|
110
|
+
edgeType: 'handoff',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
expect(() => new MultiAgentGraph(input)).toThrow(
|
|
116
|
+
/grouped direct edge.*command-routed source.*router/i
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('rejects a prompted direct edge from a command-routed source', () => {
|
|
121
|
+
const input: t.MultiAgentGraphInput = {
|
|
122
|
+
runId: 'hybrid-prompt',
|
|
123
|
+
agents: [
|
|
124
|
+
makeAgent('router'),
|
|
125
|
+
makeAgent('result'),
|
|
126
|
+
makeAgent('alternate'),
|
|
127
|
+
],
|
|
128
|
+
edges: [
|
|
129
|
+
{
|
|
130
|
+
from: 'router',
|
|
131
|
+
to: 'result',
|
|
132
|
+
edgeType: 'direct',
|
|
133
|
+
prompt: 'Continue through the direct path.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
from: 'router',
|
|
137
|
+
to: 'alternate',
|
|
138
|
+
edgeType: 'handoff',
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
expect(() => new MultiAgentGraph(input)).toThrow(
|
|
144
|
+
/prompted direct edge.*command-routed source.*router/i
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('rejects a command-routed source sharing a prompted destination group', () => {
|
|
149
|
+
const input: t.MultiAgentGraphInput = {
|
|
150
|
+
runId: 'hybrid-prompt-group',
|
|
151
|
+
agents: [
|
|
152
|
+
makeAgent('router'),
|
|
153
|
+
makeAgent('worker'),
|
|
154
|
+
makeAgent('result'),
|
|
155
|
+
makeAgent('alternate'),
|
|
156
|
+
],
|
|
157
|
+
edges: [
|
|
158
|
+
{ from: 'router', to: 'result', edgeType: 'direct' },
|
|
159
|
+
{
|
|
160
|
+
from: 'worker',
|
|
161
|
+
to: 'result',
|
|
162
|
+
edgeType: 'direct',
|
|
163
|
+
prompt: 'Combine the inputs.',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
from: 'router',
|
|
167
|
+
to: 'alternate',
|
|
168
|
+
edgeType: 'handoff',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
expect(() => new MultiAgentGraph(input)).toThrow(
|
|
174
|
+
/prompted direct edge.*command-routed source.*router/i
|
|
175
|
+
);
|
|
176
|
+
});
|
|
91
177
|
});
|