@librechat/agents 3.1.67 → 3.1.68-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 +16 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +91 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -0
- package/dist/cjs/graphs/MultiAgentGraph.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 +54 -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/index.cjs +41 -0
- package/dist/cjs/summarization/index.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +165 -19
- 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 +511 -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 +15 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +91 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
- package/dist/esm/graphs/MultiAgentGraph.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 +13 -2
- 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/index.mjs +41 -1
- package/dist/esm/summarization/index.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +165 -19
- 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 +505 -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 +10 -1
- package/dist/types/graphs/Graph.d.ts +2 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +12 -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/index.d.ts +2 -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 +137 -0
- package/dist/types/tools/subagent/index.d.ts +2 -0
- package/dist/types/types/graph.d.ts +61 -2
- package/dist/types/types/index.d.ts +1 -0
- 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 +5 -1
- package/src/agents/AgentContext.ts +26 -2
- package/src/common/enum.ts +15 -0
- package/src/graphs/Graph.ts +113 -0
- package/src/graphs/MultiAgentGraph.ts +39 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -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/scripts/subagent-event-driven-debug.ts +190 -0
- package/src/scripts/subagent-tools-debug.ts +160 -0
- package/src/specs/subagent.test.ts +305 -0
- package/src/summarization/__tests__/node.test.ts +42 -0
- package/src/summarization/__tests__/trigger.test.ts +100 -1
- package/src/summarization/index.ts +47 -0
- package/src/summarization/node.ts +202 -24
- 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 +1148 -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 +676 -0
- package/src/tools/subagent/index.ts +13 -0
- package/src/types/graph.ts +80 -1
- package/src/types/index.ts +1 -0
- package/src/types/run.ts +20 -0
- package/src/types/skill.ts +11 -0
- package/src/types/tools.ts +41 -1
|
@@ -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
|
+
});
|
|
@@ -343,6 +343,48 @@ describe('createSummarizeNode', () => {
|
|
|
343
343
|
).toBeUndefined();
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
it('catches model initialization errors and falls back to metadata stub', async () => {
|
|
347
|
+
captureEvents();
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Simulate the "Unsupported LLM provider" case — e.g. when a caller
|
|
351
|
+
* forwards an unrecognized provider name (custom-endpoint label) that
|
|
352
|
+
* getChatModelClass cannot resolve. Prior to the defense-in-depth fix,
|
|
353
|
+
* this error was thrown outside the try/catch in executeSummarizationWithFallback
|
|
354
|
+
* and bubbled up silently. Now it is caught and the metadata stub is used.
|
|
355
|
+
*/
|
|
356
|
+
jest.spyOn(providers, 'getChatModelClass').mockImplementation(() => {
|
|
357
|
+
throw new Error('Unsupported LLM provider: Ollama');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const setSummary = jest.fn();
|
|
361
|
+
const agentContext = createAgentContext({ setSummary } as never);
|
|
362
|
+
const graph = mockGraph();
|
|
363
|
+
const node = createSummarizeNode({
|
|
364
|
+
agentContext,
|
|
365
|
+
graph,
|
|
366
|
+
generateStepId,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await expect(
|
|
370
|
+
node(
|
|
371
|
+
{
|
|
372
|
+
messages: [new HumanMessage('Test message')],
|
|
373
|
+
summarizationRequest: {
|
|
374
|
+
remainingContextTokens: 1000,
|
|
375
|
+
agentId: 'agent_0',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{} as RunnableConfig
|
|
379
|
+
)
|
|
380
|
+
).resolves.not.toThrow();
|
|
381
|
+
|
|
382
|
+
expect(setSummary).toHaveBeenCalledWith(
|
|
383
|
+
expect.stringContaining('[Metadata summary:'),
|
|
384
|
+
expect.any(Number)
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
346
388
|
it('falls back to metadata stub when primary LLM call fails', async () => {
|
|
347
389
|
captureEvents();
|
|
348
390
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
shouldTriggerSummarization,
|
|
3
|
+
_resetUnrecognizedTriggerWarnings,
|
|
4
|
+
} from '@/summarization';
|
|
2
5
|
|
|
3
6
|
describe('shouldTriggerSummarization', () => {
|
|
4
7
|
it('uses pre-prune pressure for token_ratio triggers when messages were pruned', () => {
|
|
@@ -47,4 +50,100 @@ describe('shouldTriggerSummarization', () => {
|
|
|
47
50
|
|
|
48
51
|
expect(result).toBe(false);
|
|
49
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('unrecognized trigger type', () => {
|
|
55
|
+
let warnSpy: jest.SpyInstance;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
_resetUnrecognizedTriggerWarnings();
|
|
59
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
warnSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not fire and warns once per unrecognized type', () => {
|
|
67
|
+
const baseParams = {
|
|
68
|
+
maxContextTokens: 2500,
|
|
69
|
+
prePruneContextTokens: 2400,
|
|
70
|
+
remainingContextTokens: 100,
|
|
71
|
+
messagesToRefineCount: 4,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Cast via `unknown` because the type union guards against this at compile
|
|
75
|
+
// time; we are intentionally exercising the runtime fallback.
|
|
76
|
+
const result1 = shouldTriggerSummarization({
|
|
77
|
+
...baseParams,
|
|
78
|
+
trigger: { type: 'token_count', value: 8000 } as unknown as {
|
|
79
|
+
type: 'token_ratio';
|
|
80
|
+
value: number;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result1).toBe(false);
|
|
85
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(warnSpy.mock.calls[0][0]).toContain('token_count');
|
|
87
|
+
expect(warnSpy.mock.calls[0][0]).toContain('token_ratio');
|
|
88
|
+
expect(warnSpy.mock.calls[0][0]).toContain('remaining_tokens');
|
|
89
|
+
expect(warnSpy.mock.calls[0][0]).toContain('messages_to_refine');
|
|
90
|
+
|
|
91
|
+
// Same unrecognized type a second time: no duplicate warning.
|
|
92
|
+
shouldTriggerSummarization({
|
|
93
|
+
...baseParams,
|
|
94
|
+
trigger: { type: 'token_count', value: 8000 } as unknown as {
|
|
95
|
+
type: 'token_ratio';
|
|
96
|
+
value: number;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
100
|
+
|
|
101
|
+
// Different unrecognized type: warns again, once.
|
|
102
|
+
shouldTriggerSummarization({
|
|
103
|
+
...baseParams,
|
|
104
|
+
trigger: { type: 'nonsense', value: 1 } as unknown as {
|
|
105
|
+
type: 'token_ratio';
|
|
106
|
+
value: number;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(warnSpy.mock.calls[1][0]).toContain('nonsense');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not grow memory unboundedly under a flood of unique types', () => {
|
|
114
|
+
const baseParams = {
|
|
115
|
+
maxContextTokens: 2500,
|
|
116
|
+
prePruneContextTokens: 2400,
|
|
117
|
+
remainingContextTokens: 100,
|
|
118
|
+
messagesToRefineCount: 4,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < 500; i++) {
|
|
122
|
+
shouldTriggerSummarization({
|
|
123
|
+
...baseParams,
|
|
124
|
+
trigger: { type: `bogus-${i}`, value: 1 } as unknown as {
|
|
125
|
+
type: 'token_ratio';
|
|
126
|
+
value: number;
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Still logged each new type (up to the cap) — we never silently dropped
|
|
132
|
+
// warnings; we just evicted oldest entries from the dedup set.
|
|
133
|
+
expect(warnSpy).toHaveBeenCalledTimes(500);
|
|
134
|
+
|
|
135
|
+
// Re-warns for a recently-seen type that should still be in the cache
|
|
136
|
+
// (last one just inserted). No duplicate warning means the dedup set
|
|
137
|
+
// still functions; the size cap did not break the dedup contract.
|
|
138
|
+
const beforeRecent = warnSpy.mock.calls.length;
|
|
139
|
+
shouldTriggerSummarization({
|
|
140
|
+
...baseParams,
|
|
141
|
+
trigger: { type: 'bogus-499', value: 1 } as unknown as {
|
|
142
|
+
type: 'token_ratio';
|
|
143
|
+
value: number;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
expect(warnSpy).toHaveBeenCalledTimes(beforeRecent);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
50
149
|
});
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
import type { SummarizationTrigger } from '@/types';
|
|
2
2
|
|
|
3
|
+
const VALID_TRIGGER_TYPES = [
|
|
4
|
+
'token_ratio',
|
|
5
|
+
'remaining_tokens',
|
|
6
|
+
'messages_to_refine',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upper bound on the dedup set for unrecognized trigger types. Bounds memory in
|
|
11
|
+
* case a caller threads dynamic/user-provided strings through `trigger.type`.
|
|
12
|
+
* Well above the handful of legit misconfigurations a process would ever see.
|
|
13
|
+
*/
|
|
14
|
+
const MAX_WARNED_TRIGGER_TYPES = 32;
|
|
15
|
+
|
|
16
|
+
const warnedUnrecognizedTriggerTypes = new Set<string>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Warn (once per process, per unrecognized type) when the configured trigger
|
|
20
|
+
* type is something the runtime does not evaluate. Without this, a misconfigured
|
|
21
|
+
* `trigger.type` silently disables summarization with no visible signal.
|
|
22
|
+
*
|
|
23
|
+
* The dedup set is size-capped; on overflow we evict the oldest entry (Set
|
|
24
|
+
* preserves insertion order) so we keep bounded memory and still warn on
|
|
25
|
+
* recently-seen types.
|
|
26
|
+
*/
|
|
27
|
+
function warnUnrecognizedTriggerType(type: string): void {
|
|
28
|
+
if (warnedUnrecognizedTriggerTypes.has(type)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (warnedUnrecognizedTriggerTypes.size >= MAX_WARNED_TRIGGER_TYPES) {
|
|
32
|
+
const oldest = warnedUnrecognizedTriggerTypes.values().next().value;
|
|
33
|
+
if (oldest !== undefined) {
|
|
34
|
+
warnedUnrecognizedTriggerTypes.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
warnedUnrecognizedTriggerTypes.add(type);
|
|
38
|
+
console.warn(
|
|
39
|
+
`[shouldTriggerSummarization] Unrecognized trigger.type: "${type}". ` +
|
|
40
|
+
`Summarization will not fire. Valid values: ${VALID_TRIGGER_TYPES.join(', ')}.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** For tests only. Resets the dedup set so warnings can be observed again. */
|
|
45
|
+
export function _resetUnrecognizedTriggerWarnings(): void {
|
|
46
|
+
warnedUnrecognizedTriggerTypes.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
3
49
|
/**
|
|
4
50
|
* Determines whether summarization should be triggered based on the configured trigger
|
|
5
51
|
* and current context state.
|
|
@@ -98,5 +144,6 @@ export function shouldTriggerSummarization(params: {
|
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
// Unrecognized trigger type: cannot evaluate, do not fire.
|
|
147
|
+
warnUnrecognizedTriggerType(trigger.type);
|
|
101
148
|
return false;
|
|
102
149
|
}
|