@librechat/agents 3.1.67 → 3.1.68-dev.1

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 (185) 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 +91 -0
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +36 -0
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  10. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  11. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  12. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  13. package/dist/cjs/hooks/matchers.cjs +256 -0
  14. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  15. package/dist/cjs/hooks/types.cjs +27 -0
  16. package/dist/cjs/hooks/types.cjs.map +1 -0
  17. package/dist/cjs/main.cjs +54 -0
  18. package/dist/cjs/main.cjs.map +1 -1
  19. package/dist/cjs/messages/format.cjs +74 -12
  20. package/dist/cjs/messages/format.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +111 -0
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/summarization/index.cjs +41 -0
  24. package/dist/cjs/summarization/index.cjs.map +1 -1
  25. package/dist/cjs/summarization/node.cjs +165 -19
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/BashExecutor.cjs +165 -0
  28. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  29. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +287 -0
  30. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  31. package/dist/cjs/tools/CodeExecutor.cjs +0 -9
  32. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +7 -23
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  35. package/dist/cjs/tools/ReadFile.cjs +43 -0
  36. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  37. package/dist/cjs/tools/SkillTool.cjs +50 -0
  38. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  39. package/dist/cjs/tools/SubagentTool.cjs +92 -0
  40. package/dist/cjs/tools/SubagentTool.cjs.map +1 -0
  41. package/dist/cjs/tools/ToolNode.cjs +304 -140
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/ToolSearch.cjs +2 -13
  44. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  45. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  46. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +511 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -0
  49. package/dist/esm/agents/AgentContext.mjs +23 -3
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/common/enum.mjs +15 -2
  52. package/dist/esm/common/enum.mjs.map +1 -1
  53. package/dist/esm/graphs/Graph.mjs +91 -0
  54. package/dist/esm/graphs/Graph.mjs.map +1 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
  56. package/dist/esm/graphs/MultiAgentGraph.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/run.mjs +111 -0
  70. package/dist/esm/run.mjs.map +1 -1
  71. package/dist/esm/summarization/index.mjs +41 -1
  72. package/dist/esm/summarization/index.mjs.map +1 -1
  73. package/dist/esm/summarization/node.mjs +165 -19
  74. package/dist/esm/summarization/node.mjs.map +1 -1
  75. package/dist/esm/tools/BashExecutor.mjs +159 -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 +306 -142
  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/types/agents/AgentContext.d.ts +6 -0
  98. package/dist/types/common/enum.d.ts +10 -2
  99. package/dist/types/graphs/Graph.d.ts +2 -0
  100. package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
  101. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  102. package/dist/types/hooks/executeHooks.d.ts +79 -0
  103. package/dist/types/hooks/index.d.ts +6 -0
  104. package/dist/types/hooks/matchers.d.ts +95 -0
  105. package/dist/types/hooks/types.d.ts +320 -0
  106. package/dist/types/index.d.ts +8 -0
  107. package/dist/types/messages/format.d.ts +2 -1
  108. package/dist/types/run.d.ts +1 -0
  109. package/dist/types/summarization/index.d.ts +2 -0
  110. package/dist/types/summarization/node.d.ts +2 -0
  111. package/dist/types/tools/BashExecutor.d.ts +45 -0
  112. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  113. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -9
  114. package/dist/types/tools/ReadFile.d.ts +28 -0
  115. package/dist/types/tools/SkillTool.d.ts +40 -0
  116. package/dist/types/tools/SubagentTool.d.ts +36 -0
  117. package/dist/types/tools/ToolNode.d.ts +24 -2
  118. package/dist/types/tools/ToolSearch.d.ts +2 -2
  119. package/dist/types/tools/skillCatalog.d.ts +19 -0
  120. package/dist/types/tools/subagent/SubagentExecutor.d.ts +137 -0
  121. package/dist/types/tools/subagent/index.d.ts +2 -0
  122. package/dist/types/types/graph.d.ts +61 -2
  123. package/dist/types/types/index.d.ts +1 -0
  124. package/dist/types/types/run.d.ts +20 -0
  125. package/dist/types/types/skill.d.ts +9 -0
  126. package/dist/types/types/tools.d.ts +38 -10
  127. package/package.json +5 -1
  128. package/src/agents/AgentContext.ts +26 -2
  129. package/src/common/enum.ts +15 -1
  130. package/src/graphs/Graph.ts +113 -0
  131. package/src/graphs/MultiAgentGraph.ts +39 -0
  132. package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
  133. package/src/hooks/HookRegistry.ts +208 -0
  134. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  135. package/src/hooks/__tests__/compactHooks.test.ts +214 -0
  136. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  137. package/src/hooks/__tests__/integration.test.ts +337 -0
  138. package/src/hooks/__tests__/matchers.test.ts +238 -0
  139. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  140. package/src/hooks/executeHooks.ts +375 -0
  141. package/src/hooks/index.ts +57 -0
  142. package/src/hooks/matchers.ts +280 -0
  143. package/src/hooks/types.ts +404 -0
  144. package/src/index.ts +10 -0
  145. package/src/messages/format.ts +74 -4
  146. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  147. package/src/run.ts +126 -0
  148. package/src/scripts/multi-agent-subagent.ts +246 -0
  149. package/src/scripts/programmatic_exec.ts +1 -10
  150. package/src/scripts/subagent-event-driven-debug.ts +190 -0
  151. package/src/scripts/subagent-tools-debug.ts +160 -0
  152. package/src/scripts/test_code_api.ts +0 -7
  153. package/src/scripts/tool_search.ts +1 -10
  154. package/src/specs/subagent.test.ts +305 -0
  155. package/src/summarization/__tests__/node.test.ts +42 -0
  156. package/src/summarization/__tests__/trigger.test.ts +100 -1
  157. package/src/summarization/index.ts +47 -0
  158. package/src/summarization/node.ts +202 -24
  159. package/src/tools/BashExecutor.ts +193 -0
  160. package/src/tools/BashProgrammaticToolCalling.ts +381 -0
  161. package/src/tools/CodeExecutor.ts +0 -11
  162. package/src/tools/ProgrammaticToolCalling.ts +4 -29
  163. package/src/tools/ReadFile.ts +39 -0
  164. package/src/tools/SkillTool.ts +46 -0
  165. package/src/tools/SubagentTool.ts +100 -0
  166. package/src/tools/ToolNode.ts +391 -169
  167. package/src/tools/ToolSearch.ts +3 -19
  168. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +7 -8
  169. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1
  170. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  171. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  172. package/src/tools/__tests__/SubagentExecutor.test.ts +1148 -0
  173. package/src/tools/__tests__/SubagentTool.test.ts +149 -0
  174. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  175. package/src/tools/__tests__/ToolSearch.integration.test.ts +7 -8
  176. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  177. package/src/tools/__tests__/subagentHooks.test.ts +215 -0
  178. package/src/tools/skillCatalog.ts +126 -0
  179. package/src/tools/subagent/SubagentExecutor.ts +676 -0
  180. package/src/tools/subagent/index.ts +13 -0
  181. package/src/types/graph.ts +80 -1
  182. package/src/types/index.ts +1 -0
  183. package/src/types/run.ts +20 -0
  184. package/src/types/skill.ts +11 -0
  185. package/src/types/tools.ts +41 -10
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { Constants } from '@/common';
3
+ import {
4
+ SubagentToolName,
5
+ SubagentToolDescription,
6
+ SubagentToolDefinition,
7
+ SubagentToolSchema,
8
+ createSubagentToolDefinition,
9
+ buildSubagentToolParams,
10
+ } from '../SubagentTool';
11
+ import type { SubagentConfig } from '@/types';
12
+
13
+ describe('SubagentTool', () => {
14
+ describe('schema structure', () => {
15
+ it('has description as required string property', () => {
16
+ expect(SubagentToolSchema.properties.description.type).toBe('string');
17
+ expect(SubagentToolSchema.required).toContain('description');
18
+ });
19
+
20
+ it('has subagent_type as required string property', () => {
21
+ expect(SubagentToolSchema.properties.subagent_type.type).toBe('string');
22
+ expect(SubagentToolSchema.required).toContain('subagent_type');
23
+ });
24
+
25
+ it('is an object type schema', () => {
26
+ expect(SubagentToolSchema.type).toBe('object');
27
+ });
28
+ });
29
+
30
+ describe('SubagentToolDefinition', () => {
31
+ it('has correct name', () => {
32
+ expect(SubagentToolDefinition.name).toBe(Constants.SUBAGENT);
33
+ });
34
+
35
+ it('references the same schema object', () => {
36
+ expect(SubagentToolDefinition.parameters).toBe(SubagentToolSchema);
37
+ });
38
+
39
+ it('has a non-empty description', () => {
40
+ expect(SubagentToolDefinition.description).toBe(SubagentToolDescription);
41
+ expect(SubagentToolDefinition.description!.length).toBeGreaterThan(0);
42
+ });
43
+ });
44
+
45
+ describe('SubagentToolName', () => {
46
+ it('equals Constants.SUBAGENT', () => {
47
+ expect(SubagentToolName).toBe('subagent');
48
+ expect(SubagentToolName).toBe(Constants.SUBAGENT);
49
+ });
50
+ });
51
+
52
+ describe('createSubagentToolDefinition', () => {
53
+ const configs: SubagentConfig[] = [
54
+ {
55
+ type: 'researcher',
56
+ name: 'Research Agent',
57
+ description: 'Searches and summarizes information',
58
+ },
59
+ {
60
+ type: 'coder',
61
+ name: 'Coding Agent',
62
+ description: 'Writes and reviews code',
63
+ },
64
+ ];
65
+
66
+ it('populates subagent_type enum from configs', () => {
67
+ const def = createSubagentToolDefinition(configs);
68
+ const schema = def.parameters as Record<string, unknown>;
69
+ const props = schema.properties as Record<
70
+ string,
71
+ Record<string, unknown>
72
+ >;
73
+ expect(props.subagent_type.enum).toEqual(['researcher', 'coder']);
74
+ });
75
+
76
+ it('includes type descriptions in tool description', () => {
77
+ const def = createSubagentToolDefinition(configs);
78
+ expect(def.description).toContain('"researcher" (Research Agent)');
79
+ expect(def.description).toContain('"coder" (Coding Agent)');
80
+ expect(def.description).toContain('Searches and summarizes information');
81
+ expect(def.description).toContain('Writes and reviews code');
82
+ });
83
+
84
+ it('has correct name', () => {
85
+ const def = createSubagentToolDefinition(configs);
86
+ expect(def.name).toBe(Constants.SUBAGENT);
87
+ });
88
+
89
+ it('has required description and subagent_type fields', () => {
90
+ const def = createSubagentToolDefinition(configs);
91
+ const schema = def.parameters as Record<string, unknown>;
92
+ expect(schema.required).toContain('description');
93
+ expect(schema.required).toContain('subagent_type');
94
+ });
95
+
96
+ it('works with single config', () => {
97
+ const def = createSubagentToolDefinition([configs[0]]);
98
+ const schema = def.parameters as Record<string, unknown>;
99
+ const props = schema.properties as Record<
100
+ string,
101
+ Record<string, unknown>
102
+ >;
103
+ expect(props.subagent_type.enum).toEqual(['researcher']);
104
+ });
105
+ });
106
+
107
+ describe('buildSubagentToolParams', () => {
108
+ const configs: SubagentConfig[] = [
109
+ {
110
+ type: 'researcher',
111
+ name: 'Research Agent',
112
+ description: 'Searches and summarizes information',
113
+ },
114
+ {
115
+ type: 'coder',
116
+ name: 'Coding Agent',
117
+ description: 'Writes and reviews code',
118
+ },
119
+ ];
120
+
121
+ it('returns name matching Constants.SUBAGENT', () => {
122
+ const params = buildSubagentToolParams(configs);
123
+ expect(params.name).toBe(Constants.SUBAGENT);
124
+ });
125
+
126
+ it('schema has enum populated from config types', () => {
127
+ const params = buildSubagentToolParams(configs);
128
+ const props = params.schema.properties as Record<
129
+ string,
130
+ Record<string, unknown>
131
+ >;
132
+ expect(props.subagent_type.enum).toEqual(['researcher', 'coder']);
133
+ });
134
+
135
+ it('description includes type listings', () => {
136
+ const params = buildSubagentToolParams(configs);
137
+ expect(params.description).toContain('"researcher" (Research Agent)');
138
+ expect(params.description).toContain('"coder" (Coding Agent)');
139
+ });
140
+
141
+ it('produces same schema as createSubagentToolDefinition', () => {
142
+ const params = buildSubagentToolParams(configs);
143
+ const def = createSubagentToolDefinition(configs);
144
+ expect(params.name).toBe(def.name);
145
+ expect(params.description).toBe(def.description);
146
+ expect(params.schema).toEqual(def.parameters);
147
+ });
148
+ });
149
+ });
@@ -216,7 +216,7 @@ describe('ToolNode code execution session management', () => {
216
216
  toolNode as unknown as {
217
217
  storeCodeSessionFromResults: (
218
218
  results: t.ToolExecuteResult[],
219
- requests: t.ToolCallRequest[]
219
+ requestMap: Map<string, t.ToolCallRequest>
220
220
  ) => void;
221
221
  }
222
222
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -233,7 +233,7 @@ describe('ToolNode code execution session management', () => {
233
233
  status: 'success',
234
234
  },
235
235
  ],
236
- [{ id: 'tc1', name: Constants.EXECUTE_CODE, args: {} }]
236
+ new Map([['tc1', { id: 'tc1', name: Constants.EXECUTE_CODE, args: {} }]])
237
237
  );
238
238
 
239
239
  const stored = sessions.get(
@@ -265,7 +265,7 @@ describe('ToolNode code execution session management', () => {
265
265
  toolNode as unknown as {
266
266
  storeCodeSessionFromResults: (
267
267
  results: t.ToolExecuteResult[],
268
- requests: t.ToolCallRequest[]
268
+ requestMap: Map<string, t.ToolCallRequest>
269
269
  ) => void;
270
270
  }
271
271
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -279,7 +279,7 @@ describe('ToolNode code execution session management', () => {
279
279
  status: 'success',
280
280
  },
281
281
  ],
282
- [{ id: 'tc2', name: Constants.EXECUTE_CODE, args: {} }]
282
+ new Map([['tc2', { id: 'tc2', name: Constants.EXECUTE_CODE, args: {} }]])
283
283
  );
284
284
 
285
285
  const stored = sessions.get(
@@ -312,7 +312,7 @@ describe('ToolNode code execution session management', () => {
312
312
  toolNode as unknown as {
313
313
  storeCodeSessionFromResults: (
314
314
  results: t.ToolExecuteResult[],
315
- requests: t.ToolCallRequest[]
315
+ requestMap: Map<string, t.ToolCallRequest>
316
316
  ) => void;
317
317
  }
318
318
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -329,7 +329,7 @@ describe('ToolNode code execution session management', () => {
329
329
  status: 'success',
330
330
  },
331
331
  ],
332
- [{ id: 'tc3', name: Constants.EXECUTE_CODE, args: {} }]
332
+ new Map([['tc3', { id: 'tc3', name: Constants.EXECUTE_CODE, args: {} }]])
333
333
  );
334
334
 
335
335
  const stored = sessions.get(
@@ -365,7 +365,7 @@ describe('ToolNode code execution session management', () => {
365
365
  toolNode as unknown as {
366
366
  storeCodeSessionFromResults: (
367
367
  results: t.ToolExecuteResult[],
368
- requests: t.ToolCallRequest[]
368
+ requestMap: Map<string, t.ToolCallRequest>
369
369
  ) => void;
370
370
  }
371
371
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -379,7 +379,7 @@ describe('ToolNode code execution session management', () => {
379
379
  status: 'success',
380
380
  },
381
381
  ],
382
- [{ id: 'tc4', name: Constants.EXECUTE_CODE, args: {} }]
382
+ new Map([['tc4', { id: 'tc4', name: Constants.EXECUTE_CODE, args: {} }]])
383
383
  );
384
384
 
385
385
  const stored = sessions.get(
@@ -404,7 +404,7 @@ describe('ToolNode code execution session management', () => {
404
404
  toolNode as unknown as {
405
405
  storeCodeSessionFromResults: (
406
406
  results: t.ToolExecuteResult[],
407
- requests: t.ToolCallRequest[]
407
+ requestMap: Map<string, t.ToolCallRequest>
408
408
  ) => void;
409
409
  }
410
410
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -418,7 +418,7 @@ describe('ToolNode code execution session management', () => {
418
418
  status: 'success',
419
419
  },
420
420
  ],
421
- [{ id: 'tc5', name: 'web_search', args: {} }]
421
+ new Map([['tc5', { id: 'tc5', name: 'web_search', args: {} }]])
422
422
  );
423
423
 
424
424
  expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
@@ -438,7 +438,7 @@ describe('ToolNode code execution session management', () => {
438
438
  toolNode as unknown as {
439
439
  storeCodeSessionFromResults: (
440
440
  results: t.ToolExecuteResult[],
441
- requests: t.ToolCallRequest[]
441
+ requestMap: Map<string, t.ToolCallRequest>
442
442
  ) => void;
443
443
  }
444
444
  ).storeCodeSessionFromResults.bind(toolNode);
@@ -456,7 +456,7 @@ describe('ToolNode code execution session management', () => {
456
456
  errorMessage: 'execution failed',
457
457
  },
458
458
  ],
459
- [{ id: 'tc6', name: Constants.EXECUTE_CODE, args: {} }]
459
+ new Map([['tc6', { id: 'tc6', name: Constants.EXECUTE_CODE, args: {} }]])
460
460
  );
461
461
 
462
462
  expect(sessions.has(Constants.EXECUTE_CODE)).toBe(false);
@@ -3,10 +3,10 @@
3
3
  * Integration tests for Tool Search Regex.
4
4
  * These tests hit the LIVE Code API and verify end-to-end search functionality.
5
5
  *
6
- * Run with: npm test -- ToolSearch.integration.test.ts
6
+ * Run with: RUN_CODE_INTEGRATION_TESTS=1 npm test -- ToolSearch.integration.test.ts
7
7
  *
8
- * Requires LIBRECHAT_CODE_API_KEY environment variable.
9
- * Tests are skipped when the API key is not available.
8
+ * Opt-in via the `RUN_CODE_INTEGRATION_TESTS` environment variable
9
+ * these tests hit a real sandbox so they don't run in CI by default.
10
10
  */
11
11
  import { config as dotenvConfig } from 'dotenv';
12
12
  dotenvConfig();
@@ -15,17 +15,16 @@ import { describe, it, expect, beforeAll } from '@jest/globals';
15
15
  import { createToolSearch } from '../ToolSearch';
16
16
  import { createToolSearchToolRegistry } from '@/test/mockTools';
17
17
 
18
- const apiKey = process.env.LIBRECHAT_CODE_API_KEY;
19
- const shouldSkip = apiKey == null || apiKey === '';
18
+ const shouldSkip = process.env.RUN_CODE_INTEGRATION_TESTS !== '1';
20
19
 
21
- const describeIfApiKey = shouldSkip ? describe.skip : describe;
20
+ const describeIfLive = shouldSkip ? describe.skip : describe;
22
21
 
23
- describeIfApiKey('ToolSearch - Live API Integration', () => {
22
+ describeIfLive('ToolSearch - Live API Integration', () => {
24
23
  let searchTool: ReturnType<typeof createToolSearch>;
25
24
  const toolRegistry = createToolSearchToolRegistry();
26
25
 
27
26
  beforeAll(() => {
28
- searchTool = createToolSearch({ apiKey: apiKey!, toolRegistry });
27
+ searchTool = createToolSearch({ toolRegistry });
29
28
  });
30
29
 
31
30
  it('searches for expense-related tools', async () => {
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { formatSkillCatalog } from '../skillCatalog';
3
+ import type { SkillCatalogEntry } from '@/types';
4
+
5
+ describe('formatSkillCatalog', () => {
6
+ it('returns empty string for empty array', () => {
7
+ expect(formatSkillCatalog([])).toBe('');
8
+ });
9
+
10
+ it('formats a single skill with header', () => {
11
+ const skills: SkillCatalogEntry[] = [
12
+ {
13
+ name: 'pdf-processor',
14
+ description: 'Processes PDF files into structured data.',
15
+ },
16
+ ];
17
+ const result = formatSkillCatalog(skills);
18
+ expect(result).toBe(
19
+ '## Available Skills\n\n- pdf-processor: Processes PDF files into structured data.'
20
+ );
21
+ });
22
+
23
+ it('formats multiple skills within budget', () => {
24
+ const skills: SkillCatalogEntry[] = [
25
+ { name: 'pdf-processor', description: 'Processes PDF files.' },
26
+ { name: 'review-pr', description: 'Reviews pull requests.' },
27
+ { name: 'meeting-notes', description: 'Formats meeting transcripts.' },
28
+ ];
29
+ const result = formatSkillCatalog(skills);
30
+ expect(result).toContain('## Available Skills');
31
+ expect(result).toContain('- pdf-processor: Processes PDF files.');
32
+ expect(result).toContain('- review-pr: Reviews pull requests.');
33
+ expect(result).toContain('- meeting-notes: Formats meeting transcripts.');
34
+ });
35
+
36
+ it('caps per-entry descriptions at maxEntryChars', () => {
37
+ const longDesc = 'A'.repeat(300);
38
+ const skills: SkillCatalogEntry[] = [
39
+ { name: 'long-skill', description: longDesc },
40
+ ];
41
+ const result = formatSkillCatalog(skills);
42
+ expect(result).toContain('- long-skill: ' + 'A'.repeat(249) + '\u2026');
43
+ expect(result).not.toContain('A'.repeat(300));
44
+ });
45
+
46
+ it('truncates descriptions proportionally when over budget', () => {
47
+ const skills: SkillCatalogEntry[] = Array.from({ length: 10 }, (_, i) => ({
48
+ name: `sk-${i}`,
49
+ description: 'D'.repeat(200),
50
+ }));
51
+ // Budget = 10000 * 0.01 * 4 = 400 chars — enough for names + short descs, not full 200-char descs
52
+ const result = formatSkillCatalog(skills, {
53
+ contextWindowTokens: 10000,
54
+ budgetPercent: 0.01,
55
+ charsPerToken: 4,
56
+ });
57
+ expect(result).toContain('## Available Skills');
58
+ for (let i = 0; i < 10; i++) {
59
+ expect(result).toContain(`sk-${i}`);
60
+ }
61
+ // Full 200-char descriptions should be truncated
62
+ expect(result).not.toContain('D'.repeat(200));
63
+ });
64
+
65
+ it('falls back to names-only when extremely over budget', () => {
66
+ const skills: SkillCatalogEntry[] = Array.from({ length: 10 }, (_, i) => ({
67
+ name: `s${i}`,
68
+ description: 'Very detailed description that is quite long and verbose.',
69
+ }));
70
+ // Budget = 2000 * 0.01 * 4 = 80 chars — enough for names-only but not descriptions
71
+ const result = formatSkillCatalog(skills, {
72
+ contextWindowTokens: 2000,
73
+ budgetPercent: 0.01,
74
+ charsPerToken: 4,
75
+ });
76
+ expect(result).toContain('## Available Skills');
77
+ expect(result).toContain('- s0');
78
+ // Verify entry lines have no descriptions (names-only format)
79
+ const entryLines = result.split('\n').filter((l) => l.startsWith('- '));
80
+ for (const line of entryLines) {
81
+ expect(line).toMatch(/^- s\d+$/);
82
+ }
83
+ });
84
+
85
+ it('respects custom options', () => {
86
+ const skills: SkillCatalogEntry[] = [
87
+ { name: 'test', description: 'A'.repeat(100) },
88
+ ];
89
+ const result = formatSkillCatalog(skills, { maxEntryChars: 50 });
90
+ expect(result).toContain('A'.repeat(49) + '\u2026');
91
+ expect(result).not.toContain('A'.repeat(100));
92
+ });
93
+
94
+ it('includes skills with descriptions shorter than minDescLength', () => {
95
+ const skills: SkillCatalogEntry[] = [
96
+ { name: 'short', description: 'Hi' },
97
+ { name: 'normal', description: 'A normal description here.' },
98
+ ];
99
+ const result = formatSkillCatalog(skills);
100
+ expect(result).toContain('- short: Hi');
101
+ expect(result).toContain('- normal: A normal description here.');
102
+ });
103
+
104
+ it('handles all skills with zero-length descriptions as names-only', () => {
105
+ const skills: SkillCatalogEntry[] = [
106
+ { name: 'alpha', description: '' },
107
+ { name: 'beta', description: '' },
108
+ ];
109
+ const result = formatSkillCatalog(skills);
110
+ expect(result).toBe('## Available Skills\n\n- alpha\n- beta');
111
+ });
112
+
113
+ it('has no trailing or leading whitespace', () => {
114
+ const skills: SkillCatalogEntry[] = [
115
+ { name: 'test', description: 'A test skill.' },
116
+ ];
117
+ const result = formatSkillCatalog(skills);
118
+ expect(result).toBe(result.trim());
119
+ const lines = result.split('\n');
120
+ for (const line of lines) {
121
+ expect(line).toBe(line.trimEnd());
122
+ }
123
+ });
124
+
125
+ it('truncates names-only list when even names exceed budget', () => {
126
+ const skills: SkillCatalogEntry[] = Array.from({ length: 100 }, (_, i) => ({
127
+ name: `skill-with-a-long-name-${i}`,
128
+ description: 'Some description.',
129
+ }));
130
+ // Budget so small that even names-only for 100 skills exceeds it
131
+ const result = formatSkillCatalog(skills, {
132
+ contextWindowTokens: 100,
133
+ budgetPercent: 0.01,
134
+ charsPerToken: 4,
135
+ });
136
+ // Should still have the header and at least one entry, but not all 100
137
+ if (result === '') {
138
+ // Budget too small for even one entry — valid edge case
139
+ expect(result).toBe('');
140
+ } else {
141
+ expect(result).toContain('## Available Skills');
142
+ const entryLines = result.split('\n').filter((l) => l.startsWith('- '));
143
+ expect(entryLines.length).toBeLessThan(100);
144
+ expect(entryLines.length).toBeGreaterThan(0);
145
+ expect(result.length).toBeLessThanOrEqual(100 * 0.01 * 4);
146
+ }
147
+ });
148
+
149
+ it('ignores displayTitle in output', () => {
150
+ const skills: SkillCatalogEntry[] = [
151
+ {
152
+ name: 'my-skill',
153
+ description: 'Does stuff.',
154
+ displayTitle: 'My Fancy Skill',
155
+ },
156
+ ];
157
+ const result = formatSkillCatalog(skills);
158
+ expect(result).not.toContain('My Fancy Skill');
159
+ expect(result).toContain('- my-skill: Does stuff.');
160
+ });
161
+ });
@@ -0,0 +1,215 @@
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 * as t from '@/types';
5
+ import type {
6
+ HookCallback,
7
+ SubagentStartHookInput,
8
+ SubagentStartHookOutput,
9
+ SubagentStopHookInput,
10
+ SubagentStopHookOutput,
11
+ } from '@/hooks/types';
12
+ import { HookRegistry } from '@/hooks/HookRegistry';
13
+ import { Run } from '@/run';
14
+ import {
15
+ Constants,
16
+ GraphEvents,
17
+ Providers,
18
+ ToolEndHandler,
19
+ ModelEndHandler,
20
+ } from '@/index';
21
+ import * as providers from '@/llm/providers';
22
+
23
+ const CHILD_RESPONSE = 'Hook test child response.';
24
+
25
+ const callerConfig = {
26
+ configurable: { thread_id: 'hook-test-thread' },
27
+ streamMode: 'values' as const,
28
+ version: 'v2' as const,
29
+ };
30
+
31
+ const originalGetChatModelClass = providers.getChatModelClass;
32
+
33
+ function makeSubagentToolCall(): ToolCall {
34
+ return {
35
+ name: Constants.SUBAGENT,
36
+ args: {
37
+ description: 'Test task for hook verification',
38
+ subagent_type: 'researcher',
39
+ },
40
+ id: `call_sub_${Date.now()}`,
41
+ type: 'tool_call',
42
+ };
43
+ }
44
+
45
+ function createParentAgent(): t.AgentInputs {
46
+ return {
47
+ agentId: 'hook-parent',
48
+ provider: Providers.OPENAI,
49
+ clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
50
+ instructions: 'Delegate research tasks to subagents.',
51
+ maxContextTokens: 8000,
52
+ subagentConfigs: [
53
+ {
54
+ type: 'researcher',
55
+ name: 'Researcher',
56
+ description: 'Researches topics',
57
+ agentInputs: {
58
+ agentId: 'researcher-child',
59
+ provider: Providers.OPENAI,
60
+ clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
61
+ instructions: 'Answer concisely.',
62
+ maxContextTokens: 8000,
63
+ },
64
+ },
65
+ ],
66
+ };
67
+ }
68
+
69
+ async function createSubagentRun(
70
+ hooks: HookRegistry,
71
+ runId = `subagent-hook-${Date.now()}`
72
+ ): Promise<Run<t.IState>> {
73
+ return Run.create<t.IState>({
74
+ runId,
75
+ graphConfig: {
76
+ type: 'standard',
77
+ agents: [createParentAgent()],
78
+ },
79
+ returnContent: true,
80
+ skipCleanup: true,
81
+ customHandlers: {
82
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
83
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
84
+ },
85
+ hooks,
86
+ });
87
+ }
88
+
89
+ describe('Subagent hook integration (end-to-end via Run)', () => {
90
+ jest.setTimeout(15000);
91
+
92
+ let getChatModelClassSpy: jest.SpyInstance;
93
+
94
+ beforeEach(() => {
95
+ getChatModelClassSpy = jest
96
+ .spyOn(providers, 'getChatModelClass')
97
+ .mockImplementation(((provider: Providers) => {
98
+ if (provider === Providers.OPENAI) {
99
+ return class extends FakeListChatModel {
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ constructor(_options: any) {
102
+ super({ responses: [CHILD_RESPONSE] });
103
+ }
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ } as any;
106
+ }
107
+ return originalGetChatModelClass(provider);
108
+ }) as typeof providers.getChatModelClass);
109
+ });
110
+
111
+ afterEach(() => {
112
+ getChatModelClassSpy.mockRestore();
113
+ });
114
+
115
+ it('SubagentStart fires with correct payload through real Run pipeline', async () => {
116
+ const registry = new HookRegistry();
117
+ let captured: SubagentStartHookInput | undefined;
118
+
119
+ const hook: HookCallback<'SubagentStart'> = async (
120
+ input
121
+ ): Promise<SubagentStartHookOutput> => {
122
+ captured = input;
123
+ return {};
124
+ };
125
+ registry.register('SubagentStart', { hooks: [hook] });
126
+
127
+ const tc = makeSubagentToolCall();
128
+ const run = await createSubagentRun(registry);
129
+ run.Graph!.overrideTestModel(['Delegating...', 'Final answer.'], 5, [tc]);
130
+
131
+ await run.processStream(
132
+ { messages: [new HumanMessage('research something')] },
133
+ callerConfig
134
+ );
135
+
136
+ expect(captured).toBeDefined();
137
+ expect(captured!.hook_event_name).toBe('SubagentStart');
138
+ expect(captured!.agentType).toBe('researcher');
139
+ expect(captured!.parentAgentId).toBe('hook-parent');
140
+ expect(captured!.threadId).toBe('hook-test-thread');
141
+ expect(captured!.inputs).toHaveLength(1);
142
+ expect(captured!.inputs[0].content).toContain(
143
+ 'Test task for hook verification'
144
+ );
145
+ });
146
+
147
+ it('SubagentStop fires with messages from child execution', async () => {
148
+ const registry = new HookRegistry();
149
+ let captured: SubagentStopHookInput | undefined;
150
+
151
+ const hook: HookCallback<'SubagentStop'> = async (
152
+ input
153
+ ): Promise<SubagentStopHookOutput> => {
154
+ captured = input;
155
+ return {};
156
+ };
157
+ registry.register('SubagentStop', { hooks: [hook] });
158
+
159
+ const tc = makeSubagentToolCall();
160
+ const run = await createSubagentRun(registry);
161
+ run.Graph!.overrideTestModel(['Delegating...', 'Final answer.'], 5, [tc]);
162
+
163
+ await run.processStream(
164
+ { messages: [new HumanMessage('research something')] },
165
+ callerConfig
166
+ );
167
+
168
+ expect(captured).toBeDefined();
169
+ expect(captured!.hook_event_name).toBe('SubagentStop');
170
+ expect(captured!.agentType).toBe('researcher');
171
+ expect(captured!.threadId).toBe('hook-test-thread');
172
+ expect(captured!.messages.length).toBeGreaterThan(0);
173
+ });
174
+
175
+ it('SubagentStart deny blocks subagent execution and returns blocked message', async () => {
176
+ const registry = new HookRegistry();
177
+ const denyHook: HookCallback<
178
+ 'SubagentStart'
179
+ > = async (): Promise<SubagentStartHookOutput> => ({
180
+ decision: 'deny',
181
+ reason: 'policy violation',
182
+ });
183
+ registry.register('SubagentStart', {
184
+ pattern: '^researcher$',
185
+ hooks: [denyHook],
186
+ });
187
+
188
+ const tc = makeSubagentToolCall();
189
+ const run = await createSubagentRun(registry);
190
+ run.Graph!.overrideTestModel(
191
+ ['Delegating...', 'The subagent was blocked.'],
192
+ 5,
193
+ [tc]
194
+ );
195
+
196
+ await run.processStream(
197
+ { messages: [new HumanMessage('research something')] },
198
+ callerConfig
199
+ );
200
+
201
+ const runMessages = run.getRunMessages();
202
+ expect(runMessages).toBeDefined();
203
+
204
+ const toolMessages = runMessages!.filter(
205
+ (msg) =>
206
+ msg._getType() === 'tool' &&
207
+ 'name' in msg &&
208
+ msg.name === Constants.SUBAGENT
209
+ );
210
+ expect(toolMessages.length).toBe(1);
211
+ expect(String(toolMessages[0].content)).toContain(
212
+ 'Blocked: policy violation'
213
+ );
214
+ });
215
+ });