@librechat/agents 3.1.67-dev.4 → 3.1.68

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -23
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +0 -16
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +0 -91
  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/main.cjs +1 -53
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/messages/format.cjs +12 -74
  12. package/dist/cjs/messages/format.cjs.map +1 -1
  13. package/dist/cjs/run.cjs +0 -111
  14. package/dist/cjs/run.cjs.map +1 -1
  15. package/dist/cjs/summarization/index.cjs +41 -0
  16. package/dist/cjs/summarization/index.cjs.map +1 -1
  17. package/dist/cjs/summarization/node.cjs +121 -63
  18. package/dist/cjs/summarization/node.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +140 -304
  20. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  21. package/dist/esm/agents/AgentContext.mjs +3 -23
  22. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  23. package/dist/esm/common/enum.mjs +1 -15
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +0 -91
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +36 -0
  28. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  29. package/dist/esm/main.mjs +2 -13
  30. package/dist/esm/main.mjs.map +1 -1
  31. package/dist/esm/messages/format.mjs +4 -66
  32. package/dist/esm/messages/format.mjs.map +1 -1
  33. package/dist/esm/run.mjs +0 -111
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/summarization/index.mjs +41 -1
  36. package/dist/esm/summarization/index.mjs.map +1 -1
  37. package/dist/esm/summarization/node.mjs +121 -63
  38. package/dist/esm/summarization/node.mjs.map +1 -1
  39. package/dist/esm/tools/ToolNode.mjs +142 -306
  40. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  41. package/dist/types/agents/AgentContext.d.ts +0 -6
  42. package/dist/types/common/enum.d.ts +1 -10
  43. package/dist/types/graphs/Graph.d.ts +0 -2
  44. package/dist/types/graphs/MultiAgentGraph.d.ts +12 -0
  45. package/dist/types/index.d.ts +0 -8
  46. package/dist/types/messages/format.d.ts +1 -2
  47. package/dist/types/run.d.ts +0 -1
  48. package/dist/types/summarization/index.d.ts +2 -0
  49. package/dist/types/summarization/node.d.ts +0 -2
  50. package/dist/types/tools/ToolNode.d.ts +2 -24
  51. package/dist/types/types/graph.d.ts +2 -61
  52. package/dist/types/types/index.d.ts +0 -1
  53. package/dist/types/types/run.d.ts +0 -20
  54. package/dist/types/types/tools.d.ts +1 -38
  55. package/package.json +1 -5
  56. package/src/agents/AgentContext.ts +2 -26
  57. package/src/common/enum.ts +0 -15
  58. package/src/graphs/Graph.ts +0 -113
  59. package/src/graphs/MultiAgentGraph.ts +39 -0
  60. package/src/graphs/__tests__/MultiAgentGraph.test.ts +91 -0
  61. package/src/index.ts +0 -10
  62. package/src/messages/format.ts +4 -74
  63. package/src/run.ts +0 -126
  64. package/src/summarization/__tests__/node.test.ts +42 -0
  65. package/src/summarization/__tests__/trigger.test.ts +100 -1
  66. package/src/summarization/index.ts +47 -0
  67. package/src/summarization/node.ts +149 -77
  68. package/src/tools/ToolNode.ts +169 -391
  69. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  70. package/src/types/graph.ts +1 -80
  71. package/src/types/index.ts +0 -1
  72. package/src/types/run.ts +0 -20
  73. package/src/types/tools.ts +1 -41
  74. package/dist/cjs/hooks/HookRegistry.cjs +0 -162
  75. package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
  76. package/dist/cjs/hooks/executeHooks.cjs +0 -276
  77. package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
  78. package/dist/cjs/hooks/matchers.cjs +0 -256
  79. package/dist/cjs/hooks/matchers.cjs.map +0 -1
  80. package/dist/cjs/hooks/types.cjs +0 -27
  81. package/dist/cjs/hooks/types.cjs.map +0 -1
  82. package/dist/cjs/tools/BashExecutor.cjs +0 -175
  83. package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
  84. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
  85. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
  86. package/dist/cjs/tools/ReadFile.cjs +0 -43
  87. package/dist/cjs/tools/ReadFile.cjs.map +0 -1
  88. package/dist/cjs/tools/SkillTool.cjs +0 -50
  89. package/dist/cjs/tools/SkillTool.cjs.map +0 -1
  90. package/dist/cjs/tools/SubagentTool.cjs +0 -92
  91. package/dist/cjs/tools/SubagentTool.cjs.map +0 -1
  92. package/dist/cjs/tools/skillCatalog.cjs +0 -84
  93. package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
  94. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +0 -511
  95. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +0 -1
  96. package/dist/esm/hooks/HookRegistry.mjs +0 -160
  97. package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
  98. package/dist/esm/hooks/executeHooks.mjs +0 -273
  99. package/dist/esm/hooks/executeHooks.mjs.map +0 -1
  100. package/dist/esm/hooks/matchers.mjs +0 -251
  101. package/dist/esm/hooks/matchers.mjs.map +0 -1
  102. package/dist/esm/hooks/types.mjs +0 -25
  103. package/dist/esm/hooks/types.mjs.map +0 -1
  104. package/dist/esm/tools/BashExecutor.mjs +0 -169
  105. package/dist/esm/tools/BashExecutor.mjs.map +0 -1
  106. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
  107. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
  108. package/dist/esm/tools/ReadFile.mjs +0 -38
  109. package/dist/esm/tools/ReadFile.mjs.map +0 -1
  110. package/dist/esm/tools/SkillTool.mjs +0 -45
  111. package/dist/esm/tools/SkillTool.mjs.map +0 -1
  112. package/dist/esm/tools/SubagentTool.mjs +0 -85
  113. package/dist/esm/tools/SubagentTool.mjs.map +0 -1
  114. package/dist/esm/tools/skillCatalog.mjs +0 -82
  115. package/dist/esm/tools/skillCatalog.mjs.map +0 -1
  116. package/dist/esm/tools/subagent/SubagentExecutor.mjs +0 -505
  117. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +0 -1
  118. package/dist/types/hooks/HookRegistry.d.ts +0 -56
  119. package/dist/types/hooks/executeHooks.d.ts +0 -79
  120. package/dist/types/hooks/index.d.ts +0 -6
  121. package/dist/types/hooks/matchers.d.ts +0 -95
  122. package/dist/types/hooks/types.d.ts +0 -320
  123. package/dist/types/tools/BashExecutor.d.ts +0 -45
  124. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
  125. package/dist/types/tools/ReadFile.d.ts +0 -28
  126. package/dist/types/tools/SkillTool.d.ts +0 -40
  127. package/dist/types/tools/SubagentTool.d.ts +0 -36
  128. package/dist/types/tools/skillCatalog.d.ts +0 -19
  129. package/dist/types/tools/subagent/SubagentExecutor.d.ts +0 -137
  130. package/dist/types/tools/subagent/index.d.ts +0 -2
  131. package/dist/types/types/skill.d.ts +0 -9
  132. package/src/hooks/HookRegistry.ts +0 -208
  133. package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
  134. package/src/hooks/__tests__/compactHooks.test.ts +0 -214
  135. package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
  136. package/src/hooks/__tests__/integration.test.ts +0 -337
  137. package/src/hooks/__tests__/matchers.test.ts +0 -238
  138. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  139. package/src/hooks/executeHooks.ts +0 -375
  140. package/src/hooks/index.ts +0 -57
  141. package/src/hooks/matchers.ts +0 -280
  142. package/src/hooks/types.ts +0 -404
  143. package/src/messages/formatAgentMessages.skills.test.ts +0 -334
  144. package/src/scripts/multi-agent-subagent.ts +0 -246
  145. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  146. package/src/scripts/subagent-tools-debug.ts +0 -160
  147. package/src/specs/subagent.test.ts +0 -305
  148. package/src/tools/BashExecutor.ts +0 -205
  149. package/src/tools/BashProgrammaticToolCalling.ts +0 -397
  150. package/src/tools/ReadFile.ts +0 -39
  151. package/src/tools/SkillTool.ts +0 -46
  152. package/src/tools/SubagentTool.ts +0 -100
  153. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  154. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  155. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -1148
  156. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  157. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  158. package/src/tools/__tests__/subagentHooks.test.ts +0 -215
  159. package/src/tools/skillCatalog.ts +0 -126
  160. package/src/tools/subagent/SubagentExecutor.ts +0 -676
  161. package/src/tools/subagent/index.ts +0 -13
  162. package/src/types/skill.ts +0 -11
@@ -1,149 +0,0 @@
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
- });
@@ -1,161 +0,0 @@
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
- });
@@ -1,215 +0,0 @@
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
- });
@@ -1,126 +0,0 @@
1
- // src/tools/skillCatalog.ts
2
- import type { SkillCatalogEntry } from '@/types';
3
-
4
- const HEADER = '## Available Skills';
5
- const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
6
- const DEFAULT_BUDGET_PERCENT = 0.01;
7
- const DEFAULT_MAX_ENTRY_CHARS = 250;
8
- const DEFAULT_MIN_DESC_LENGTH = 20;
9
- const DEFAULT_CHARS_PER_TOKEN = 4;
10
-
11
- export type SkillCatalogOptions = {
12
- /** Total context window in tokens. Default: 200_000 */
13
- contextWindowTokens?: number;
14
- /** Fraction of context budget for catalog. Default: 0.01 (1%) */
15
- budgetPercent?: number;
16
- /** Max chars per entry description. Default: 250 */
17
- maxEntryChars?: number;
18
- /** Descriptions below this length trigger names-only fallback. Default: 20 */
19
- minDescLength?: number;
20
- /** Approximate chars per token for budget calculation. Default: 4 */
21
- charsPerToken?: number;
22
- };
23
-
24
- /**
25
- * Formats a skill catalog for injection into agent context.
26
- * Uses a truncation ladder: full descriptions, proportional truncation, names-only.
27
- * Returns empty string for empty input.
28
- */
29
- export function formatSkillCatalog(
30
- skills: SkillCatalogEntry[],
31
- opts?: SkillCatalogOptions
32
- ): string {
33
- if (skills.length === 0) return '';
34
-
35
- const contextWindowTokens =
36
- opts?.contextWindowTokens ?? DEFAULT_CONTEXT_WINDOW_TOKENS;
37
- const budgetPercent = opts?.budgetPercent ?? DEFAULT_BUDGET_PERCENT;
38
- const maxEntryChars = Math.max(
39
- 1,
40
- opts?.maxEntryChars ?? DEFAULT_MAX_ENTRY_CHARS
41
- );
42
- const minDescLength = opts?.minDescLength ?? DEFAULT_MIN_DESC_LENGTH;
43
- const charsPerToken = opts?.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
44
-
45
- const budgetChars = Math.floor(
46
- contextWindowTokens * budgetPercent * charsPerToken
47
- );
48
-
49
- const capped = skills.map((s) => ({
50
- name: s.name,
51
- description:
52
- s.description.length > maxEntryChars
53
- ? s.description.slice(0, maxEntryChars - 1) + '\u2026'
54
- : s.description,
55
- }));
56
-
57
- const fullOutput = formatEntries(capped);
58
- if (fullOutput.length <= budgetChars) return fullOutput;
59
-
60
- const headerLen = HEADER.length + 2;
61
- const newlineChars = capped.length > 1 ? capped.length - 1 : 0;
62
- const availableChars = budgetChars - headerLen - newlineChars;
63
- const perEntryOverhead = 4;
64
- const nameCharsTotal = capped.reduce(
65
- (sum, s) => sum + s.name.length + perEntryOverhead,
66
- 0
67
- );
68
- const availableForDescs = availableChars - nameCharsTotal;
69
-
70
- if (availableForDescs <= 0) {
71
- return fitNamesOnly(capped, budgetChars);
72
- }
73
-
74
- const maxDescPerEntry = Math.floor(availableForDescs / capped.length);
75
-
76
- if (maxDescPerEntry < minDescLength) {
77
- return fitNamesOnly(capped, budgetChars);
78
- }
79
-
80
- const truncated = capped.map((s) => ({
81
- name: s.name,
82
- description:
83
- s.description.length > maxDescPerEntry
84
- ? s.description.slice(0, maxDescPerEntry - 1) + '\u2026'
85
- : s.description,
86
- }));
87
-
88
- const result = formatEntries(truncated);
89
- if (result.length <= budgetChars) return result;
90
- return fitNamesOnly(capped, budgetChars);
91
- }
92
-
93
- function formatEntries(
94
- entries: { name: string; description: string }[]
95
- ): string {
96
- const lines = entries.map((e) =>
97
- e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`
98
- );
99
- return `${HEADER}\n\n${lines.join('\n')}`;
100
- }
101
-
102
- /** Names-only fallback that drops trailing entries if the list still exceeds budget. */
103
- function fitNamesOnly(
104
- entries: { name: string }[],
105
- budgetChars: number
106
- ): string {
107
- // Format: "HEADER\n\n- name1\n- name2\n..."
108
- // Running sum avoids O(n²) repeated string construction.
109
- const prefix = HEADER.length + 2; // "HEADER\n\n"
110
- const entryOverhead = 2; // "- "
111
- let total = prefix;
112
- let fitCount = 0;
113
-
114
- for (let i = 0; i < entries.length; i++) {
115
- const added = (i > 0 ? 1 : 0) + entryOverhead + entries[i].name.length;
116
- if (total + added > budgetChars) break;
117
- total += added;
118
- fitCount = i + 1;
119
- }
120
-
121
- if (fitCount === 0) return '';
122
- const namesOnly = entries
123
- .slice(0, fitCount)
124
- .map((s) => ({ name: s.name, description: '' }));
125
- return formatEntries(namesOnly);
126
- }