@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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
2
|
+
|
|
3
|
+
dotenvConfig(
|
|
4
|
+
process.env.LIVE_ENV_PATH == null
|
|
5
|
+
? undefined
|
|
6
|
+
: { path: process.env.LIVE_ENV_PATH }
|
|
7
|
+
);
|
|
8
|
+
dotenvConfig();
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
11
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
import { Constants, GraphEvents, Providers } from '@/common';
|
|
14
|
+
import { StandardGraph } from '@/graphs/Graph';
|
|
15
|
+
import { Run } from '@/run';
|
|
16
|
+
|
|
17
|
+
jest.setTimeout(180_000);
|
|
18
|
+
|
|
19
|
+
const liveEnabled = process.env.RUN_GRAPH_SUBAGENT_LIVE_TESTS === '1';
|
|
20
|
+
const requestedProvider = process.env.GRAPH_SUBAGENT_LIVE_PROVIDER;
|
|
21
|
+
const describeIfLive = liveEnabled ? describe : describe.skip;
|
|
22
|
+
|
|
23
|
+
type LiveProviderCase = {
|
|
24
|
+
label: string;
|
|
25
|
+
provider: Providers;
|
|
26
|
+
model: string;
|
|
27
|
+
apiKey?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const LIVE_PROVIDER_CASES: readonly LiveProviderCase[] = [
|
|
31
|
+
{
|
|
32
|
+
label: 'Anthropic Haiku',
|
|
33
|
+
provider: Providers.ANTHROPIC,
|
|
34
|
+
model:
|
|
35
|
+
process.env.ANTHROPIC_GRAPH_SUBAGENT_LIVE_MODEL ?? 'claude-haiku-4-5',
|
|
36
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'OpenAI GPT-4.1 mini',
|
|
40
|
+
provider: Providers.OPENAI,
|
|
41
|
+
model: process.env.OPENAI_GRAPH_SUBAGENT_LIVE_MODEL ?? 'gpt-4.1-mini',
|
|
42
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const explicitlyRequestedCase =
|
|
47
|
+
requestedProvider == null || requestedProvider === ''
|
|
48
|
+
? undefined
|
|
49
|
+
: LIVE_PROVIDER_CASES.find(
|
|
50
|
+
({ provider }) =>
|
|
51
|
+
provider.toLowerCase() === requestedProvider.toLowerCase()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
liveEnabled &&
|
|
56
|
+
requestedProvider != null &&
|
|
57
|
+
requestedProvider !== '' &&
|
|
58
|
+
explicitlyRequestedCase == null
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unknown GRAPH_SUBAGENT_LIVE_PROVIDER "${requestedProvider}".`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
liveEnabled &&
|
|
66
|
+
explicitlyRequestedCase != null &&
|
|
67
|
+
(explicitlyRequestedCase.apiKey == null ||
|
|
68
|
+
explicitlyRequestedCase.apiKey === '')
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Missing credentials for GRAPH_SUBAGENT_LIVE_PROVIDER "${requestedProvider}".`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
liveEnabled &&
|
|
76
|
+
explicitlyRequestedCase == null &&
|
|
77
|
+
!LIVE_PROVIDER_CASES.some(({ apiKey }) => apiKey != null && apiKey !== '')
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'No graph-subagent live-provider credentials are configured.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createLiveAgent(
|
|
85
|
+
testCase: LiveProviderCase,
|
|
86
|
+
agentId: string,
|
|
87
|
+
marker: string
|
|
88
|
+
): t.AgentInputs {
|
|
89
|
+
return {
|
|
90
|
+
agentId,
|
|
91
|
+
provider: testCase.provider,
|
|
92
|
+
clientOptions: {
|
|
93
|
+
modelName: testCase.model,
|
|
94
|
+
apiKey: testCase.apiKey,
|
|
95
|
+
temperature: 0,
|
|
96
|
+
maxTokens: 64,
|
|
97
|
+
streaming: true,
|
|
98
|
+
streamUsage: true,
|
|
99
|
+
},
|
|
100
|
+
instructions: `You are the ${agentId} validation stage. When asked to proceed, reply with exactly ${marker} and no other text.`,
|
|
101
|
+
maxContextTokens: 8_000,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getGraphSubagentTool(run: Run<t.IState>): t.GenericTool {
|
|
106
|
+
const tools = (run.Graph as StandardGraph).agentContexts.get('parent')
|
|
107
|
+
?.graphTools as t.GenericTool[] | undefined;
|
|
108
|
+
const subagentTool = tools?.find(
|
|
109
|
+
(tool) => 'name' in tool && tool.name === Constants.SUBAGENT
|
|
110
|
+
);
|
|
111
|
+
if (subagentTool == null) {
|
|
112
|
+
throw new Error('Expected graph subagent tool');
|
|
113
|
+
}
|
|
114
|
+
return subagentTool;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describeIfLive('Graph subagent live providers', () => {
|
|
118
|
+
for (const testCase of LIVE_PROVIDER_CASES) {
|
|
119
|
+
const providerSelected =
|
|
120
|
+
requestedProvider == null ||
|
|
121
|
+
requestedProvider === '' ||
|
|
122
|
+
requestedProvider.toLowerCase() === testCase.provider.toLowerCase();
|
|
123
|
+
const itIfAvailable =
|
|
124
|
+
providerSelected && testCase.apiKey != null && testCase.apiKey !== ''
|
|
125
|
+
? it
|
|
126
|
+
: it.skip;
|
|
127
|
+
|
|
128
|
+
itIfAvailable(
|
|
129
|
+
`${testCase.label} runs a bounded team and returns only its result member`,
|
|
130
|
+
async () => {
|
|
131
|
+
const usageEvents: t.SubagentUsageEvent[] = [];
|
|
132
|
+
const updateEvents: t.SubagentUpdateEvent[] = [];
|
|
133
|
+
const entry = createLiveAgent(testCase, 'entry', 'ENTRY_MARKER');
|
|
134
|
+
const worker = createLiveAgent(testCase, 'worker', 'WORKER_MARKER');
|
|
135
|
+
const resultAgent = createLiveAgent(testCase, 'result', 'FINAL_MARKER');
|
|
136
|
+
const parent: t.AgentInputs = {
|
|
137
|
+
...createLiveAgent(testCase, 'parent', 'PARENT_MARKER'),
|
|
138
|
+
maxSubagentDepth: 1,
|
|
139
|
+
subagentConfigs: [
|
|
140
|
+
{
|
|
141
|
+
kind: 'graph',
|
|
142
|
+
type: 'live-team',
|
|
143
|
+
name: 'Live Team',
|
|
144
|
+
description: 'Runs a three-member live-provider chain.',
|
|
145
|
+
maxTurns: 2,
|
|
146
|
+
agents: [entry, worker, resultAgent],
|
|
147
|
+
edges: [
|
|
148
|
+
{
|
|
149
|
+
from: 'entry',
|
|
150
|
+
to: 'worker',
|
|
151
|
+
edgeType: 'direct',
|
|
152
|
+
prompt: 'Proceed with the worker validation stage.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
from: 'worker',
|
|
156
|
+
to: 'result',
|
|
157
|
+
edgeType: 'direct',
|
|
158
|
+
prompt: 'Proceed with the result validation stage.',
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
entryAgentId: 'entry',
|
|
162
|
+
resultAgentId: 'result',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
const rootRunId = `graph-subagent-live-${testCase.provider}-${Date.now()}`;
|
|
167
|
+
const run = await Run.create<t.IState>({
|
|
168
|
+
runId: rootRunId,
|
|
169
|
+
graphConfig: { type: 'standard', agents: [parent] },
|
|
170
|
+
returnContent: true,
|
|
171
|
+
skipCleanup: true,
|
|
172
|
+
subagentUsageSink: (event) => {
|
|
173
|
+
usageEvents.push(event);
|
|
174
|
+
},
|
|
175
|
+
customHandlers: {
|
|
176
|
+
[GraphEvents.ON_SUBAGENT_UPDATE]: {
|
|
177
|
+
handle: (_event, data): void => {
|
|
178
|
+
updateEvents.push(data as t.SubagentUpdateEvent);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const invokeConfig: RunnableConfig = {
|
|
184
|
+
configurable: {
|
|
185
|
+
thread_id: `graph-subagent-live-${testCase.provider}`,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const output = await getGraphSubagentTool(run).invoke(
|
|
190
|
+
{
|
|
191
|
+
description: 'Begin the entry validation stage.',
|
|
192
|
+
subagent_type: 'live-team',
|
|
193
|
+
},
|
|
194
|
+
invokeConfig
|
|
195
|
+
);
|
|
196
|
+
const outputText = String(output);
|
|
197
|
+
|
|
198
|
+
expect(outputText).toContain('FINAL_MARKER');
|
|
199
|
+
expect(outputText).not.toContain('ENTRY_MARKER');
|
|
200
|
+
expect(outputText).not.toContain('WORKER_MARKER');
|
|
201
|
+
expect(usageEvents.map((event) => event.memberAgentId).sort()).toEqual([
|
|
202
|
+
'entry',
|
|
203
|
+
'result',
|
|
204
|
+
'worker',
|
|
205
|
+
]);
|
|
206
|
+
for (const event of usageEvents) {
|
|
207
|
+
expect(event.runId).toBe(rootRunId);
|
|
208
|
+
expect(event.parentRunId).toBe(rootRunId);
|
|
209
|
+
expect(event.depth).toBe(1);
|
|
210
|
+
expect(event.ancestry?.map((entry) => entry.subagentType)).toEqual([
|
|
211
|
+
'live-team',
|
|
212
|
+
]);
|
|
213
|
+
expect(event.subagentKind).toBe('graph');
|
|
214
|
+
expect(event.provider).toBe(testCase.provider);
|
|
215
|
+
expect(event.model).toBeTruthy();
|
|
216
|
+
expect(event.usage.total_tokens).toBeGreaterThan(0);
|
|
217
|
+
}
|
|
218
|
+
expect(
|
|
219
|
+
updateEvents.filter((event) => event.phase === 'start')
|
|
220
|
+
).toHaveLength(1);
|
|
221
|
+
expect(
|
|
222
|
+
updateEvents.filter((event) => event.phase === 'stop')
|
|
223
|
+
).toHaveLength(1);
|
|
224
|
+
for (const event of updateEvents) {
|
|
225
|
+
expect(event.runId).toBe(rootRunId);
|
|
226
|
+
expect(event.parentRunId).toBe(rootRunId);
|
|
227
|
+
expect(event.depth).toBe(1);
|
|
228
|
+
expect(event.ancestry?.map((entry) => entry.subagentType)).toEqual([
|
|
229
|
+
'live-team',
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
expect(
|
|
233
|
+
new Set(
|
|
234
|
+
updateEvents
|
|
235
|
+
.map((event) => event.memberAgentId)
|
|
236
|
+
.filter((agentId): agentId is string => agentId != null)
|
|
237
|
+
)
|
|
238
|
+
).toEqual(new Set(['entry', 'worker', 'result']));
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
@@ -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'),
|