@librechat/agents 3.1.66-dev.0 → 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 +47 -18
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +69 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/main.cjs +12 -0
- package/dist/cjs/main.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/SubagentTool.cjs +92 -0
- package/dist/cjs/tools/SubagentTool.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 +47 -18
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +69 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -0
- package/dist/esm/main.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/SubagentTool.mjs +85 -0
- package/dist/esm/tools/SubagentTool.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 +12 -0
- package/dist/types/common/enum.d.ts +2 -1
- package/dist/types/hooks/types.d.ts +12 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/summarization/node.d.ts +2 -0
- package/dist/types/tools/SubagentTool.d.ts +36 -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/llm.d.ts +14 -2
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +54 -17
- package/src/agents/__tests__/AgentContext.test.ts +110 -0
- package/src/common/enum.ts +1 -0
- package/src/graphs/Graph.ts +88 -0
- package/src/hooks/__tests__/compactHooks.test.ts +214 -0
- package/src/hooks/index.ts +4 -2
- package/src/hooks/types.ts +17 -1
- package/src/index.ts +2 -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/SubagentTool.ts +100 -0
- package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
- package/src/tools/__tests__/SubagentTool.test.ts +149 -0
- package/src/tools/__tests__/subagentHooks.test.ts +215 -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/llm.ts +16 -2
package/src/graphs/Graph.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { nanoid } from 'nanoid';
|
|
3
|
+
import { tool } from '@langchain/core/tools';
|
|
3
4
|
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
|
4
5
|
import { Runnable, RunnableConfig } from '@langchain/core/runnables';
|
|
5
6
|
import { ToolMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
joinKeys,
|
|
40
41
|
sleep,
|
|
41
42
|
} from '@/utils';
|
|
43
|
+
import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
|
|
44
|
+
import { buildSubagentToolParams } from '@/tools/SubagentTool';
|
|
42
45
|
import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
43
46
|
import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
|
|
44
47
|
import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
|
|
@@ -1152,6 +1155,90 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1152
1155
|
throw new Error(`Agent context not found for agentId: ${agentId}`);
|
|
1153
1156
|
}
|
|
1154
1157
|
|
|
1158
|
+
/**
|
|
1159
|
+
* Depth countdown across graph boundaries: the parent's `maxSubagentDepth`
|
|
1160
|
+
* becomes this executor's `maxDepth`. When the child graph is constructed,
|
|
1161
|
+
* `buildChildInputs()` decrements `maxSubagentDepth` on the child's
|
|
1162
|
+
* `AgentInputs` (only when `allowNested: true`; otherwise subagentConfigs
|
|
1163
|
+
* are stripped entirely). The child graph's own `createAgentNode()` then
|
|
1164
|
+
* reads the decremented value here and creates a narrower executor —
|
|
1165
|
+
* recursion is bounded even though each graph has its own separate
|
|
1166
|
+
* executor instance.
|
|
1167
|
+
*/
|
|
1168
|
+
const effectiveSubagentDepth = agentContext.maxSubagentDepth ?? 1;
|
|
1169
|
+
if (
|
|
1170
|
+
agentContext.subagentConfigs != null &&
|
|
1171
|
+
agentContext.subagentConfigs.length > 0 &&
|
|
1172
|
+
effectiveSubagentDepth > 0
|
|
1173
|
+
) {
|
|
1174
|
+
const resolvedConfigs = resolveSubagentConfigs(
|
|
1175
|
+
agentContext.subagentConfigs,
|
|
1176
|
+
agentContext
|
|
1177
|
+
);
|
|
1178
|
+
if (resolvedConfigs.length > 0) {
|
|
1179
|
+
const executor = new SubagentExecutor({
|
|
1180
|
+
configs: new Map(resolvedConfigs.map((c) => [c.type, c])),
|
|
1181
|
+
parentSignal: this.signal,
|
|
1182
|
+
hookRegistry: this.hookRegistry,
|
|
1183
|
+
parentRunId: this.runId ?? '',
|
|
1184
|
+
parentAgentId: agentContext.agentId,
|
|
1185
|
+
tokenCounter: agentContext.tokenCounter,
|
|
1186
|
+
maxDepth: effectiveSubagentDepth,
|
|
1187
|
+
createChildGraph: (input): StandardGraph => new StandardGraph(input),
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
const subagentTool = tool(async (rawInput, config) => {
|
|
1191
|
+
const input = rawInput as {
|
|
1192
|
+
description?: string;
|
|
1193
|
+
subagent_type?: string;
|
|
1194
|
+
};
|
|
1195
|
+
const description =
|
|
1196
|
+
typeof input.description === 'string' &&
|
|
1197
|
+
input.description.trim().length > 0
|
|
1198
|
+
? input.description
|
|
1199
|
+
: 'No task description provided';
|
|
1200
|
+
const subagentType =
|
|
1201
|
+
typeof input.subagent_type === 'string' ? input.subagent_type : '';
|
|
1202
|
+
const threadId = config.configurable?.thread_id as string | undefined;
|
|
1203
|
+
const result = await executor.execute({
|
|
1204
|
+
description,
|
|
1205
|
+
subagentType,
|
|
1206
|
+
threadId,
|
|
1207
|
+
});
|
|
1208
|
+
return result.content;
|
|
1209
|
+
}, buildSubagentToolParams(resolvedConfigs));
|
|
1210
|
+
|
|
1211
|
+
if (!agentContext.graphTools) {
|
|
1212
|
+
agentContext.graphTools = [];
|
|
1213
|
+
}
|
|
1214
|
+
(agentContext.graphTools as t.GenericTool[]).push(subagentTool);
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Refresh toolSchemaTokens to include the subagent tool's schema.
|
|
1218
|
+
* `calculateInstructionTokens()` was kicked off in `fromConfig()`
|
|
1219
|
+
* before graphTools was populated, so its result did not count this
|
|
1220
|
+
* tool. Without this retrigger, token-budget/pruning logic
|
|
1221
|
+
* underestimates prompt overhead.
|
|
1222
|
+
*/
|
|
1223
|
+
if (agentContext.tokenCounter) {
|
|
1224
|
+
const { tokenCounter, baseIndexTokenCountMap } = agentContext;
|
|
1225
|
+
agentContext.tokenCalculationPromise = agentContext
|
|
1226
|
+
.calculateInstructionTokens(tokenCounter)
|
|
1227
|
+
.then(() => {
|
|
1228
|
+
agentContext.updateTokenMapWithInstructions(
|
|
1229
|
+
baseIndexTokenCountMap
|
|
1230
|
+
);
|
|
1231
|
+
})
|
|
1232
|
+
.catch((err) => {
|
|
1233
|
+
console.error(
|
|
1234
|
+
'Error recalculating instruction tokens after subagent tool injection:',
|
|
1235
|
+
err
|
|
1236
|
+
);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1155
1242
|
const agentNode = `${AGENT}${agentId}` as const;
|
|
1156
1243
|
const toolNode = `${TOOLS}${agentId}` as const;
|
|
1157
1244
|
const summarizeNode = `${SUMMARIZE}${agentId}` as const;
|
|
@@ -1207,6 +1294,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1207
1294
|
},
|
|
1208
1295
|
runId: this.runId,
|
|
1209
1296
|
isMultiAgent: this.isMultiAgentGraph(),
|
|
1297
|
+
hookRegistry: this.hookRegistry,
|
|
1210
1298
|
dispatchRunStep: async (runStep, nodeConfig) => {
|
|
1211
1299
|
this.contentData.push(runStep);
|
|
1212
1300
|
this.contentIndexMap.set(runStep.id, runStep.index);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// src/hooks/__tests__/compactHooks.test.ts
|
|
2
|
+
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
|
3
|
+
import { FakeListChatModel } from '@langchain/core/utils/testing';
|
|
4
|
+
import { HookRegistry } from '../HookRegistry';
|
|
5
|
+
import { Run } from '@/run';
|
|
6
|
+
import * as providers from '@/llm/providers';
|
|
7
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
8
|
+
import { createTokenCounter } from '@/utils/tokens';
|
|
9
|
+
import { Providers, GraphEvents } from '@/common';
|
|
10
|
+
import type * as t from '@/types';
|
|
11
|
+
import type {
|
|
12
|
+
HookCallback,
|
|
13
|
+
PreCompactHookInput,
|
|
14
|
+
PreCompactHookOutput,
|
|
15
|
+
PostCompactHookInput,
|
|
16
|
+
PostCompactHookOutput,
|
|
17
|
+
} from '../types';
|
|
18
|
+
|
|
19
|
+
const SUMMARY_RESPONSE = '## Summary\nUser asked a question and got an answer.';
|
|
20
|
+
|
|
21
|
+
const callerConfig = {
|
|
22
|
+
configurable: { thread_id: 'compact-test' },
|
|
23
|
+
streamMode: 'values' as const,
|
|
24
|
+
version: 'v2' as const,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let getChatModelClassSpy: jest.SpyInstance;
|
|
28
|
+
const originalGetChatModelClass = providers.getChatModelClass;
|
|
29
|
+
|
|
30
|
+
function mockSummarizationModel(): void {
|
|
31
|
+
getChatModelClassSpy = jest
|
|
32
|
+
.spyOn(providers, 'getChatModelClass')
|
|
33
|
+
.mockImplementation(((provider: Providers) => {
|
|
34
|
+
if (provider === Providers.OPENAI) {
|
|
35
|
+
return class extends FakeListChatModel {
|
|
36
|
+
constructor(
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
_options: any
|
|
39
|
+
) {
|
|
40
|
+
super({ responses: [SUMMARY_RESPONSE] });
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
} as any;
|
|
44
|
+
}
|
|
45
|
+
return originalGetChatModelClass(provider);
|
|
46
|
+
}) as typeof providers.getChatModelClass);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildConversation(): t.IState {
|
|
50
|
+
const messages: import('@langchain/core/messages').BaseMessage[] = [];
|
|
51
|
+
for (let i = 0; i < 20; i++) {
|
|
52
|
+
messages.push(new HumanMessage(`Question ${i}: ` + 'padding '.repeat(50)));
|
|
53
|
+
messages.push(new AIMessage(`Answer ${i}: ` + 'response '.repeat(50)));
|
|
54
|
+
}
|
|
55
|
+
return { messages };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createCompactingRun(
|
|
59
|
+
tokenCounter: t.TokenCounter,
|
|
60
|
+
hooks?: HookRegistry,
|
|
61
|
+
runId = 'compact-run'
|
|
62
|
+
): Promise<Run<t.IState>> {
|
|
63
|
+
const conversation = buildConversation();
|
|
64
|
+
const indexTokenCountMap: Record<string, number> = {};
|
|
65
|
+
for (let i = 0; i < conversation.messages.length; i++) {
|
|
66
|
+
indexTokenCountMap[String(i)] = tokenCounter(conversation.messages[i]);
|
|
67
|
+
}
|
|
68
|
+
return Run.create<t.IState>({
|
|
69
|
+
runId,
|
|
70
|
+
graphConfig: {
|
|
71
|
+
type: 'standard',
|
|
72
|
+
llmConfig: {
|
|
73
|
+
provider: Providers.OPENAI,
|
|
74
|
+
streaming: true,
|
|
75
|
+
streamUsage: false,
|
|
76
|
+
},
|
|
77
|
+
instructions: 'Be concise.',
|
|
78
|
+
maxContextTokens: 200,
|
|
79
|
+
summarizationEnabled: true,
|
|
80
|
+
summarizationConfig: {
|
|
81
|
+
provider: Providers.OPENAI,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
returnContent: true,
|
|
85
|
+
skipCleanup: true,
|
|
86
|
+
customHandlers: {
|
|
87
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
88
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
89
|
+
},
|
|
90
|
+
hooks,
|
|
91
|
+
tokenCounter,
|
|
92
|
+
indexTokenCountMap,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('Compaction hook integration', () => {
|
|
97
|
+
jest.setTimeout(30_000);
|
|
98
|
+
|
|
99
|
+
let tokenCounter: t.TokenCounter;
|
|
100
|
+
|
|
101
|
+
beforeAll(async () => {
|
|
102
|
+
tokenCounter = await createTokenCounter();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
mockSummarizationModel();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterEach(() => {
|
|
110
|
+
getChatModelClassSpy.mockRestore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('PreCompact', () => {
|
|
114
|
+
it('fires with messagesBeforeCount and trigger', async () => {
|
|
115
|
+
const registry = new HookRegistry();
|
|
116
|
+
let captured: PreCompactHookInput | undefined;
|
|
117
|
+
const hook: HookCallback<'PreCompact'> = async (
|
|
118
|
+
input
|
|
119
|
+
): Promise<PreCompactHookOutput> => {
|
|
120
|
+
captured = input;
|
|
121
|
+
return {};
|
|
122
|
+
};
|
|
123
|
+
registry.register('PreCompact', { hooks: [hook] });
|
|
124
|
+
|
|
125
|
+
const run = await createCompactingRun(tokenCounter, registry);
|
|
126
|
+
run.Graph!.overrideTestModel(['Final answer after compaction.']);
|
|
127
|
+
const inputs = buildConversation();
|
|
128
|
+
await run.processStream(inputs, callerConfig);
|
|
129
|
+
|
|
130
|
+
expect(captured).toBeDefined();
|
|
131
|
+
expect(captured!.hook_event_name).toBe('PreCompact');
|
|
132
|
+
expect(captured!.messagesBeforeCount).toBeGreaterThan(0);
|
|
133
|
+
expect(captured!.runId).toBe('compact-run');
|
|
134
|
+
expect(captured!.threadId).toBe('compact-test');
|
|
135
|
+
expect(captured!.trigger).toBe('default');
|
|
136
|
+
expect(captured!.agentId).toBeDefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('PostCompact', () => {
|
|
141
|
+
it('fires with summary text after compaction', async () => {
|
|
142
|
+
const registry = new HookRegistry();
|
|
143
|
+
let captured: PostCompactHookInput | undefined;
|
|
144
|
+
const hook: HookCallback<'PostCompact'> = async (
|
|
145
|
+
input
|
|
146
|
+
): Promise<PostCompactHookOutput> => {
|
|
147
|
+
captured = input;
|
|
148
|
+
return {};
|
|
149
|
+
};
|
|
150
|
+
registry.register('PostCompact', { hooks: [hook] });
|
|
151
|
+
|
|
152
|
+
const run = await createCompactingRun(tokenCounter, registry);
|
|
153
|
+
run.Graph!.overrideTestModel(['Final answer after compaction.']);
|
|
154
|
+
const inputs = buildConversation();
|
|
155
|
+
await run.processStream(inputs, callerConfig);
|
|
156
|
+
|
|
157
|
+
expect(captured).toBeDefined();
|
|
158
|
+
expect(captured!.hook_event_name).toBe('PostCompact');
|
|
159
|
+
expect(captured!.threadId).toBe('compact-test');
|
|
160
|
+
expect(captured!.summary).toBe(SUMMARY_RESPONSE);
|
|
161
|
+
expect(captured!.messagesAfterCount).toBe(0);
|
|
162
|
+
expect(captured!.agentId).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('error resilience', () => {
|
|
167
|
+
it('throwing PreCompact hook does not crash compaction', async () => {
|
|
168
|
+
const registry = new HookRegistry();
|
|
169
|
+
const throwingHook: HookCallback<
|
|
170
|
+
'PreCompact'
|
|
171
|
+
> = async (): Promise<PreCompactHookOutput> => {
|
|
172
|
+
throw new Error('pre hook crash');
|
|
173
|
+
};
|
|
174
|
+
registry.register('PreCompact', { hooks: [throwingHook] });
|
|
175
|
+
|
|
176
|
+
const run = await createCompactingRun(tokenCounter, registry);
|
|
177
|
+
run.Graph!.overrideTestModel(['Answer after compaction.']);
|
|
178
|
+
const inputs = buildConversation();
|
|
179
|
+
|
|
180
|
+
await expect(
|
|
181
|
+
run.processStream(inputs, callerConfig)
|
|
182
|
+
).resolves.not.toThrow();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('throwing PostCompact hook does not crash compaction', async () => {
|
|
186
|
+
const registry = new HookRegistry();
|
|
187
|
+
const throwingHook: HookCallback<
|
|
188
|
+
'PostCompact'
|
|
189
|
+
> = async (): Promise<PostCompactHookOutput> => {
|
|
190
|
+
throw new Error('post hook crash');
|
|
191
|
+
};
|
|
192
|
+
registry.register('PostCompact', { hooks: [throwingHook] });
|
|
193
|
+
|
|
194
|
+
const run = await createCompactingRun(tokenCounter, registry);
|
|
195
|
+
run.Graph!.overrideTestModel(['Answer after compaction.']);
|
|
196
|
+
const inputs = buildConversation();
|
|
197
|
+
|
|
198
|
+
await expect(
|
|
199
|
+
run.processStream(inputs, callerConfig)
|
|
200
|
+
).resolves.not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('no-hooks baseline', () => {
|
|
205
|
+
it('summarization works identically without hooks', async () => {
|
|
206
|
+
const run = await createCompactingRun(tokenCounter);
|
|
207
|
+
run.Graph!.overrideTestModel(['Answer.']);
|
|
208
|
+
const inputs = buildConversation();
|
|
209
|
+
const result = await run.processStream(inputs, callerConfig);
|
|
210
|
+
|
|
211
|
+
expect(result).toBeDefined();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Hook lifecycle system for `@librechat/agents`. Re-exported from
|
|
4
4
|
// `src/index.ts` and consumed by `Run.processStream` (RunStart,
|
|
5
|
-
// UserPromptSubmit, Stop, StopFailure)
|
|
6
|
-
// (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied)
|
|
5
|
+
// UserPromptSubmit, Stop, StopFailure), `ToolNode.dispatchToolEvents`
|
|
6
|
+
// (PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied),
|
|
7
|
+
// `createSummarizeNode` (PreCompact, PostCompact), and
|
|
8
|
+
// `SubagentExecutor.execute` (SubagentStart, SubagentStop).
|
|
7
9
|
export { HookRegistry } from './HookRegistry';
|
|
8
10
|
export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
|
|
9
11
|
export {
|
package/src/hooks/types.ts
CHANGED
|
@@ -139,12 +139,28 @@ export interface StopFailureHookInput extends BaseHookInput {
|
|
|
139
139
|
export interface PreCompactHookInput extends BaseHookInput {
|
|
140
140
|
hook_event_name: 'PreCompact';
|
|
141
141
|
messagesBeforeCount: number;
|
|
142
|
-
|
|
142
|
+
/**
|
|
143
|
+
* What triggered compaction. Matches `SummarizationTrigger.type` from the
|
|
144
|
+
* agent's summarization config. `'default'` means no trigger was
|
|
145
|
+
* configured and compaction fired because messages were pruned.
|
|
146
|
+
*/
|
|
147
|
+
trigger:
|
|
148
|
+
| 'token_ratio'
|
|
149
|
+
| 'remaining_tokens'
|
|
150
|
+
| 'messages_to_refine'
|
|
151
|
+
| 'default'
|
|
152
|
+
| (string & {});
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
export interface PostCompactHookInput extends BaseHookInput {
|
|
146
156
|
hook_event_name: 'PostCompact';
|
|
147
157
|
summary: string;
|
|
158
|
+
/**
|
|
159
|
+
* Number of messages remaining after compaction. The summarize node
|
|
160
|
+
* returns a `removeAll` signal that clears all messages from state;
|
|
161
|
+
* the summary itself is injected into the system prompt, not as a
|
|
162
|
+
* message. This is `0` at the point of hook dispatch.
|
|
163
|
+
*/
|
|
148
164
|
messagesAfterCount: number;
|
|
149
165
|
}
|
|
150
166
|
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export * from './tools/BashExecutor';
|
|
|
18
18
|
export * from './tools/ProgrammaticToolCalling';
|
|
19
19
|
export * from './tools/BashProgrammaticToolCalling';
|
|
20
20
|
export * from './tools/SkillTool';
|
|
21
|
+
export * from './tools/SubagentTool';
|
|
22
|
+
export * from './tools/subagent';
|
|
21
23
|
export * from './tools/ReadFile';
|
|
22
24
|
export * from './tools/skillCatalog';
|
|
23
25
|
export * from './tools/ToolSearch';
|
|
@@ -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);
|