@librechat/agents 3.1.66 → 3.1.67-dev.0
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 +23 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +14 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +72 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/HookRegistry.cjs +162 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
- package/dist/cjs/hooks/executeHooks.cjs +276 -0
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
- package/dist/cjs/hooks/matchers.cjs +256 -0
- package/dist/cjs/hooks/matchers.cjs.map +1 -0
- package/dist/cjs/hooks/types.cjs +27 -0
- package/dist/cjs/hooks/types.cjs.map +1 -0
- package/dist/cjs/main.cjs +52 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +74 -12
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +111 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +44 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +175 -0
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ReadFile.cjs +43 -0
- package/dist/cjs/tools/ReadFile.cjs.map +1 -0
- package/dist/cjs/tools/SkillTool.cjs +50 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -0
- package/dist/cjs/tools/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +304 -140
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/skillCatalog.cjs +84 -0
- package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +261 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +23 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +13 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +72 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/HookRegistry.mjs +160 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
- package/dist/esm/hooks/executeHooks.mjs +273 -0
- package/dist/esm/hooks/executeHooks.mjs.map +1 -0
- package/dist/esm/hooks/matchers.mjs +251 -0
- package/dist/esm/hooks/matchers.mjs.map +1 -0
- package/dist/esm/hooks/types.mjs +25 -0
- package/dist/esm/hooks/types.mjs.map +1 -0
- package/dist/esm/main.mjs +12 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +66 -4
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +111 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +44 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +169 -0
- package/dist/esm/tools/BashExecutor.mjs.map +1 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ReadFile.mjs +38 -0
- package/dist/esm/tools/ReadFile.mjs.map +1 -0
- package/dist/esm/tools/SkillTool.mjs +45 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -0
- package/dist/esm/tools/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +306 -142
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/skillCatalog.mjs +82 -0
- package/dist/esm/tools/skillCatalog.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +256 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +6 -0
- package/dist/types/common/enum.d.ts +8 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +56 -0
- package/dist/types/hooks/executeHooks.d.ts +79 -0
- package/dist/types/hooks/index.d.ts +6 -0
- package/dist/types/hooks/matchers.d.ts +95 -0
- package/dist/types/hooks/types.d.ts +320 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/messages/format.d.ts +2 -1
- package/dist/types/run.d.ts +1 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/BashExecutor.d.ts +45 -0
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
- package/dist/types/tools/ReadFile.d.ts +28 -0
- package/dist/types/tools/SkillTool.d.ts +40 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -0
- package/dist/types/tools/ToolNode.d.ts +24 -2
- package/dist/types/tools/skillCatalog.d.ts +19 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +83 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/types/graph.d.ts +25 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/llm.d.ts +14 -2
- package/dist/types/types/run.d.ts +20 -0
- package/dist/types/types/skill.d.ts +9 -0
- package/dist/types/types/tools.d.ts +38 -1
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +13 -0
- package/src/graphs/Graph.ts +92 -0
- package/src/hooks/HookRegistry.ts +208 -0
- package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
- package/src/hooks/__tests__/integration.test.ts +337 -0
- package/src/hooks/__tests__/matchers.test.ts +238 -0
- package/src/hooks/__tests__/toolHooks.test.ts +669 -0
- package/src/hooks/executeHooks.ts +375 -0
- package/src/hooks/index.ts +57 -0
- package/src/hooks/matchers.ts +280 -0
- package/src/hooks/types.ts +404 -0
- package/src/index.ts +10 -0
- package/src/messages/format.ts +74 -4
- package/src/messages/formatAgentMessages.skills.test.ts +334 -0
- package/src/run.ts +126 -0
- package/src/scripts/multi-agent-subagent.ts +246 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/node.ts +53 -0
- package/src/tools/BashExecutor.ts +205 -0
- package/src/tools/BashProgrammaticToolCalling.ts +397 -0
- package/src/tools/ReadFile.ts +39 -0
- package/src/tools/SkillTool.ts +46 -0
- package/src/tools/SubagentTool.ts +100 -0
- package/src/tools/ToolNode.ts +391 -169
- package/src/tools/__tests__/ReadFile.test.ts +44 -0
- package/src/tools/__tests__/SkillTool.test.ts +442 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
- package/src/tools/__tests__/skillCatalog.test.ts +161 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -0
- package/src/tools/skillCatalog.ts +126 -0
- package/src/tools/subagent/SubagentExecutor.ts +344 -0
- package/src/tools/subagent/index.ts +12 -0
- package/src/types/graph.ts +27 -0
- package/src/types/index.ts +1 -0
- package/src/types/llm.ts +16 -2
- package/src/types/run.ts +20 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +41 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
config();
|
|
3
|
+
|
|
4
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
5
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
6
|
+
import type * as t from '@/types';
|
|
7
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
8
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
9
|
+
import { Providers, GraphEvents, Constants } from '@/common';
|
|
10
|
+
import { Run } from '@/run';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manual verification script for the subagent primitive.
|
|
14
|
+
*
|
|
15
|
+
* Configures a supervisor agent with two subagent types (researcher, coder),
|
|
16
|
+
* sends a query, and confirms:
|
|
17
|
+
* 1. The parent agent delegates to a subagent via the `subagent` tool
|
|
18
|
+
* 2. The child executes with isolated context (fresh message history)
|
|
19
|
+
* 3. Only the filtered text result returns to the parent
|
|
20
|
+
* 4. The parent incorporates the result and responds
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* OPENAI_API_KEY=... npx ts-node -r tsconfig-paths/register src/scripts/multi-agent-subagent.ts
|
|
24
|
+
*
|
|
25
|
+
* Or with Anthropic:
|
|
26
|
+
* ANTHROPIC_API_KEY=... npx ts-node -r tsconfig-paths/register src/scripts/multi-agent-subagent.ts --provider anthropic
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const useAnthropic =
|
|
30
|
+
process.argv.includes('--provider') &&
|
|
31
|
+
process.argv[process.argv.indexOf('--provider') + 1] === 'anthropic';
|
|
32
|
+
|
|
33
|
+
const provider = useAnthropic ? Providers.ANTHROPIC : Providers.OPENAI;
|
|
34
|
+
const apiKey = useAnthropic
|
|
35
|
+
? process.env.ANTHROPIC_API_KEY
|
|
36
|
+
: process.env.OPENAI_API_KEY;
|
|
37
|
+
const modelName = useAnthropic ? 'claude-sonnet-4-20250514' : 'gpt-5.4';
|
|
38
|
+
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
console.error(
|
|
41
|
+
`Missing ${useAnthropic ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'} environment variable`
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function testSubagentPrimitive() {
|
|
47
|
+
console.log('=== Subagent Primitive Manual Verification ===\n');
|
|
48
|
+
console.log(`Provider: ${provider}`);
|
|
49
|
+
console.log(`Model: ${modelName}\n`);
|
|
50
|
+
|
|
51
|
+
const { aggregateContent } = createContentAggregator();
|
|
52
|
+
|
|
53
|
+
const parentAgent: t.AgentInputs = {
|
|
54
|
+
agentId: 'supervisor',
|
|
55
|
+
provider,
|
|
56
|
+
clientOptions: { modelName, apiKey },
|
|
57
|
+
instructions: `You are a supervisor agent. You have access to specialized subagents.
|
|
58
|
+
|
|
59
|
+
When the user asks a research question, delegate it to the "researcher" subagent.
|
|
60
|
+
When the user asks for code, delegate it to the "coder" subagent.
|
|
61
|
+
|
|
62
|
+
After receiving the subagent's result, synthesize it into a clear final answer for the user.
|
|
63
|
+
Always use a subagent for research or coding tasks — do not answer directly.`,
|
|
64
|
+
maxContextTokens: 16000,
|
|
65
|
+
subagentConfigs: [
|
|
66
|
+
{
|
|
67
|
+
type: 'researcher',
|
|
68
|
+
name: 'Research Specialist',
|
|
69
|
+
description:
|
|
70
|
+
'Researches topics and provides detailed summaries with sources.',
|
|
71
|
+
agentInputs: {
|
|
72
|
+
agentId: 'researcher',
|
|
73
|
+
provider,
|
|
74
|
+
clientOptions: { modelName, apiKey },
|
|
75
|
+
instructions: `You are a research specialist working in an isolated context.
|
|
76
|
+
You receive a single task description and must answer it thoroughly.
|
|
77
|
+
Be concise but comprehensive. Include key facts and details.`,
|
|
78
|
+
maxContextTokens: 8000,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'coder',
|
|
83
|
+
name: 'Coding Specialist',
|
|
84
|
+
description:
|
|
85
|
+
'Writes, reviews, and explains code in any programming language.',
|
|
86
|
+
agentInputs: {
|
|
87
|
+
agentId: 'coder',
|
|
88
|
+
provider,
|
|
89
|
+
clientOptions: { modelName, apiKey },
|
|
90
|
+
instructions: `You are a coding specialist working in an isolated context.
|
|
91
|
+
You receive a single task description and must provide working code.
|
|
92
|
+
Include brief explanations. Use clean, idiomatic code.`,
|
|
93
|
+
maxContextTokens: 8000,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const customHandlers: Record<string, t.EventHandler> = {
|
|
100
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
101
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
102
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
103
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
104
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
105
|
+
aggregateContent({
|
|
106
|
+
event: event as GraphEvents,
|
|
107
|
+
data: data as t.RunStep,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
112
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
113
|
+
aggregateContent({
|
|
114
|
+
event: event as GraphEvents,
|
|
115
|
+
data: data as t.RunStep,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
120
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
121
|
+
aggregateContent({
|
|
122
|
+
event: event as GraphEvents,
|
|
123
|
+
data: data as t.RunStepDeltaEvent,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
128
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
129
|
+
aggregateContent({
|
|
130
|
+
event: event as GraphEvents,
|
|
131
|
+
data: data as t.MessageDeltaEvent,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const run = await Run.create<t.IState>({
|
|
138
|
+
runId: `subagent-manual-${Date.now()}`,
|
|
139
|
+
graphConfig: {
|
|
140
|
+
type: 'standard',
|
|
141
|
+
agents: [parentAgent],
|
|
142
|
+
},
|
|
143
|
+
returnContent: true,
|
|
144
|
+
customHandlers,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log('--- Run created ---');
|
|
148
|
+
console.log(
|
|
149
|
+
`Subagent tool present: ${
|
|
150
|
+
(
|
|
151
|
+
(run.Graph as import('@/graphs/Graph').StandardGraph).agentContexts.get(
|
|
152
|
+
'supervisor'
|
|
153
|
+
)?.graphTools as t.GenericTool[] | undefined
|
|
154
|
+
)?.some((t) => 'name' in t && t.name === Constants.SUBAGENT) ?? false
|
|
155
|
+
}\n`
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const conversationHistory: BaseMessage[] = [];
|
|
159
|
+
|
|
160
|
+
// Turn 1: Research question (should delegate to researcher subagent)
|
|
161
|
+
console.log('=== Turn 1: Research Question ===\n');
|
|
162
|
+
console.log(
|
|
163
|
+
'User: What are the three laws of thermodynamics? Explain briefly.\n'
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const userMessage = new HumanMessage(
|
|
167
|
+
'What are the three laws of thermodynamics? Explain briefly.'
|
|
168
|
+
);
|
|
169
|
+
conversationHistory.push(userMessage);
|
|
170
|
+
|
|
171
|
+
const callerConfig = {
|
|
172
|
+
configurable: { thread_id: 'subagent-verify' },
|
|
173
|
+
streamMode: 'values' as const,
|
|
174
|
+
version: 'v2' as const,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
console.log('--- Streaming response ---\n');
|
|
178
|
+
const result = await run.processStream(
|
|
179
|
+
{ messages: conversationHistory },
|
|
180
|
+
callerConfig
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const runMessages = run.getRunMessages();
|
|
184
|
+
console.log('\n\n--- Run Messages ---\n');
|
|
185
|
+
|
|
186
|
+
if (runMessages) {
|
|
187
|
+
for (const msg of runMessages) {
|
|
188
|
+
const type = msg._getType();
|
|
189
|
+
if (type === 'tool') {
|
|
190
|
+
const name = 'name' in msg ? msg.name : 'unknown';
|
|
191
|
+
const rawContent =
|
|
192
|
+
typeof msg.content === 'string'
|
|
193
|
+
? msg.content
|
|
194
|
+
: JSON.stringify(msg.content);
|
|
195
|
+
const content = rawContent.slice(0, 200);
|
|
196
|
+
const truncated = rawContent.length > 200 ? '...' : '';
|
|
197
|
+
console.log(`[ToolMessage] name=${name}`);
|
|
198
|
+
console.log(` content: ${content}${truncated}\n`);
|
|
199
|
+
} else if (type === 'ai') {
|
|
200
|
+
const content =
|
|
201
|
+
typeof msg.content === 'string'
|
|
202
|
+
? msg.content.slice(0, 300)
|
|
203
|
+
: JSON.stringify(msg.content).slice(0, 300);
|
|
204
|
+
const toolCalls = 'tool_calls' in msg ? msg.tool_calls : undefined;
|
|
205
|
+
console.log(`[AIMessage]`);
|
|
206
|
+
if (toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
207
|
+
for (const tc of toolCalls) {
|
|
208
|
+
console.log(
|
|
209
|
+
` tool_call: ${tc.name}(${JSON.stringify(tc.args).slice(0, 100)}...)`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
console.log(
|
|
214
|
+
` content: ${content}${content.length >= 300 ? '...' : ''}\n`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const subagentToolMessages = runMessages.filter(
|
|
220
|
+
(msg) =>
|
|
221
|
+
msg._getType() === 'tool' &&
|
|
222
|
+
'name' in msg &&
|
|
223
|
+
msg.name === Constants.SUBAGENT
|
|
224
|
+
);
|
|
225
|
+
console.log(`\n--- Verification ---`);
|
|
226
|
+
console.log(`Subagent tool calls found: ${subagentToolMessages.length}`);
|
|
227
|
+
console.log(`Total run messages: ${runMessages.length}`);
|
|
228
|
+
console.log(`Result content parts: ${result?.length ?? 0}`);
|
|
229
|
+
|
|
230
|
+
if (subagentToolMessages.length > 0) {
|
|
231
|
+
console.log(
|
|
232
|
+
'\nSUCCESS: Subagent was invoked and returned a filtered result.'
|
|
233
|
+
);
|
|
234
|
+
console.log(
|
|
235
|
+
'The child context was isolated — only the final text came back.'
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
console.log('\nNOTE: No subagent tool calls detected.');
|
|
239
|
+
console.log('The LLM may have answered directly without delegating.');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log('\n=== Done ===');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
testSubagentPrimitive().catch(console.error);
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
2
|
+
import { FakeListChatModel } from '@langchain/core/utils/testing';
|
|
3
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
4
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
5
|
+
import type * as t from '@/types';
|
|
6
|
+
import { Run } from '@/run';
|
|
7
|
+
import {
|
|
8
|
+
Constants,
|
|
9
|
+
GraphEvents,
|
|
10
|
+
Providers,
|
|
11
|
+
ToolEndHandler,
|
|
12
|
+
ModelEndHandler,
|
|
13
|
+
StandardGraph,
|
|
14
|
+
} from '@/index';
|
|
15
|
+
import * as providers from '@/llm/providers';
|
|
16
|
+
|
|
17
|
+
const CHILD_RESPONSE = 'Research result: Paris is the capital of France.';
|
|
18
|
+
|
|
19
|
+
const callerConfig: Partial<RunnableConfig> & {
|
|
20
|
+
version: 'v1' | 'v2';
|
|
21
|
+
streamMode: string;
|
|
22
|
+
} = {
|
|
23
|
+
configurable: { thread_id: 'subagent-test-thread' },
|
|
24
|
+
streamMode: 'values',
|
|
25
|
+
version: 'v2' as const,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createParentAgent = (): t.AgentInputs => ({
|
|
29
|
+
agentId: 'parent',
|
|
30
|
+
provider: Providers.OPENAI,
|
|
31
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
32
|
+
instructions:
|
|
33
|
+
'You are a supervisor. Delegate research tasks using the subagent tool.',
|
|
34
|
+
maxContextTokens: 8000,
|
|
35
|
+
subagentConfigs: [
|
|
36
|
+
{
|
|
37
|
+
type: 'researcher',
|
|
38
|
+
name: 'Research Agent',
|
|
39
|
+
description: 'Researches and summarizes information',
|
|
40
|
+
agentInputs: {
|
|
41
|
+
agentId: 'researcher',
|
|
42
|
+
provider: Providers.OPENAI,
|
|
43
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
44
|
+
instructions: 'You are a research agent. Answer concisely.',
|
|
45
|
+
maxContextTokens: 8000,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Subagent Integration', () => {
|
|
52
|
+
jest.setTimeout(30000);
|
|
53
|
+
|
|
54
|
+
let getChatModelClassSpy: jest.SpyInstance;
|
|
55
|
+
const originalGetChatModelClass = providers.getChatModelClass;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
getChatModelClassSpy = jest
|
|
59
|
+
.spyOn(providers, 'getChatModelClass')
|
|
60
|
+
.mockImplementation(((provider: Providers) => {
|
|
61
|
+
if (provider === Providers.OPENAI) {
|
|
62
|
+
return class extends FakeListChatModel {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
constructor(_options: any) {
|
|
65
|
+
super({ responses: [CHILD_RESPONSE] });
|
|
66
|
+
}
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
} as any;
|
|
69
|
+
}
|
|
70
|
+
return originalGetChatModelClass(provider);
|
|
71
|
+
}) as typeof providers.getChatModelClass);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
getChatModelClassSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create subagent tool on agent context', async () => {
|
|
79
|
+
const run = await Run.create<t.IState>({
|
|
80
|
+
runId: `subagent-test-${Date.now()}`,
|
|
81
|
+
graphConfig: {
|
|
82
|
+
type: 'standard',
|
|
83
|
+
agents: [createParentAgent()],
|
|
84
|
+
},
|
|
85
|
+
returnContent: true,
|
|
86
|
+
skipCleanup: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(run.Graph).toBeDefined();
|
|
90
|
+
const parentContext = (run.Graph as StandardGraph).agentContexts.get(
|
|
91
|
+
'parent'
|
|
92
|
+
);
|
|
93
|
+
expect(parentContext).toBeDefined();
|
|
94
|
+
expect(parentContext?.graphTools).toBeDefined();
|
|
95
|
+
|
|
96
|
+
const subagentTool = (parentContext?.graphTools as t.GenericTool[]).find(
|
|
97
|
+
(t) => 'name' in t && t.name === Constants.SUBAGENT
|
|
98
|
+
);
|
|
99
|
+
expect(subagentTool).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should execute subagent and return filtered result to parent', async () => {
|
|
103
|
+
const customHandlers: Record<string, t.EventHandler> = {
|
|
104
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
105
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const run = await Run.create<t.IState>({
|
|
109
|
+
runId: `subagent-exec-${Date.now()}`,
|
|
110
|
+
graphConfig: {
|
|
111
|
+
type: 'standard',
|
|
112
|
+
agents: [createParentAgent()],
|
|
113
|
+
},
|
|
114
|
+
returnContent: true,
|
|
115
|
+
skipCleanup: true,
|
|
116
|
+
customHandlers,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const subagentToolCall: ToolCall = {
|
|
120
|
+
id: 'call_subagent_1',
|
|
121
|
+
name: Constants.SUBAGENT,
|
|
122
|
+
args: {
|
|
123
|
+
description: 'What is the capital of France?',
|
|
124
|
+
subagent_type: 'researcher',
|
|
125
|
+
},
|
|
126
|
+
type: 'tool_call',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
run.Graph?.overrideTestModel(
|
|
130
|
+
[
|
|
131
|
+
'Let me delegate this research task.',
|
|
132
|
+
`Based on the research: ${CHILD_RESPONSE}`,
|
|
133
|
+
],
|
|
134
|
+
10,
|
|
135
|
+
[subagentToolCall]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = await run.processStream(
|
|
139
|
+
{ messages: [new HumanMessage('What is the capital of France?')] },
|
|
140
|
+
callerConfig
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result).toBeDefined();
|
|
144
|
+
|
|
145
|
+
const runMessages = run.getRunMessages();
|
|
146
|
+
expect(runMessages).toBeDefined();
|
|
147
|
+
expect(runMessages!.length).toBeGreaterThan(0);
|
|
148
|
+
|
|
149
|
+
const toolMessages = runMessages!.filter(
|
|
150
|
+
(msg) => msg._getType() === 'tool'
|
|
151
|
+
);
|
|
152
|
+
const subagentResult = toolMessages.find(
|
|
153
|
+
(msg) => 'name' in msg && msg.name === Constants.SUBAGENT
|
|
154
|
+
);
|
|
155
|
+
expect(subagentResult).toBeDefined();
|
|
156
|
+
expect(String(subagentResult!.content)).toContain('Paris');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should not create subagent tool when no subagentConfigs', async () => {
|
|
160
|
+
const agentWithoutSubagents: t.AgentInputs = {
|
|
161
|
+
agentId: 'plain',
|
|
162
|
+
provider: Providers.OPENAI,
|
|
163
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
164
|
+
instructions: 'Plain agent without subagents.',
|
|
165
|
+
maxContextTokens: 8000,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const run = await Run.create<t.IState>({
|
|
169
|
+
runId: `no-subagent-${Date.now()}`,
|
|
170
|
+
graphConfig: {
|
|
171
|
+
type: 'standard',
|
|
172
|
+
agents: [agentWithoutSubagents],
|
|
173
|
+
},
|
|
174
|
+
returnContent: true,
|
|
175
|
+
skipCleanup: true,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const context = (run.Graph as StandardGraph).agentContexts.get('plain');
|
|
179
|
+
const tools = context?.graphTools as t.GenericTool[] | undefined;
|
|
180
|
+
const subagentTool = tools?.find(
|
|
181
|
+
(t) => 'name' in t && t.name === Constants.SUBAGENT
|
|
182
|
+
);
|
|
183
|
+
expect(subagentTool).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle self-spawn subagent config', async () => {
|
|
187
|
+
const agentWithSelfSpawn: t.AgentInputs = {
|
|
188
|
+
agentId: 'self-parent',
|
|
189
|
+
provider: Providers.OPENAI,
|
|
190
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
191
|
+
instructions: 'Agent with self-spawn for context isolation.',
|
|
192
|
+
maxContextTokens: 8000,
|
|
193
|
+
subagentConfigs: [
|
|
194
|
+
{
|
|
195
|
+
type: 'isolated',
|
|
196
|
+
name: 'Isolated Worker',
|
|
197
|
+
description: 'Runs a task with isolated context',
|
|
198
|
+
self: true,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const run = await Run.create<t.IState>({
|
|
204
|
+
runId: `self-spawn-${Date.now()}`,
|
|
205
|
+
graphConfig: {
|
|
206
|
+
type: 'standard',
|
|
207
|
+
agents: [agentWithSelfSpawn],
|
|
208
|
+
},
|
|
209
|
+
returnContent: true,
|
|
210
|
+
skipCleanup: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const context = (run.Graph as StandardGraph).agentContexts.get(
|
|
214
|
+
'self-parent'
|
|
215
|
+
);
|
|
216
|
+
const tools = context?.graphTools as t.GenericTool[] | undefined;
|
|
217
|
+
const subagentTool = tools?.find(
|
|
218
|
+
(t) => 'name' in t && t.name === Constants.SUBAGENT
|
|
219
|
+
);
|
|
220
|
+
expect(subagentTool).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should not create subagent tool when maxSubagentDepth is 0', async () => {
|
|
224
|
+
const agentWithZeroDepth: t.AgentInputs = {
|
|
225
|
+
...createParentAgent(),
|
|
226
|
+
agentId: 'zero-depth',
|
|
227
|
+
maxSubagentDepth: 0,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const run = await Run.create<t.IState>({
|
|
231
|
+
runId: `zero-depth-${Date.now()}`,
|
|
232
|
+
graphConfig: {
|
|
233
|
+
type: 'standard',
|
|
234
|
+
agents: [agentWithZeroDepth],
|
|
235
|
+
},
|
|
236
|
+
returnContent: true,
|
|
237
|
+
skipCleanup: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const context = (run.Graph as StandardGraph).agentContexts.get(
|
|
241
|
+
'zero-depth'
|
|
242
|
+
);
|
|
243
|
+
const tools = context?.graphTools as t.GenericTool[] | undefined;
|
|
244
|
+
const subagentTool = tools?.find(
|
|
245
|
+
(t) => 'name' in t && t.name === Constants.SUBAGENT
|
|
246
|
+
);
|
|
247
|
+
expect(subagentTool).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should account for subagent tool schema in toolSchemaTokens', async () => {
|
|
251
|
+
/** Simple char-count tokenizer — deterministic, lets us assert presence. */
|
|
252
|
+
const tokenCounter: t.TokenCounter = (message) => {
|
|
253
|
+
const content = message.content;
|
|
254
|
+
if (typeof content === 'string') return content.length;
|
|
255
|
+
if (Array.isArray(content)) return JSON.stringify(content).length;
|
|
256
|
+
return JSON.stringify(content).length;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const agentWithSubagent = createParentAgent();
|
|
260
|
+
const runWith = await Run.create<t.IState>({
|
|
261
|
+
runId: `with-sub-${Date.now()}`,
|
|
262
|
+
graphConfig: {
|
|
263
|
+
type: 'standard',
|
|
264
|
+
agents: [agentWithSubagent],
|
|
265
|
+
},
|
|
266
|
+
tokenCounter,
|
|
267
|
+
returnContent: true,
|
|
268
|
+
skipCleanup: true,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const agentWithoutSubagent: t.AgentInputs = {
|
|
272
|
+
agentId: 'plain',
|
|
273
|
+
provider: Providers.OPENAI,
|
|
274
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
275
|
+
instructions:
|
|
276
|
+
'You are a supervisor. Delegate research tasks using the subagent tool.',
|
|
277
|
+
maxContextTokens: 8000,
|
|
278
|
+
};
|
|
279
|
+
const runWithout = await Run.create<t.IState>({
|
|
280
|
+
runId: `without-sub-${Date.now()}`,
|
|
281
|
+
graphConfig: {
|
|
282
|
+
type: 'standard',
|
|
283
|
+
agents: [agentWithoutSubagent],
|
|
284
|
+
},
|
|
285
|
+
tokenCounter,
|
|
286
|
+
returnContent: true,
|
|
287
|
+
skipCleanup: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const contextWith = (runWith.Graph as StandardGraph).agentContexts.get(
|
|
291
|
+
'parent'
|
|
292
|
+
);
|
|
293
|
+
const contextWithout = (
|
|
294
|
+
runWithout.Graph as StandardGraph
|
|
295
|
+
).agentContexts.get('plain');
|
|
296
|
+
|
|
297
|
+
await contextWith?.tokenCalculationPromise;
|
|
298
|
+
await contextWithout?.tokenCalculationPromise;
|
|
299
|
+
|
|
300
|
+
/** Subagent tool schema is ~600 chars; expect measurable difference. */
|
|
301
|
+
expect(contextWith!.toolSchemaTokens).toBeGreaterThan(
|
|
302
|
+
contextWithout!.toolSchemaTokens
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
8
8
|
import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
|
|
9
9
|
import type { AgentContext } from '@/agents/AgentContext';
|
|
10
|
+
import type { HookRegistry } from '@/hooks';
|
|
10
11
|
import type { OnChunk } from '@/llm/invoke';
|
|
11
12
|
import type * as t from '@/types';
|
|
12
13
|
import { ContentTypes, GraphEvents, StepTypes, Providers } from '@/common';
|
|
@@ -17,6 +18,7 @@ import { getMaxOutputTokensKey } from '@/llm/request';
|
|
|
17
18
|
import { addCacheControl } from '@/messages/cache';
|
|
18
19
|
import { initializeModel } from '@/llm/init';
|
|
19
20
|
import { getChunkContent } from '@/stream';
|
|
21
|
+
import { executeHooks } from '@/hooks';
|
|
20
22
|
|
|
21
23
|
const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
|
|
22
24
|
|
|
@@ -530,6 +532,35 @@ async function dispatchCompletionEvents(params: {
|
|
|
530
532
|
);
|
|
531
533
|
}
|
|
532
534
|
|
|
535
|
+
const sessionId = graph.runId ?? '';
|
|
536
|
+
if (graph.hookRegistry?.hasHookFor('PostCompact', sessionId) === true) {
|
|
537
|
+
const threadId = (
|
|
538
|
+
runnableConfig?.configurable as Record<string, unknown> | undefined
|
|
539
|
+
)?.thread_id as string | undefined;
|
|
540
|
+
const firstBlock = summaryBlock.content?.[0];
|
|
541
|
+
const summaryText =
|
|
542
|
+
firstBlock != null &&
|
|
543
|
+
typeof firstBlock === 'object' &&
|
|
544
|
+
'text' in firstBlock &&
|
|
545
|
+
typeof firstBlock.text === 'string'
|
|
546
|
+
? firstBlock.text
|
|
547
|
+
: '';
|
|
548
|
+
await executeHooks({
|
|
549
|
+
registry: graph.hookRegistry,
|
|
550
|
+
input: {
|
|
551
|
+
hook_event_name: 'PostCompact',
|
|
552
|
+
runId: sessionId,
|
|
553
|
+
threadId,
|
|
554
|
+
agentId,
|
|
555
|
+
summary: summaryText,
|
|
556
|
+
messagesAfterCount: 0,
|
|
557
|
+
},
|
|
558
|
+
sessionId,
|
|
559
|
+
}).catch(() => {
|
|
560
|
+
/* PostCompact is observational — swallow errors */
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
533
564
|
agentContext.rebuildTokenMapAfterSummarization({});
|
|
534
565
|
}
|
|
535
566
|
|
|
@@ -545,6 +576,7 @@ interface CreateSummarizeNodeParams {
|
|
|
545
576
|
config?: RunnableConfig;
|
|
546
577
|
runId?: string;
|
|
547
578
|
isMultiAgent: boolean;
|
|
579
|
+
hookRegistry?: HookRegistry;
|
|
548
580
|
dispatchRunStep: (
|
|
549
581
|
runStep: t.RunStep,
|
|
550
582
|
config?: RunnableConfig
|
|
@@ -650,6 +682,27 @@ export function createSummarizeNode({
|
|
|
650
682
|
);
|
|
651
683
|
}
|
|
652
684
|
|
|
685
|
+
const sessionId = graph.runId ?? '';
|
|
686
|
+
if (graph.hookRegistry?.hasHookFor('PreCompact', sessionId) === true) {
|
|
687
|
+
const threadId = (
|
|
688
|
+
runnableConfig?.configurable as Record<string, unknown> | undefined
|
|
689
|
+
)?.thread_id as string | undefined;
|
|
690
|
+
await executeHooks({
|
|
691
|
+
registry: graph.hookRegistry,
|
|
692
|
+
input: {
|
|
693
|
+
hook_event_name: 'PreCompact',
|
|
694
|
+
runId: sessionId,
|
|
695
|
+
threadId,
|
|
696
|
+
agentId: request.agentId,
|
|
697
|
+
messagesBeforeCount: messagesToRefine.length,
|
|
698
|
+
trigger: agentContext.summarizationConfig?.trigger?.type ?? 'default',
|
|
699
|
+
},
|
|
700
|
+
sessionId,
|
|
701
|
+
}).catch(() => {
|
|
702
|
+
/* PreCompact is observational — swallow errors */
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
653
706
|
const isSelfSummarizeModel =
|
|
654
707
|
clientConfig.provider === (agentContext.provider as string);
|
|
655
708
|
const hasPromptCache =
|