@librechat/agents 3.4.1 → 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 +57 -33
- 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/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/main.cjs +20 -11
- package/dist/cjs/run.cjs +48 -38
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +13 -9
- package/dist/cjs/tools/ToolNode.cjs.map +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 +672 -343
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +3 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
- 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/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +57 -33
- 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/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/main.mjs +12 -10
- package/dist/esm/run.mjs +48 -38
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +13 -9
- package/dist/esm/tools/ToolNode.mjs.map +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 +672 -343
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +3 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
- 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/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +7 -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/run.d.ts +2 -2
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- 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 +32 -37
- package/dist/types/tools/subagent/SubagentReplay.d.ts +5 -1
- 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/types/graph.d.ts +184 -19
- package/package.json +3 -1
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +111 -60
- package/src/graphs/MultiAgentGraph.ts +149 -47
- 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/run.ts +50 -38
- 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/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +22 -10
- package/src/tools/__tests__/SubagentExecutor.test.ts +198 -9
- package/src/tools/__tests__/SubagentReplay.test.ts +29 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +28 -2
- package/src/tools/__tests__/subagentHooks.test.ts +90 -3
- 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 +1245 -563
- package/src/tools/subagent/SubagentReplay.ts +10 -1
- 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/types/graph.ts +217 -19
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { AIMessageChunk } from '@langchain/core/messages';
|
|
4
|
+
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
5
|
+
import { FakeListChatModel } from '@langchain/core/utils/testing';
|
|
6
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
7
|
+
import type { GraphSubagentConfig } from '@/types';
|
|
8
|
+
import type * as t from '@/types';
|
|
9
|
+
import { createFakeStreamingLLM } from '@/llm/fake';
|
|
10
|
+
import { Constants, Providers } from '@/common';
|
|
11
|
+
import { StandardGraph } from '@/graphs/Graph';
|
|
12
|
+
import { Run } from '@/run';
|
|
13
|
+
|
|
14
|
+
const invokeConfig: RunnableConfig = {
|
|
15
|
+
configurable: { thread_id: 'graph-subagent-test' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const usage = {
|
|
19
|
+
input_tokens: 5,
|
|
20
|
+
output_tokens: 3,
|
|
21
|
+
total_tokens: 8,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
class UsageFakeChatModel extends FakeListChatModel {
|
|
25
|
+
constructor() {
|
|
26
|
+
super({ responses: ['member response'] });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override async *_streamResponseChunks(
|
|
30
|
+
...args: Parameters<FakeListChatModel['_streamResponseChunks']>
|
|
31
|
+
): ReturnType<FakeListChatModel['_streamResponseChunks']> {
|
|
32
|
+
yield* super._streamResponseChunks(...args);
|
|
33
|
+
yield new ChatGenerationChunk({
|
|
34
|
+
text: '',
|
|
35
|
+
message: new AIMessageChunk({
|
|
36
|
+
content: '',
|
|
37
|
+
usage_metadata: { ...usage },
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class LoopingFakeChatModel extends FakeListChatModel {
|
|
44
|
+
private invocationCount = 0;
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
super({ responses: ['unused'] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override async *_streamResponseChunks(
|
|
51
|
+
..._args: Parameters<FakeListChatModel['_streamResponseChunks']>
|
|
52
|
+
): ReturnType<FakeListChatModel['_streamResponseChunks']> {
|
|
53
|
+
this.invocationCount++;
|
|
54
|
+
if (this.invocationCount <= 2) {
|
|
55
|
+
yield new ChatGenerationChunk({
|
|
56
|
+
text: '',
|
|
57
|
+
message: new AIMessageChunk({
|
|
58
|
+
content: '',
|
|
59
|
+
tool_call_chunks: [
|
|
60
|
+
{
|
|
61
|
+
name: 'loop_tool',
|
|
62
|
+
args: '{}',
|
|
63
|
+
id: `loop-${this.invocationCount}`,
|
|
64
|
+
index: 0,
|
|
65
|
+
type: 'tool_call_chunk',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
yield new ChatGenerationChunk({
|
|
73
|
+
text: 'escaped member turn limit',
|
|
74
|
+
message: new AIMessageChunk('escaped member turn limit'),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const makeAgent = (agentId: string): t.AgentInputs => ({
|
|
80
|
+
agentId,
|
|
81
|
+
provider: Providers.OPENAI,
|
|
82
|
+
clientOptions: { modelName: `${agentId}-model`, apiKey: 'test-key' },
|
|
83
|
+
instructions: `You are ${agentId}.`,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const makeGraphConfig = (): GraphSubagentConfig => ({
|
|
87
|
+
kind: 'graph',
|
|
88
|
+
type: 'research-team',
|
|
89
|
+
name: 'Research Team',
|
|
90
|
+
description: 'Coordinates parallel research and synthesis.',
|
|
91
|
+
entryAgentId: 'coordinator',
|
|
92
|
+
resultAgentId: 'synthesizer',
|
|
93
|
+
agents: [
|
|
94
|
+
makeAgent('coordinator'),
|
|
95
|
+
makeAgent('left'),
|
|
96
|
+
makeAgent('right'),
|
|
97
|
+
makeAgent('synthesizer'),
|
|
98
|
+
],
|
|
99
|
+
edges: [
|
|
100
|
+
{
|
|
101
|
+
from: 'coordinator',
|
|
102
|
+
to: ['left', 'right'],
|
|
103
|
+
edgeType: 'direct',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
from: ['left', 'right'],
|
|
107
|
+
to: 'synthesizer',
|
|
108
|
+
edgeType: 'direct',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const createRun = async (
|
|
114
|
+
graphConfig: GraphSubagentConfig,
|
|
115
|
+
overrides: Partial<t.RunConfig> = {}
|
|
116
|
+
): Promise<Run<t.IState>> =>
|
|
117
|
+
Run.create<t.IState>({
|
|
118
|
+
runId: `graph-subagent-${Date.now()}-${Math.random()}`,
|
|
119
|
+
graphConfig: {
|
|
120
|
+
type: 'standard',
|
|
121
|
+
agents: [
|
|
122
|
+
{
|
|
123
|
+
...makeAgent('parent'),
|
|
124
|
+
maxSubagentDepth: 2,
|
|
125
|
+
subagentConfigs: [graphConfig],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
returnContent: true,
|
|
130
|
+
skipCleanup: true,
|
|
131
|
+
...overrides,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const getGraphSubagentTool = (run: Run<t.IState>): t.GenericTool => {
|
|
135
|
+
const tools = (run.Graph as StandardGraph).agentContexts.get('parent')
|
|
136
|
+
?.graphTools as t.GenericTool[] | undefined;
|
|
137
|
+
const tool = tools?.find(
|
|
138
|
+
(candidate) => 'name' in candidate && candidate.name === Constants.SUBAGENT
|
|
139
|
+
);
|
|
140
|
+
if (tool == null) {
|
|
141
|
+
throw new Error('Expected graph subagent tool');
|
|
142
|
+
}
|
|
143
|
+
return tool;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
describe('Graph subagent integration', () => {
|
|
147
|
+
it('runs a convergent direct DAG and returns only the designated result', async () => {
|
|
148
|
+
const graphConfig = { ...makeGraphConfig(), maxTurns: 1 };
|
|
149
|
+
const run = await createRun(graphConfig);
|
|
150
|
+
(run.Graph as StandardGraph).setSubagentModelOverride(
|
|
151
|
+
createFakeStreamingLLM({
|
|
152
|
+
responses: [
|
|
153
|
+
'coordinator plan',
|
|
154
|
+
'left research',
|
|
155
|
+
'right research',
|
|
156
|
+
'synthesized answer',
|
|
157
|
+
],
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const result = await getGraphSubagentTool(run).invoke(
|
|
162
|
+
{
|
|
163
|
+
description: 'Research and synthesize the question.',
|
|
164
|
+
subagent_type: 'research-team',
|
|
165
|
+
},
|
|
166
|
+
invokeConfig
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result).toBe('synthesized answer');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('does not fall back to worker text when the result member is textless', async () => {
|
|
173
|
+
const graphConfig: GraphSubagentConfig = {
|
|
174
|
+
...makeGraphConfig(),
|
|
175
|
+
agents: [makeAgent('worker'), makeAgent('result')],
|
|
176
|
+
edges: [{ from: 'worker', to: 'result', edgeType: 'direct' }],
|
|
177
|
+
entryAgentId: 'worker',
|
|
178
|
+
resultAgentId: 'result',
|
|
179
|
+
};
|
|
180
|
+
const run = await createRun(graphConfig);
|
|
181
|
+
(run.Graph as StandardGraph).setSubagentModelOverride(
|
|
182
|
+
createFakeStreamingLLM({ responses: ['worker text', ''] })
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const result = await getGraphSubagentTool(run).invoke(
|
|
186
|
+
{
|
|
187
|
+
description: 'Complete the two-step task.',
|
|
188
|
+
subagent_type: 'research-team',
|
|
189
|
+
},
|
|
190
|
+
invokeConfig
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result).toBe('Task completed');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('keeps each member turn budget independent from the outer graph budget', async () => {
|
|
197
|
+
const loopTool = tool(async (): Promise<string> => 'continue', {
|
|
198
|
+
name: 'loop_tool',
|
|
199
|
+
description: 'Continue the member loop.',
|
|
200
|
+
schema: z.object({}),
|
|
201
|
+
});
|
|
202
|
+
const graphConfig: GraphSubagentConfig = {
|
|
203
|
+
...makeGraphConfig(),
|
|
204
|
+
maxTurns: 1,
|
|
205
|
+
agents: [
|
|
206
|
+
{ ...makeAgent('entry'), graphTools: [loopTool] },
|
|
207
|
+
makeAgent('result'),
|
|
208
|
+
],
|
|
209
|
+
edges: [{ from: 'entry', to: 'result', edgeType: 'direct' }],
|
|
210
|
+
entryAgentId: 'entry',
|
|
211
|
+
resultAgentId: 'result',
|
|
212
|
+
};
|
|
213
|
+
const run = await createRun(graphConfig);
|
|
214
|
+
(run.Graph as StandardGraph).setSubagentModelOverride(
|
|
215
|
+
new LoopingFakeChatModel()
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const output = await getGraphSubagentTool(run).invoke(
|
|
219
|
+
{
|
|
220
|
+
description: 'Try to exceed the member turn budget.',
|
|
221
|
+
subagent_type: 'research-team',
|
|
222
|
+
},
|
|
223
|
+
invokeConfig
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(output).toMatch(/Subagent error: Recursion limit of 3 reached/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('fails closed when human-in-the-loop is enabled', async () => {
|
|
230
|
+
const run = await createRun(makeGraphConfig(), {
|
|
231
|
+
humanInTheLoop: { enabled: true },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const result = await getGraphSubagentTool(run).invoke(
|
|
235
|
+
{
|
|
236
|
+
description: 'Do not start this graph.',
|
|
237
|
+
subagent_type: 'research-team',
|
|
238
|
+
},
|
|
239
|
+
invokeConfig
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(result).toBe(
|
|
243
|
+
'Error: Human-in-the-loop execution is not yet supported for graph subagents.'
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('attributes usage to each graph member', async () => {
|
|
248
|
+
const graphConfig: GraphSubagentConfig = {
|
|
249
|
+
...makeGraphConfig(),
|
|
250
|
+
agents: [makeAgent('entry'), makeAgent('worker'), makeAgent('result')],
|
|
251
|
+
edges: [
|
|
252
|
+
{ from: 'entry', to: 'worker', edgeType: 'direct' },
|
|
253
|
+
{ from: 'worker', to: 'result', edgeType: 'direct' },
|
|
254
|
+
],
|
|
255
|
+
entryAgentId: 'entry',
|
|
256
|
+
resultAgentId: 'result',
|
|
257
|
+
};
|
|
258
|
+
const usageEvents: t.SubagentUsageEvent[] = [];
|
|
259
|
+
const run = await createRun(graphConfig, {
|
|
260
|
+
subagentUsageSink: (event) => {
|
|
261
|
+
usageEvents.push(event);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
(run.Graph as StandardGraph).setSubagentModelOverride(
|
|
265
|
+
new UsageFakeChatModel()
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
await getGraphSubagentTool(run).invoke(
|
|
269
|
+
{
|
|
270
|
+
description: 'Complete the measured graph.',
|
|
271
|
+
subagent_type: 'research-team',
|
|
272
|
+
},
|
|
273
|
+
invokeConfig
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(usageEvents).toHaveLength(3);
|
|
277
|
+
expect(
|
|
278
|
+
usageEvents.map((event) => ({
|
|
279
|
+
memberAgentId: event.memberAgentId,
|
|
280
|
+
model: event.model,
|
|
281
|
+
provider: event.provider,
|
|
282
|
+
subagentKind: event.subagentKind,
|
|
283
|
+
}))
|
|
284
|
+
).toEqual([
|
|
285
|
+
{
|
|
286
|
+
memberAgentId: 'entry',
|
|
287
|
+
model: 'entry-model',
|
|
288
|
+
provider: Providers.OPENAI,
|
|
289
|
+
subagentKind: 'graph',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
memberAgentId: 'worker',
|
|
293
|
+
model: 'worker-model',
|
|
294
|
+
provider: Providers.OPENAI,
|
|
295
|
+
subagentKind: 'graph',
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
memberAgentId: 'result',
|
|
299
|
+
model: 'result-model',
|
|
300
|
+
provider: Providers.OPENAI,
|
|
301
|
+
subagentKind: 'graph',
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
expect(
|
|
305
|
+
new Set(usageEvents.map((event) => event.subagentAgentId)).size
|
|
306
|
+
).toBe(1);
|
|
307
|
+
const rootRunId = (run.Graph as StandardGraph).runId;
|
|
308
|
+
for (const event of usageEvents) {
|
|
309
|
+
expect(event).toMatchObject({
|
|
310
|
+
runId: rootRunId,
|
|
311
|
+
parentRunId: rootRunId,
|
|
312
|
+
depth: 1,
|
|
313
|
+
});
|
|
314
|
+
expect(event.ancestry).toHaveLength(1);
|
|
315
|
+
expect(event.ancestry?.[0]).toMatchObject({
|
|
316
|
+
subagentType: 'research-team',
|
|
317
|
+
subagentKind: 'graph',
|
|
318
|
+
parentRunId: rootRunId,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -319,6 +319,50 @@ function createAgent(tenantId: string): t.AgentInputs {
|
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
function createGraphSubagentParent(tenantId: string): t.AgentInputs {
|
|
323
|
+
const makeMember = (agentId: string): t.AgentInputs => ({
|
|
324
|
+
agentId,
|
|
325
|
+
name: `${agentId} ${tenantId}`,
|
|
326
|
+
provider: Providers.OPENAI,
|
|
327
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
328
|
+
instructions: `Complete the ${agentId} stage.`,
|
|
329
|
+
maxContextTokens: 8000,
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
...makeMember('parent'),
|
|
333
|
+
maxSubagentDepth: 1,
|
|
334
|
+
subagentConfigs: [
|
|
335
|
+
{
|
|
336
|
+
kind: 'graph',
|
|
337
|
+
type: 'research-team',
|
|
338
|
+
name: 'Research Team',
|
|
339
|
+
description: 'Runs a bounded research chain.',
|
|
340
|
+
agents: [
|
|
341
|
+
makeMember('entry'),
|
|
342
|
+
makeMember('worker'),
|
|
343
|
+
makeMember('result'),
|
|
344
|
+
],
|
|
345
|
+
edges: [
|
|
346
|
+
{
|
|
347
|
+
from: 'entry',
|
|
348
|
+
to: 'worker',
|
|
349
|
+
edgeType: 'direct',
|
|
350
|
+
prompt: 'Proceed to worker.',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
from: 'worker',
|
|
354
|
+
to: 'result',
|
|
355
|
+
edgeType: 'direct',
|
|
356
|
+
prompt: 'Proceed to result.',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
entryAgentId: 'entry',
|
|
360
|
+
resultAgentId: 'result',
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
322
366
|
async function runTenantFlow(tenantId: string): Promise<void> {
|
|
323
367
|
const runId = `routing-${tenantId}`;
|
|
324
368
|
const run = await Run.create<t.IState>({
|
|
@@ -382,6 +426,49 @@ async function runTenantFlow(tenantId: string): Promise<void> {
|
|
|
382
426
|
});
|
|
383
427
|
}
|
|
384
428
|
|
|
429
|
+
async function runGraphSubagentTenantFlow(tenantId: string): Promise<void> {
|
|
430
|
+
const runId = `routing-graph-${tenantId}`;
|
|
431
|
+
const run = await Run.create<t.IState>({
|
|
432
|
+
runId,
|
|
433
|
+
graphConfig: {
|
|
434
|
+
type: 'standard',
|
|
435
|
+
agents: [createGraphSubagentParent(tenantId)],
|
|
436
|
+
},
|
|
437
|
+
langfuse: tenantLangfuse(tenantId),
|
|
438
|
+
returnContent: true,
|
|
439
|
+
skipCleanup: true,
|
|
440
|
+
});
|
|
441
|
+
run.Graph?.overrideTestModel(
|
|
442
|
+
[
|
|
443
|
+
`Delegating graph work for ${tenantId}.`,
|
|
444
|
+
`Graph work complete for ${tenantId}.`,
|
|
445
|
+
],
|
|
446
|
+
1,
|
|
447
|
+
[
|
|
448
|
+
{
|
|
449
|
+
id: `call_graph_subagent_${tenantId}`,
|
|
450
|
+
name: Constants.SUBAGENT,
|
|
451
|
+
args: {
|
|
452
|
+
description: `Run the research chain for ${tenantId}.`,
|
|
453
|
+
subagent_type: 'research-team',
|
|
454
|
+
},
|
|
455
|
+
type: 'tool_call',
|
|
456
|
+
},
|
|
457
|
+
]
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
await run.processStream(
|
|
461
|
+
{ messages: [new HumanMessage(`Run graph work for ${tenantId}`)] },
|
|
462
|
+
{
|
|
463
|
+
...callerConfig,
|
|
464
|
+
configurable: {
|
|
465
|
+
thread_id: `graph-thread-${tenantId}`,
|
|
466
|
+
user_id: `graph-user-${tenantId}`,
|
|
467
|
+
},
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
385
472
|
const compactingTokenCounter: t.TokenCounter = (message) => {
|
|
386
473
|
if (message._getType() === 'system') {
|
|
387
474
|
return 1;
|
|
@@ -454,7 +541,7 @@ describe('Langfuse per-run routing integration', () => {
|
|
|
454
541
|
.spyOn(providers, 'getChatModelClass')
|
|
455
542
|
.mockImplementation(((provider: Providers) => {
|
|
456
543
|
if (provider === Providers.OPENAI) {
|
|
457
|
-
return class extends FakeListChatModel {
|
|
544
|
+
return class RoutingProviderFakeChatModel extends FakeListChatModel {
|
|
458
545
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
459
546
|
constructor(_options: any) {
|
|
460
547
|
super({ responses: ['provider response'] });
|
|
@@ -531,6 +618,38 @@ describe('Langfuse per-run routing integration', () => {
|
|
|
531
618
|
}
|
|
532
619
|
});
|
|
533
620
|
|
|
621
|
+
it('keeps graph-subagent member spans in the owning tenant trace', async () => {
|
|
622
|
+
await Promise.all([
|
|
623
|
+
runGraphSubagentTenantFlow('graph-tenant-a'),
|
|
624
|
+
runGraphSubagentTenantFlow('graph-tenant-b'),
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
for (const tenantId of ['graph-tenant-a', 'graph-tenant-b']) {
|
|
628
|
+
const otherTenantId =
|
|
629
|
+
tenantId === 'graph-tenant-a' ? 'graph-tenant-b' : 'graph-tenant-a';
|
|
630
|
+
const starts = startsForTenant(tenantId);
|
|
631
|
+
const traceId = traceIdFromSeed(`routing-graph-${tenantId}`);
|
|
632
|
+
|
|
633
|
+
expectTenantCredentials(starts, tenantId);
|
|
634
|
+
expectOnlyTraceIds(starts, [traceId]);
|
|
635
|
+
expectNamedSpansUseTraceId({
|
|
636
|
+
starts,
|
|
637
|
+
traceId,
|
|
638
|
+
names: [
|
|
639
|
+
`LibreChat Agent: parent ${tenantId}`,
|
|
640
|
+
'FakeChatModel',
|
|
641
|
+
'subagent',
|
|
642
|
+
],
|
|
643
|
+
});
|
|
644
|
+
expect(
|
|
645
|
+
starts.filter(
|
|
646
|
+
(record) => record.name === 'RoutingProviderFakeChatModel'
|
|
647
|
+
)
|
|
648
|
+
).toHaveLength(3);
|
|
649
|
+
expectNoCrossTenantTrace({ tenantId, otherTenantId, traceId });
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
534
653
|
it('routes parallel summarization spans to each run config', async () => {
|
|
535
654
|
await Promise.all([
|
|
536
655
|
runTenantSummarizationFlow('tenant-a'),
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
AIMessageChunk,
|
|
6
6
|
HumanMessage,
|
|
7
7
|
} from '@langchain/core/messages';
|
|
8
|
+
import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
|
|
8
9
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
9
|
-
import type { UsageMetadata } from '@langchain/core/messages';
|
|
10
10
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
11
11
|
import type * as t from '@/types';
|
|
12
12
|
import {
|
|
@@ -371,6 +371,8 @@ describe('Subagent Integration', () => {
|
|
|
371
371
|
}
|
|
372
372
|
);
|
|
373
373
|
const parentUpdateHandler = jest.fn();
|
|
374
|
+
const rootModelEndHandler = jest.fn();
|
|
375
|
+
const nestedUsageEvents: t.SubagentUsageEvent[] = [];
|
|
374
376
|
let specialistToolDefinitions: t.LCTool[] | undefined;
|
|
375
377
|
let forwardedToolResults: t.ToolExecuteResult[] | undefined;
|
|
376
378
|
|
|
@@ -409,7 +411,12 @@ describe('Subagent Integration', () => {
|
|
|
409
411
|
const invokeOptions = options as
|
|
410
412
|
| { callbacks?: unknown[] }
|
|
411
413
|
| undefined;
|
|
412
|
-
const
|
|
414
|
+
const callbacks = invokeOptions?.callbacks ?? [];
|
|
415
|
+
const forwarder = callbacks.find(
|
|
416
|
+
(callback) =>
|
|
417
|
+
typeof (callback as { handleCustomEvent?: unknown })
|
|
418
|
+
.handleCustomEvent === 'function'
|
|
419
|
+
) as
|
|
413
420
|
| {
|
|
414
421
|
handleCustomEvent?: (
|
|
415
422
|
eventName: string,
|
|
@@ -418,6 +425,27 @@ describe('Subagent Integration', () => {
|
|
|
418
425
|
) => Promise<void> | void;
|
|
419
426
|
}
|
|
420
427
|
| undefined;
|
|
428
|
+
const usageHandler = callbacks.find(
|
|
429
|
+
(callback) =>
|
|
430
|
+
typeof (callback as { handleLLMEnd?: unknown })
|
|
431
|
+
.handleLLMEnd === 'function'
|
|
432
|
+
) as
|
|
433
|
+
| {
|
|
434
|
+
handleChatModelStart?: (
|
|
435
|
+
llm: unknown,
|
|
436
|
+
messages: BaseMessage[][],
|
|
437
|
+
runId: string,
|
|
438
|
+
parentRunId?: string,
|
|
439
|
+
extraParams?: unknown,
|
|
440
|
+
tags?: string[],
|
|
441
|
+
metadata?: Record<string, unknown>
|
|
442
|
+
) => Promise<void> | void;
|
|
443
|
+
handleLLMEnd?: (
|
|
444
|
+
output: unknown,
|
|
445
|
+
runId: string
|
|
446
|
+
) => Promise<void> | void;
|
|
447
|
+
}
|
|
448
|
+
| undefined;
|
|
421
449
|
if (forwarder?.handleCustomEvent != null) {
|
|
422
450
|
forwardedToolResults = await new Promise<t.ToolExecuteResult[]>(
|
|
423
451
|
(resolve, reject) => {
|
|
@@ -442,6 +470,38 @@ describe('Subagent Integration', () => {
|
|
|
442
470
|
'specialist-run'
|
|
443
471
|
);
|
|
444
472
|
}
|
|
473
|
+
await usageHandler?.handleChatModelStart?.(
|
|
474
|
+
{},
|
|
475
|
+
[[]],
|
|
476
|
+
'specialist-model-call',
|
|
477
|
+
undefined,
|
|
478
|
+
undefined,
|
|
479
|
+
undefined,
|
|
480
|
+
{
|
|
481
|
+
agentId: 'specialist',
|
|
482
|
+
ls_model_name: 'gpt-4o-mini',
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
await usageHandler?.handleLLMEnd?.(
|
|
486
|
+
{
|
|
487
|
+
generations: [
|
|
488
|
+
[
|
|
489
|
+
{
|
|
490
|
+
text: 'specialist done',
|
|
491
|
+
message: new AIMessage({
|
|
492
|
+
content: 'specialist done',
|
|
493
|
+
usage_metadata: {
|
|
494
|
+
input_tokens: 5,
|
|
495
|
+
output_tokens: 3,
|
|
496
|
+
total_tokens: 8,
|
|
497
|
+
},
|
|
498
|
+
}),
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
'specialist-model-call'
|
|
504
|
+
);
|
|
445
505
|
return { messages: [new AIMessage('specialist done')] };
|
|
446
506
|
}),
|
|
447
507
|
} as unknown as ReturnType<StandardGraph['createWorkflow']>;
|
|
@@ -492,17 +552,22 @@ describe('Subagent Integration', () => {
|
|
|
492
552
|
};
|
|
493
553
|
|
|
494
554
|
try {
|
|
555
|
+
const rootRunId = `nested-event-tools-${Date.now()}`;
|
|
495
556
|
const run = await Run.create<t.IState>({
|
|
496
|
-
runId:
|
|
557
|
+
runId: rootRunId,
|
|
497
558
|
graphConfig: { type: 'standard', agents: [rootAgent] },
|
|
498
559
|
customHandlers: {
|
|
499
560
|
[GraphEvents.ON_TOOL_EXECUTE]: { handle: parentToolHandler },
|
|
561
|
+
[GraphEvents.CHAT_MODEL_END]: { handle: rootModelEndHandler },
|
|
500
562
|
[GraphEvents.ON_SUBAGENT_UPDATE]: {
|
|
501
563
|
handle: parentUpdateHandler,
|
|
502
564
|
},
|
|
503
565
|
},
|
|
504
566
|
returnContent: true,
|
|
505
567
|
skipCleanup: true,
|
|
568
|
+
subagentUsageSink: (event) => {
|
|
569
|
+
nestedUsageEvents.push(event);
|
|
570
|
+
},
|
|
506
571
|
});
|
|
507
572
|
const rootContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
508
573
|
'root'
|
|
@@ -530,7 +595,68 @@ describe('Subagent Integration', () => {
|
|
|
530
595
|
([, data]) => (data as t.SubagentUpdateEvent).subagentType
|
|
531
596
|
);
|
|
532
597
|
expect(forwardedSubagentTypes).toContain('router');
|
|
533
|
-
expect(forwardedSubagentTypes).
|
|
598
|
+
expect(forwardedSubagentTypes).toContain('specialist');
|
|
599
|
+
const specialistStepUpdates = parentUpdateHandler.mock.calls.filter(
|
|
600
|
+
([, data]) => {
|
|
601
|
+
const event = data as t.SubagentUpdateEvent;
|
|
602
|
+
return (
|
|
603
|
+
event.subagentType === 'specialist' &&
|
|
604
|
+
(event.data as { id?: string } | undefined)?.id ===
|
|
605
|
+
'specialist-step'
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
expect(specialistStepUpdates).toHaveLength(1);
|
|
610
|
+
const routerEvent = parentUpdateHandler.mock.calls
|
|
611
|
+
.map(([, data]) => data as t.SubagentUpdateEvent)
|
|
612
|
+
.find((event) => event.subagentType === 'router');
|
|
613
|
+
const specialistEvent =
|
|
614
|
+
specialistStepUpdates[0][1] as t.SubagentUpdateEvent;
|
|
615
|
+
expect(routerEvent).toMatchObject({
|
|
616
|
+
runId: rootRunId,
|
|
617
|
+
parentRunId: rootRunId,
|
|
618
|
+
depth: 1,
|
|
619
|
+
});
|
|
620
|
+
expect(routerEvent?.ancestry?.map((entry) => entry.subagentType)).toEqual(
|
|
621
|
+
['router']
|
|
622
|
+
);
|
|
623
|
+
expect(specialistEvent).toMatchObject({
|
|
624
|
+
runId: rootRunId,
|
|
625
|
+
parentRunId: routerEvent?.subagentRunId,
|
|
626
|
+
depth: 2,
|
|
627
|
+
});
|
|
628
|
+
expect(
|
|
629
|
+
specialistEvent.ancestry?.map((entry) => ({
|
|
630
|
+
type: entry.subagentType,
|
|
631
|
+
kind: entry.subagentKind,
|
|
632
|
+
}))
|
|
633
|
+
).toEqual([
|
|
634
|
+
{ type: 'router', kind: 'agent' },
|
|
635
|
+
{ type: 'specialist', kind: 'agent' },
|
|
636
|
+
]);
|
|
637
|
+
expect(nestedUsageEvents).toHaveLength(1);
|
|
638
|
+
expect(rootModelEndHandler).not.toHaveBeenCalled();
|
|
639
|
+
expect(nestedUsageEvents[0]).toMatchObject({
|
|
640
|
+
runId: rootRunId,
|
|
641
|
+
parentRunId: routerEvent?.subagentRunId,
|
|
642
|
+
subagentType: 'specialist',
|
|
643
|
+
subagentAgentId: 'specialist',
|
|
644
|
+
depth: 2,
|
|
645
|
+
usage: {
|
|
646
|
+
input_tokens: 5,
|
|
647
|
+
output_tokens: 3,
|
|
648
|
+
total_tokens: 8,
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
expect(
|
|
652
|
+
nestedUsageEvents[0].ancestry?.map((entry) => ({
|
|
653
|
+
type: entry.subagentType,
|
|
654
|
+
kind: entry.subagentKind,
|
|
655
|
+
}))
|
|
656
|
+
).toEqual([
|
|
657
|
+
{ type: 'router', kind: 'agent' },
|
|
658
|
+
{ type: 'specialist', kind: 'agent' },
|
|
659
|
+
]);
|
|
534
660
|
} finally {
|
|
535
661
|
createWorkflowSpy.mockRestore();
|
|
536
662
|
}
|
|
@@ -5,7 +5,7 @@ import { Constants } from '@/common';
|
|
|
5
5
|
|
|
6
6
|
export const SubagentToolName = Constants.SUBAGENT;
|
|
7
7
|
|
|
8
|
-
export const SubagentToolDescription = `Delegate a task to a specialized subagent that runs in an isolated context window. The
|
|
8
|
+
export const SubagentToolDescription = `Delegate a task to a specialized subagent or bounded agent team that runs in an isolated context window. The delegated execution returns only its designated final text result — all intermediate tool calls, reasoning, and context stay isolated.
|
|
9
9
|
|
|
10
10
|
WHEN TO USE:
|
|
11
11
|
- The task is self-contained and can be described in a single prompt.
|
|
@@ -13,9 +13,9 @@ WHEN TO USE:
|
|
|
13
13
|
- A specialized subagent is available for the task domain.
|
|
14
14
|
|
|
15
15
|
WHAT HAPPENS:
|
|
16
|
-
- A fresh agent is created with the task description as its only input.
|
|
17
|
-
- The
|
|
18
|
-
- Only the final
|
|
16
|
+
- A fresh agent or configured agent graph is created with the task description as its only input.
|
|
17
|
+
- The delegated agent or team runs to completion using isolated tools and context.
|
|
18
|
+
- Only the single agent's final response or the graph's designated result-agent response is returned to you.
|
|
19
19
|
|
|
20
20
|
CONSTRAINTS:
|
|
21
21
|
- subagent_type must match one of the available types listed below.
|