@librechat/agents 3.1.68 → 3.1.71-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 (192) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +23 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +16 -1
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +136 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  9. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  11. package/dist/cjs/hooks/matchers.cjs +256 -0
  12. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  13. package/dist/cjs/hooks/types.cjs +27 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -0
  15. package/dist/cjs/main.cjs +57 -0
  16. package/dist/cjs/main.cjs.map +1 -1
  17. package/dist/cjs/messages/format.cjs +74 -12
  18. package/dist/cjs/messages/format.cjs.map +1 -1
  19. package/dist/cjs/messages/prune.cjs +9 -2
  20. package/dist/cjs/messages/prune.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +115 -0
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/summarization/node.cjs +44 -0
  24. package/dist/cjs/summarization/node.cjs.map +1 -1
  25. package/dist/cjs/tools/BashExecutor.cjs +208 -0
  26. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  27. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
  28. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  29. package/dist/cjs/tools/CodeExecutor.cjs +0 -9
  30. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  31. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
  32. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  33. package/dist/cjs/tools/ReadFile.cjs +43 -0
  34. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  35. package/dist/cjs/tools/SkillTool.cjs +50 -0
  36. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  37. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  38. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  39. package/dist/cjs/tools/ToolNode.cjs +746 -174
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolSearch.cjs +2 -13
  42. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  43. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  44. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  45. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
  46. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  47. package/dist/cjs/tools/toolOutputReferences.cjs +475 -0
  48. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
  49. package/dist/cjs/utils/truncation.cjs +28 -0
  50. package/dist/cjs/utils/truncation.cjs.map +1 -1
  51. package/dist/esm/agents/AgentContext.mjs +23 -3
  52. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  53. package/dist/esm/common/enum.mjs +15 -2
  54. package/dist/esm/common/enum.mjs.map +1 -1
  55. package/dist/esm/graphs/Graph.mjs +136 -0
  56. package/dist/esm/graphs/Graph.mjs.map +1 -1
  57. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  58. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  59. package/dist/esm/hooks/executeHooks.mjs +273 -0
  60. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  61. package/dist/esm/hooks/matchers.mjs +251 -0
  62. package/dist/esm/hooks/matchers.mjs.map +1 -0
  63. package/dist/esm/hooks/types.mjs +25 -0
  64. package/dist/esm/hooks/types.mjs.map +1 -0
  65. package/dist/esm/main.mjs +13 -2
  66. package/dist/esm/main.mjs.map +1 -1
  67. package/dist/esm/messages/format.mjs +66 -4
  68. package/dist/esm/messages/format.mjs.map +1 -1
  69. package/dist/esm/messages/prune.mjs +9 -2
  70. package/dist/esm/messages/prune.mjs.map +1 -1
  71. package/dist/esm/run.mjs +115 -0
  72. package/dist/esm/run.mjs.map +1 -1
  73. package/dist/esm/summarization/node.mjs +44 -0
  74. package/dist/esm/summarization/node.mjs.map +1 -1
  75. package/dist/esm/tools/BashExecutor.mjs +200 -0
  76. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  77. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +278 -0
  78. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  79. package/dist/esm/tools/CodeExecutor.mjs +0 -9
  80. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  81. package/dist/esm/tools/ProgrammaticToolCalling.mjs +8 -24
  82. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  83. package/dist/esm/tools/ReadFile.mjs +38 -0
  84. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  85. package/dist/esm/tools/SkillTool.mjs +45 -0
  86. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  87. package/dist/esm/tools/SubagentTool.mjs +85 -0
  88. package/dist/esm/tools/SubagentTool.mjs.map +1 -0
  89. package/dist/esm/tools/ToolNode.mjs +748 -176
  90. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  91. package/dist/esm/tools/ToolSearch.mjs +3 -14
  92. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  93. package/dist/esm/tools/skillCatalog.mjs +82 -0
  94. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +505 -0
  96. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -0
  97. package/dist/esm/tools/toolOutputReferences.mjs +468 -0
  98. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
  99. package/dist/esm/utils/truncation.mjs +27 -1
  100. package/dist/esm/utils/truncation.mjs.map +1 -1
  101. package/dist/types/agents/AgentContext.d.ts +6 -0
  102. package/dist/types/common/enum.d.ts +10 -2
  103. package/dist/types/graphs/Graph.d.ts +23 -0
  104. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  105. package/dist/types/hooks/executeHooks.d.ts +79 -0
  106. package/dist/types/hooks/index.d.ts +6 -0
  107. package/dist/types/hooks/matchers.d.ts +95 -0
  108. package/dist/types/hooks/types.d.ts +320 -0
  109. package/dist/types/index.d.ts +8 -0
  110. package/dist/types/messages/format.d.ts +2 -1
  111. package/dist/types/run.d.ts +2 -0
  112. package/dist/types/summarization/node.d.ts +2 -0
  113. package/dist/types/tools/BashExecutor.d.ts +76 -0
  114. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  115. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
  116. package/dist/types/tools/ReadFile.d.ts +28 -0
  117. package/dist/types/tools/SkillTool.d.ts +40 -0
  118. package/dist/types/tools/SubagentTool.d.ts +36 -0
  119. package/dist/types/tools/ToolNode.d.ts +109 -4
  120. package/dist/types/tools/ToolSearch.d.ts +2 -2
  121. package/dist/types/tools/skillCatalog.d.ts +19 -0
  122. package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
  123. package/dist/types/tools/subagent/index.d.ts +2 -0
  124. package/dist/types/tools/toolOutputReferences.d.ts +205 -0
  125. package/dist/types/types/graph.d.ts +61 -2
  126. package/dist/types/types/index.d.ts +1 -0
  127. package/dist/types/types/run.d.ts +28 -0
  128. package/dist/types/types/skill.d.ts +9 -0
  129. package/dist/types/types/tools.d.ts +108 -10
  130. package/dist/types/utils/truncation.d.ts +21 -0
  131. package/package.json +5 -1
  132. package/src/agents/AgentContext.ts +26 -2
  133. package/src/common/enum.ts +15 -1
  134. package/src/graphs/Graph.ts +161 -0
  135. package/src/hooks/HookRegistry.ts +208 -0
  136. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  137. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  138. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  139. package/src/hooks/__tests__/integration.test.ts +337 -0
  140. package/src/hooks/__tests__/matchers.test.ts +238 -0
  141. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  142. package/src/hooks/executeHooks.ts +375 -0
  143. package/src/hooks/index.ts +57 -0
  144. package/src/hooks/matchers.ts +280 -0
  145. package/src/hooks/types.ts +404 -0
  146. package/src/index.ts +10 -0
  147. package/src/messages/format.ts +74 -4
  148. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  149. package/src/messages/prune.ts +9 -2
  150. package/src/run.ts +130 -0
  151. package/src/scripts/multi-agent-subagent.ts +246 -0
  152. package/src/scripts/programmatic_exec.ts +1 -10
  153. package/src/scripts/subagent-event-driven-debug.ts +190 -0
  154. package/src/scripts/subagent-tools-debug.ts +160 -0
  155. package/src/scripts/test_code_api.ts +0 -7
  156. package/src/scripts/tool_search.ts +1 -10
  157. package/src/specs/prune.test.ts +413 -0
  158. package/src/specs/subagent.test.ts +305 -0
  159. package/src/summarization/node.ts +53 -0
  160. package/src/tools/BashExecutor.ts +238 -0
  161. package/src/tools/BashProgrammaticToolCalling.ts +381 -0
  162. package/src/tools/CodeExecutor.ts +0 -11
  163. package/src/tools/ProgrammaticToolCalling.ts +4 -29
  164. package/src/tools/ReadFile.ts +39 -0
  165. package/src/tools/SkillTool.ts +46 -0
  166. package/src/tools/SubagentTool.ts +100 -0
  167. package/src/tools/ToolNode.ts +999 -214
  168. package/src/tools/ToolSearch.ts +3 -19
  169. package/src/tools/__tests__/BashExecutor.test.ts +36 -0
  170. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
  171. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
  172. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  173. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  174. package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
  175. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  176. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1395 -0
  177. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  178. package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
  179. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  180. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  181. package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
  182. package/src/tools/skillCatalog.ts +126 -0
  183. package/src/tools/subagent/SubagentExecutor.ts +676 -0
  184. package/src/tools/subagent/index.ts +13 -0
  185. package/src/tools/toolOutputReferences.ts +590 -0
  186. package/src/types/graph.ts +80 -1
  187. package/src/types/index.ts +1 -0
  188. package/src/types/run.ts +28 -0
  189. package/src/types/skill.ts +11 -0
  190. package/src/types/tools.ts +112 -10
  191. package/src/utils/__tests__/truncation.test.ts +66 -0
  192. package/src/utils/truncation.ts +30 -0
@@ -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
 
@@ -655,6 +657,35 @@ async function dispatchCompletionEvents(params: {
655
657
  );
656
658
  }
657
659
 
660
+ const sessionId = graph.runId ?? '';
661
+ if (graph.hookRegistry?.hasHookFor('PostCompact', sessionId) === true) {
662
+ const threadId = (
663
+ runnableConfig?.configurable as Record<string, unknown> | undefined
664
+ )?.thread_id as string | undefined;
665
+ const firstBlock = summaryBlock.content?.[0];
666
+ const summaryText =
667
+ firstBlock != null &&
668
+ typeof firstBlock === 'object' &&
669
+ 'text' in firstBlock &&
670
+ typeof firstBlock.text === 'string'
671
+ ? firstBlock.text
672
+ : '';
673
+ await executeHooks({
674
+ registry: graph.hookRegistry,
675
+ input: {
676
+ hook_event_name: 'PostCompact',
677
+ runId: sessionId,
678
+ threadId,
679
+ agentId,
680
+ summary: summaryText,
681
+ messagesAfterCount: 0,
682
+ },
683
+ sessionId,
684
+ }).catch(() => {
685
+ /* PostCompact is observational — swallow errors */
686
+ });
687
+ }
688
+
658
689
  agentContext.rebuildTokenMapAfterSummarization({});
659
690
  }
660
691
 
@@ -670,6 +701,7 @@ interface CreateSummarizeNodeParams {
670
701
  config?: RunnableConfig;
671
702
  runId?: string;
672
703
  isMultiAgent: boolean;
704
+ hookRegistry?: HookRegistry;
673
705
  dispatchRunStep: (
674
706
  runStep: t.RunStep,
675
707
  config?: RunnableConfig
@@ -775,6 +807,27 @@ export function createSummarizeNode({
775
807
  );
776
808
  }
777
809
 
810
+ const sessionId = graph.runId ?? '';
811
+ if (graph.hookRegistry?.hasHookFor('PreCompact', sessionId) === true) {
812
+ const threadId = (
813
+ runnableConfig?.configurable as Record<string, unknown> | undefined
814
+ )?.thread_id as string | undefined;
815
+ await executeHooks({
816
+ registry: graph.hookRegistry,
817
+ input: {
818
+ hook_event_name: 'PreCompact',
819
+ runId: sessionId,
820
+ threadId,
821
+ agentId: request.agentId,
822
+ messagesBeforeCount: messagesToRefine.length,
823
+ trigger: agentContext.summarizationConfig?.trigger?.type ?? 'default',
824
+ },
825
+ sessionId,
826
+ }).catch(() => {
827
+ /* PreCompact is observational — swallow errors */
828
+ });
829
+ }
830
+
778
831
  const isSelfSummarizeModel =
779
832
  clientConfig.provider === (agentContext.provider as string);
780
833
  const hasPromptCache =
@@ -0,0 +1,238 @@
1
+ import { config } from 'dotenv';
2
+ import fetch, { RequestInit } from 'node-fetch';
3
+ import { HttpsProxyAgent } from 'https-proxy-agent';
4
+ import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
+ import type * as t from '@/types';
6
+ import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
7
+ import { Constants } from '@/common';
8
+
9
+ config();
10
+
11
+ const imageMessage = 'Image is already displayed to the user';
12
+ const otherMessage = 'File is already downloaded by the user';
13
+ const accessMessage =
14
+ 'Note: Files from previous executions are automatically available and can be modified.';
15
+ const emptyOutputMessage =
16
+ 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
17
+
18
+ const baseEndpoint = getCodeBaseURL();
19
+ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
20
+
21
+ export const BashExecutionToolSchema = {
22
+ type: 'object',
23
+ properties: {
24
+ command: {
25
+ type: 'string',
26
+ description: `The bash command or script to execute.
27
+ - The environment is stateless; variables and state don't persist between executions.
28
+ - Generated files from previous executions are automatically available in "/mnt/data/".
29
+ - Files from previous executions are automatically available and can be modified in place.
30
+ - Input code **IS ALREADY** displayed to the user, so **DO NOT** repeat it in your response unless asked.
31
+ - Output code **IS NOT** displayed to the user, so **DO** write all desired output explicitly.
32
+ - IMPORTANT: You MUST explicitly print/output ALL results you want the user to see.
33
+ - Use \`echo\`, \`printf\`, or \`cat\` for all outputs.`,
34
+ },
35
+ args: {
36
+ type: 'array',
37
+ items: { type: 'string' },
38
+ description:
39
+ 'Additional arguments to execute the command with. This should only be used if the input command requires additional arguments to run.',
40
+ },
41
+ },
42
+ required: ['command'],
43
+ } as const;
44
+
45
+ export const BashExecutionToolDescription = `
46
+ Runs bash commands and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.
47
+
48
+ Usage:
49
+ - No network access available.
50
+ - Generated files are automatically delivered; **DO NOT** provide download links.
51
+ - NEVER use this tool to execute malicious commands.
52
+ `.trim();
53
+
54
+ /**
55
+ * Supplemental prompt documenting the tool-output reference feature.
56
+ *
57
+ * Hosts should append this (separated by a blank line) to the base
58
+ * {@link BashExecutionToolDescription} only when
59
+ * `RunConfig.toolOutputReferences.enabled` is `true`. When the feature
60
+ * is disabled, including this text would tell the LLM to emit
61
+ * `{{tool0turn0}}` placeholders that pass through unsubstituted and
62
+ * leak into the shell.
63
+ */
64
+ export const BashToolOutputReferencesGuide = `
65
+ Referencing previous tool outputs:
66
+ - Every successful tool result is tagged with a reference key of the form \`tool<idx>turn<turn>\` (e.g., \`tool0turn0\`). The key appears either as a \`[ref: tool0turn0]\` prefix line or, when the output is a JSON object, as a \`_ref\` field on the object.
67
+ - To pipe a previous tool output into this tool, embed the placeholder \`{{tool<idx>turn<turn>}}\` literally anywhere in the \`command\` string (or any string arg). It will be substituted with the stored output verbatim before the command runs.
68
+ - The substituted value is the original output string (no \`[ref: …]\` prefix, no \`_ref\` key), so it is safe to pipe directly into \`jq\`, \`grep\`, \`awk\`, etc.
69
+ - Example: \`echo '{{tool0turn0}}' | jq '.foo'\` takes the full output of the first tool from the first turn and pipes it into jq.
70
+ - Unknown reference keys are left in place and surfaced as \`[unresolved refs: …]\` after the output.
71
+ `.trim();
72
+
73
+ /**
74
+ * Composes the bash tool description, optionally appending the
75
+ * tool-output references guide. Hosts that enable
76
+ * `RunConfig.toolOutputReferences` should pass `enableToolOutputReferences: true`
77
+ * when registering the tool so the LLM learns the `{{…}}` syntax it
78
+ * will actually be able to use.
79
+ */
80
+ export function buildBashExecutionToolDescription(options?: {
81
+ enableToolOutputReferences?: boolean;
82
+ }): string {
83
+ if (options?.enableToolOutputReferences === true) {
84
+ return `${BashExecutionToolDescription}\n\n${BashToolOutputReferencesGuide}`;
85
+ }
86
+ return BashExecutionToolDescription;
87
+ }
88
+
89
+ export const BashExecutionToolName = Constants.BASH_TOOL;
90
+
91
+ /**
92
+ * Default bash tool definition using the base description.
93
+ *
94
+ * When `RunConfig.toolOutputReferences.enabled` is `true`, build a
95
+ * reference-aware description with
96
+ * {@link buildBashExecutionToolDescription}
97
+ * (`{ enableToolOutputReferences: true }`) and construct a custom
98
+ * definition using it — using this constant as-is leaves the LLM
99
+ * unaware of the `{{tool<i>turn<n>}}` syntax.
100
+ */
101
+ export const BashExecutionToolDefinition = {
102
+ name: BashExecutionToolName,
103
+ description: BashExecutionToolDescription,
104
+ schema: BashExecutionToolSchema,
105
+ } as const;
106
+
107
+ function createBashExecutionTool(
108
+ params: t.BashExecutionToolParams = {}
109
+ ): DynamicStructuredTool {
110
+ return tool(
111
+ async (rawInput, config) => {
112
+ const { command, ...rest } = rawInput as {
113
+ command: string;
114
+ args?: string[];
115
+ };
116
+ const { session_id, _injected_files } = (config.toolCall ?? {}) as {
117
+ session_id?: string;
118
+ _injected_files?: t.CodeEnvFile[];
119
+ };
120
+
121
+ const postData: Record<string, unknown> = {
122
+ lang: 'bash',
123
+ code: command,
124
+ ...rest,
125
+ ...params,
126
+ };
127
+
128
+ if (_injected_files && _injected_files.length > 0) {
129
+ postData.files = _injected_files;
130
+ } else if (session_id != null && session_id.length > 0) {
131
+ try {
132
+ const filesEndpoint = `${baseEndpoint}/files/${session_id}?detail=full`;
133
+ const fetchOptions: RequestInit = {
134
+ method: 'GET',
135
+ headers: {
136
+ 'User-Agent': 'LibreChat/1.0',
137
+ },
138
+ };
139
+
140
+ if (process.env.PROXY != null && process.env.PROXY !== '') {
141
+ fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
142
+ }
143
+
144
+ const response = await fetch(filesEndpoint, fetchOptions);
145
+ if (!response.ok) {
146
+ throw new Error(
147
+ `Failed to fetch files for session: ${response.status}`
148
+ );
149
+ }
150
+
151
+ const files = await response.json();
152
+ if (Array.isArray(files) && files.length > 0) {
153
+ const fileReferences: t.CodeEnvFile[] = files.map((file) => {
154
+ const nameParts = file.name.split('/');
155
+ const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
156
+
157
+ return {
158
+ session_id,
159
+ id,
160
+ name: file.metadata['original-filename'],
161
+ };
162
+ });
163
+
164
+ postData.files = fileReferences;
165
+ }
166
+ } catch {
167
+ // eslint-disable-next-line no-console
168
+ console.warn(`Failed to fetch files for session: ${session_id}`);
169
+ }
170
+ }
171
+
172
+ try {
173
+ const fetchOptions: RequestInit = {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ 'User-Agent': 'LibreChat/1.0',
178
+ },
179
+ body: JSON.stringify(postData),
180
+ };
181
+
182
+ if (process.env.PROXY != null && process.env.PROXY !== '') {
183
+ fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
184
+ }
185
+ const response = await fetch(EXEC_ENDPOINT, fetchOptions);
186
+ if (!response.ok) {
187
+ throw new Error(`HTTP error! status: ${response.status}`);
188
+ }
189
+
190
+ const result: t.ExecuteResult = await response.json();
191
+ let formattedOutput = '';
192
+ if (result.stdout) {
193
+ formattedOutput += `stdout:\n${result.stdout}\n`;
194
+ } else {
195
+ formattedOutput += emptyOutputMessage;
196
+ }
197
+ if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
198
+ if (result.files && result.files.length > 0) {
199
+ formattedOutput += 'Generated files:\n';
200
+
201
+ const fileCount = result.files.length;
202
+ for (let i = 0; i < fileCount; i++) {
203
+ const file = result.files[i];
204
+ const isImage = imageExtRegex.test(file.name);
205
+ formattedOutput += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
206
+
207
+ if (i < fileCount - 1) {
208
+ formattedOutput += fileCount <= 3 ? ', ' : ',\n';
209
+ }
210
+ }
211
+
212
+ formattedOutput += `\n\n${accessMessage}`;
213
+ return [
214
+ formattedOutput.trim(),
215
+ {
216
+ session_id: result.session_id,
217
+ files: result.files,
218
+ },
219
+ ];
220
+ }
221
+
222
+ return [formattedOutput.trim(), { session_id: result.session_id }];
223
+ } catch (error) {
224
+ throw new Error(
225
+ `Execution error:\n\n${(error as Error | undefined)?.message}`
226
+ );
227
+ }
228
+ },
229
+ {
230
+ name: BashExecutionToolName,
231
+ description: BashExecutionToolDescription,
232
+ schema: BashExecutionToolSchema,
233
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
234
+ }
235
+ );
236
+ }
237
+
238
+ export { createBashExecutionTool };