@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.
Files changed (60) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +47 -18
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +1 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +69 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/types.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +12 -0
  9. package/dist/cjs/main.cjs.map +1 -1
  10. package/dist/cjs/summarization/node.cjs +44 -0
  11. package/dist/cjs/summarization/node.cjs.map +1 -1
  12. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  13. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  14. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +261 -0
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  16. package/dist/esm/agents/AgentContext.mjs +47 -18
  17. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  18. package/dist/esm/common/enum.mjs +1 -0
  19. package/dist/esm/common/enum.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +69 -0
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/hooks/types.mjs.map +1 -1
  23. package/dist/esm/main.mjs +2 -0
  24. package/dist/esm/main.mjs.map +1 -1
  25. package/dist/esm/summarization/node.mjs +44 -0
  26. package/dist/esm/summarization/node.mjs.map +1 -1
  27. package/dist/esm/tools/SubagentTool.mjs +85 -0
  28. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  29. package/dist/esm/tools/subagent/SubagentExecutor.mjs +256 -0
  30. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  31. package/dist/types/agents/AgentContext.d.ts +12 -0
  32. package/dist/types/common/enum.d.ts +2 -1
  33. package/dist/types/hooks/types.d.ts +12 -1
  34. package/dist/types/index.d.ts +2 -0
  35. package/dist/types/summarization/node.d.ts +2 -0
  36. package/dist/types/tools/SubagentTool.d.ts +36 -0
  37. package/dist/types/tools/subagent/SubagentExecutor.d.ts +83 -0
  38. package/dist/types/tools/subagent/index.d.ts +2 -0
  39. package/dist/types/types/graph.d.ts +25 -0
  40. package/dist/types/types/llm.d.ts +14 -2
  41. package/package.json +2 -1
  42. package/src/agents/AgentContext.ts +54 -17
  43. package/src/agents/__tests__/AgentContext.test.ts +110 -0
  44. package/src/common/enum.ts +1 -0
  45. package/src/graphs/Graph.ts +88 -0
  46. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  47. package/src/hooks/index.ts +4 -2
  48. package/src/hooks/types.ts +17 -1
  49. package/src/index.ts +2 -0
  50. package/src/scripts/multi-agent-subagent.ts +246 -0
  51. package/src/specs/subagent.test.ts +305 -0
  52. package/src/summarization/node.ts +53 -0
  53. package/src/tools/SubagentTool.ts +100 -0
  54. package/src/tools/__tests__/SubagentExecutor.test.ts +615 -0
  55. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  56. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  57. package/src/tools/subagent/SubagentExecutor.ts +344 -0
  58. package/src/tools/subagent/index.ts +12 -0
  59. package/src/types/graph.ts +27 -0
  60. package/src/types/llm.ts +16 -2
@@ -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 =
@@ -0,0 +1,100 @@
1
+ import { Constants } from '@/common';
2
+ import type { SubagentConfig } from '@/types';
3
+ import type { JsonSchemaType, LCTool } from '@/types/tools';
4
+
5
+ export const SubagentToolName = Constants.SUBAGENT;
6
+
7
+ export const SubagentToolDescription = `Delegate a task to a specialized subagent that runs in an isolated context window. The subagent executes independently and returns only its final text result — all intermediate tool calls, reasoning, and context stay isolated.
8
+
9
+ WHEN TO USE:
10
+ - The task is self-contained and can be described in a single prompt.
11
+ - You want to offload verbose or exploratory work without bloating your own context.
12
+ - A specialized subagent is available for the task domain.
13
+
14
+ WHAT HAPPENS:
15
+ - A fresh agent is created with the task description as its only input.
16
+ - The subagent runs to completion using its own tools and context.
17
+ - Only the final text response is returned to you.
18
+
19
+ CONSTRAINTS:
20
+ - subagent_type must match one of the available types listed below.
21
+ - The subagent cannot see your conversation history.`;
22
+
23
+ const DESCRIPTION_PROP_DESCRIPTION =
24
+ 'Complete task description for the subagent. This is the ONLY information it receives — include all necessary context, requirements, and constraints.';
25
+
26
+ const SUBAGENT_TYPE_PROP_DESCRIPTION =
27
+ 'Which subagent type to delegate to. Must be one of the available types.';
28
+
29
+ export const SubagentToolSchema = {
30
+ type: 'object',
31
+ properties: {
32
+ description: {
33
+ type: 'string',
34
+ description: DESCRIPTION_PROP_DESCRIPTION,
35
+ },
36
+ subagent_type: {
37
+ type: 'string',
38
+ description: SUBAGENT_TYPE_PROP_DESCRIPTION,
39
+ },
40
+ },
41
+ required: ['description', 'subagent_type'] as string[],
42
+ } as const;
43
+
44
+ export const SubagentToolDefinition: LCTool = {
45
+ name: SubagentToolName,
46
+ description: SubagentToolDescription,
47
+ parameters: SubagentToolSchema,
48
+ };
49
+
50
+ /**
51
+ * Build the name, schema, and description params for `tool()` from available configs.
52
+ * Used by `Graph.createAgentNode()` when constructing the runtime tool instance.
53
+ * Extends `SubagentToolSchema` by populating `subagent_type.enum` dynamically.
54
+ */
55
+ export function buildSubagentToolParams(configs: SubagentConfig[]): {
56
+ name: string;
57
+ schema: JsonSchemaType;
58
+ description: string;
59
+ } {
60
+ const types = configs.map((c) => c.type);
61
+ const typeDescriptions = configs
62
+ .map((c) => `- "${c.type}" (${c.name}): ${c.description}`)
63
+ .join('\n');
64
+
65
+ return {
66
+ name: SubagentToolName,
67
+ schema: {
68
+ type: 'object',
69
+ properties: {
70
+ description: {
71
+ type: 'string',
72
+ description: DESCRIPTION_PROP_DESCRIPTION,
73
+ },
74
+ subagent_type: {
75
+ type: 'string',
76
+ enum: types,
77
+ description: `${SUBAGENT_TYPE_PROP_DESCRIPTION} Available: ${types.join(', ')}.`,
78
+ },
79
+ },
80
+ required: ['description', 'subagent_type'],
81
+ },
82
+ description: `${SubagentToolDescription}\n\nAvailable types:\n${typeDescriptions}`,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Create a SubagentTool LCTool definition with dynamic enum and description
88
+ * populated from the available subagent configs.
89
+ * Used for the tool registry in event-driven mode.
90
+ */
91
+ export function createSubagentToolDefinition(
92
+ configs: SubagentConfig[]
93
+ ): LCTool {
94
+ const params = buildSubagentToolParams(configs);
95
+ return {
96
+ name: params.name,
97
+ description: params.description,
98
+ parameters: params.schema,
99
+ };
100
+ }