@librechat/agents 3.1.66-dev.0 → 3.1.67

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 (120) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +24 -15
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +0 -13
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +0 -3
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +0 -40
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/format.cjs +12 -74
  10. package/dist/cjs/messages/format.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +0 -111
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/tools/ToolNode.cjs +140 -304
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/esm/agents/AgentContext.mjs +24 -15
  16. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  17. package/dist/esm/common/enum.mjs +1 -12
  18. package/dist/esm/common/enum.mjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +0 -3
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/main.mjs +1 -10
  22. package/dist/esm/main.mjs.map +1 -1
  23. package/dist/esm/messages/format.mjs +4 -66
  24. package/dist/esm/messages/format.mjs.map +1 -1
  25. package/dist/esm/run.mjs +0 -111
  26. package/dist/esm/run.mjs.map +1 -1
  27. package/dist/esm/tools/ToolNode.mjs +142 -306
  28. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  29. package/dist/types/agents/AgentContext.d.ts +6 -0
  30. package/dist/types/common/enum.d.ts +1 -7
  31. package/dist/types/graphs/Graph.d.ts +0 -2
  32. package/dist/types/index.d.ts +0 -6
  33. package/dist/types/messages/format.d.ts +1 -2
  34. package/dist/types/run.d.ts +0 -1
  35. package/dist/types/tools/ToolNode.d.ts +2 -24
  36. package/dist/types/types/index.d.ts +0 -1
  37. package/dist/types/types/llm.d.ts +14 -2
  38. package/dist/types/types/run.d.ts +0 -20
  39. package/dist/types/types/tools.d.ts +1 -38
  40. package/package.json +1 -1
  41. package/src/agents/AgentContext.ts +28 -15
  42. package/src/agents/__tests__/AgentContext.test.ts +110 -0
  43. package/src/common/enum.ts +0 -12
  44. package/src/graphs/Graph.ts +0 -4
  45. package/src/index.ts +0 -8
  46. package/src/messages/format.ts +4 -74
  47. package/src/run.ts +0 -126
  48. package/src/tools/ToolNode.ts +169 -391
  49. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  50. package/src/types/index.ts +0 -1
  51. package/src/types/llm.ts +16 -2
  52. package/src/types/run.ts +0 -20
  53. package/src/types/tools.ts +1 -41
  54. package/dist/cjs/hooks/HookRegistry.cjs +0 -162
  55. package/dist/cjs/hooks/HookRegistry.cjs.map +0 -1
  56. package/dist/cjs/hooks/executeHooks.cjs +0 -276
  57. package/dist/cjs/hooks/executeHooks.cjs.map +0 -1
  58. package/dist/cjs/hooks/matchers.cjs +0 -256
  59. package/dist/cjs/hooks/matchers.cjs.map +0 -1
  60. package/dist/cjs/hooks/types.cjs +0 -27
  61. package/dist/cjs/hooks/types.cjs.map +0 -1
  62. package/dist/cjs/tools/BashExecutor.cjs +0 -175
  63. package/dist/cjs/tools/BashExecutor.cjs.map +0 -1
  64. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -296
  65. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +0 -1
  66. package/dist/cjs/tools/ReadFile.cjs +0 -43
  67. package/dist/cjs/tools/ReadFile.cjs.map +0 -1
  68. package/dist/cjs/tools/SkillTool.cjs +0 -50
  69. package/dist/cjs/tools/SkillTool.cjs.map +0 -1
  70. package/dist/cjs/tools/skillCatalog.cjs +0 -84
  71. package/dist/cjs/tools/skillCatalog.cjs.map +0 -1
  72. package/dist/esm/hooks/HookRegistry.mjs +0 -160
  73. package/dist/esm/hooks/HookRegistry.mjs.map +0 -1
  74. package/dist/esm/hooks/executeHooks.mjs +0 -273
  75. package/dist/esm/hooks/executeHooks.mjs.map +0 -1
  76. package/dist/esm/hooks/matchers.mjs +0 -251
  77. package/dist/esm/hooks/matchers.mjs.map +0 -1
  78. package/dist/esm/hooks/types.mjs +0 -25
  79. package/dist/esm/hooks/types.mjs.map +0 -1
  80. package/dist/esm/tools/BashExecutor.mjs +0 -169
  81. package/dist/esm/tools/BashExecutor.mjs.map +0 -1
  82. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -287
  83. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +0 -1
  84. package/dist/esm/tools/ReadFile.mjs +0 -38
  85. package/dist/esm/tools/ReadFile.mjs.map +0 -1
  86. package/dist/esm/tools/SkillTool.mjs +0 -45
  87. package/dist/esm/tools/SkillTool.mjs.map +0 -1
  88. package/dist/esm/tools/skillCatalog.mjs +0 -82
  89. package/dist/esm/tools/skillCatalog.mjs.map +0 -1
  90. package/dist/types/hooks/HookRegistry.d.ts +0 -56
  91. package/dist/types/hooks/executeHooks.d.ts +0 -79
  92. package/dist/types/hooks/index.d.ts +0 -6
  93. package/dist/types/hooks/matchers.d.ts +0 -95
  94. package/dist/types/hooks/types.d.ts +0 -309
  95. package/dist/types/tools/BashExecutor.d.ts +0 -45
  96. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +0 -72
  97. package/dist/types/tools/ReadFile.d.ts +0 -28
  98. package/dist/types/tools/SkillTool.d.ts +0 -40
  99. package/dist/types/tools/skillCatalog.d.ts +0 -19
  100. package/dist/types/types/skill.d.ts +0 -9
  101. package/src/hooks/HookRegistry.ts +0 -208
  102. package/src/hooks/__tests__/HookRegistry.test.ts +0 -190
  103. package/src/hooks/__tests__/executeHooks.test.ts +0 -1013
  104. package/src/hooks/__tests__/integration.test.ts +0 -337
  105. package/src/hooks/__tests__/matchers.test.ts +0 -238
  106. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  107. package/src/hooks/executeHooks.ts +0 -375
  108. package/src/hooks/index.ts +0 -55
  109. package/src/hooks/matchers.ts +0 -280
  110. package/src/hooks/types.ts +0 -388
  111. package/src/messages/formatAgentMessages.skills.test.ts +0 -334
  112. package/src/tools/BashExecutor.ts +0 -205
  113. package/src/tools/BashProgrammaticToolCalling.ts +0 -397
  114. package/src/tools/ReadFile.ts +0 -39
  115. package/src/tools/SkillTool.ts +0 -46
  116. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  117. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  118. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  119. package/src/tools/skillCatalog.ts +0 -126
  120. package/src/types/skill.ts +0 -11
@@ -1,442 +0,0 @@
1
- import { z } from 'zod';
2
- import { tool } from '@langchain/core/tools';
3
- import { describe, it, expect } from '@jest/globals';
4
- import { AIMessage, HumanMessage } from '@langchain/core/messages';
5
- import type { BaseMessage } from '@langchain/core/messages';
6
- import type { StructuredToolInterface } from '@langchain/core/tools';
7
- import type * as t from '@/types';
8
- import * as events from '@/utils/events';
9
- import { ToolNode } from '../ToolNode';
10
- import { Constants } from '@/common';
11
- import {
12
- SkillToolDescription,
13
- SkillToolDefinition,
14
- SkillToolSchema,
15
- SkillToolName,
16
- } from '../SkillTool';
17
-
18
- describe('SkillTool', () => {
19
- describe('schema structure', () => {
20
- it('has skillName as required string property', () => {
21
- expect(SkillToolSchema.properties.skillName.type).toBe('string');
22
- expect(SkillToolSchema.required).toContain('skillName');
23
- });
24
-
25
- it('has args as optional string property', () => {
26
- expect(SkillToolSchema.properties.args.type).toBe('string');
27
- expect(SkillToolSchema.required).not.toContain('args');
28
- });
29
-
30
- it('is an object type schema', () => {
31
- expect(SkillToolSchema.type).toBe('object');
32
- });
33
- });
34
-
35
- describe('SkillToolDefinition', () => {
36
- it('has correct name', () => {
37
- expect(SkillToolDefinition.name).toBe(Constants.SKILL_TOOL);
38
- });
39
-
40
- it('references the same SkillToolSchema object (no duplication)', () => {
41
- expect(SkillToolDefinition.parameters).toBe(SkillToolSchema);
42
- });
43
-
44
- it('has a non-empty description', () => {
45
- expect(SkillToolDefinition.description).toBe(SkillToolDescription);
46
- expect(SkillToolDefinition.description.length).toBeGreaterThan(0);
47
- });
48
- });
49
-
50
- describe('SkillToolName', () => {
51
- it('equals Constants.SKILL_TOOL', () => {
52
- expect(SkillToolName).toBe('skill');
53
- expect(SkillToolName).toBe(Constants.SKILL_TOOL);
54
- });
55
- });
56
-
57
- describe('InjectedMessage type-check', () => {
58
- it('constructs a valid ToolExecuteResult with injectedMessages', () => {
59
- const result: t.ToolExecuteResult = {
60
- toolCallId: 'call_1',
61
- content: 'Skill loaded successfully.',
62
- status: 'success',
63
- injectedMessages: [
64
- {
65
- role: 'user',
66
- content: '# PDF Processor Instructions\n\nFollow these steps...',
67
- isMeta: true,
68
- source: 'skill',
69
- skillName: 'pdf-processor',
70
- },
71
- {
72
- role: 'system',
73
- content: 'Skill files are available at /skills/pdf-processor/',
74
- source: 'skill',
75
- skillName: 'pdf-processor',
76
- },
77
- ],
78
- };
79
-
80
- expect(result.injectedMessages).toHaveLength(2);
81
- expect(result.injectedMessages![0].role).toBe('user');
82
- expect(result.injectedMessages![1].role).toBe('system');
83
- });
84
-
85
- it('accepts MessageContentComplex[] content', () => {
86
- const result: t.ToolExecuteResult = {
87
- toolCallId: 'call_1',
88
- content: '',
89
- status: 'success',
90
- injectedMessages: [
91
- {
92
- role: 'user',
93
- content: [
94
- { type: 'text', text: 'Skill instructions here' },
95
- { type: 'image_url', image_url: { url: 'data:image/png;...' } },
96
- ],
97
- isMeta: true,
98
- source: 'skill',
99
- skillName: 'visual-skill',
100
- },
101
- ],
102
- };
103
-
104
- expect(Array.isArray(result.injectedMessages![0].content)).toBe(true);
105
- });
106
- });
107
-
108
- describe('ToolNode injectedMessages plumbing (event-driven)', () => {
109
- const createDummyTool = (name = 'dummy'): StructuredToolInterface =>
110
- tool(async () => 'dummy', {
111
- name,
112
- description: 'dummy',
113
- schema: z.object({ x: z.string() }),
114
- }) as unknown as StructuredToolInterface;
115
-
116
- function mockEventDispatch(
117
- mockResults: t.ToolExecuteResult[]
118
- ): jest.SpyInstance {
119
- return jest
120
- .spyOn(events, 'safeDispatchCustomEvent')
121
- .mockImplementation(async (_event, data) => {
122
- const request = data as Record<string, unknown>;
123
- if (typeof request.resolve === 'function') {
124
- (request.resolve as (r: t.ToolExecuteResult[]) => void)(
125
- mockResults
126
- );
127
- }
128
- });
129
- }
130
-
131
- afterEach(() => {
132
- jest.restoreAllMocks();
133
- });
134
-
135
- it('appends injected messages AFTER ToolMessages in output', async () => {
136
- const toolNode = new ToolNode({
137
- tools: [createDummyTool()],
138
- eventDrivenMode: true,
139
- agentId: 'test-agent',
140
- toolCallStepIds: new Map([['call_1', 'step_1']]),
141
- });
142
-
143
- const aiMsg = new AIMessage({
144
- content: '',
145
- tool_calls: [{ id: 'call_1', name: 'dummy', args: { x: 'hello' } }],
146
- });
147
-
148
- mockEventDispatch([
149
- {
150
- toolCallId: 'call_1',
151
- content: 'Tool result text',
152
- status: 'success',
153
- injectedMessages: [
154
- {
155
- role: 'user',
156
- content: 'Injected skill body content',
157
- isMeta: true,
158
- source: 'skill',
159
- skillName: 'test-skill',
160
- },
161
- {
162
- role: 'system',
163
- content: 'System context hint',
164
- source: 'system',
165
- },
166
- ],
167
- },
168
- ]);
169
-
170
- const result = await toolNode.invoke({ messages: [aiMsg] });
171
- const messages = (result as { messages: BaseMessage[] }).messages;
172
-
173
- expect(messages).toHaveLength(3);
174
-
175
- // ToolMessage comes FIRST (preserves AIMessage -> ToolMessage adjacency)
176
- expect(messages[0]._getType()).toBe('tool');
177
-
178
- // Injected messages come AFTER
179
- const second = messages[1] as HumanMessage;
180
- expect(second).toBeInstanceOf(HumanMessage);
181
- expect(second.content).toBe('Injected skill body content');
182
- expect(second.additional_kwargs.role).toBe('user');
183
- expect(second.additional_kwargs.isMeta).toBe(true);
184
- expect(second.additional_kwargs.source).toBe('skill');
185
- expect(second.additional_kwargs.skillName).toBe('test-skill');
186
-
187
- // role: 'system' also becomes HumanMessage (avoids provider rejections)
188
- const third = messages[2] as HumanMessage;
189
- expect(third).toBeInstanceOf(HumanMessage);
190
- expect(third.content).toBe('System context hint');
191
- expect(third.additional_kwargs.role).toBe('system');
192
- expect(third.additional_kwargs.source).toBe('system');
193
- });
194
-
195
- it('returns only ToolMessages when no injectedMessages present', async () => {
196
- const toolNode = new ToolNode({
197
- tools: [createDummyTool()],
198
- eventDrivenMode: true,
199
- agentId: 'test-agent',
200
- toolCallStepIds: new Map([['call_2', 'step_2']]),
201
- });
202
-
203
- const aiMsg = new AIMessage({
204
- content: '',
205
- tool_calls: [{ id: 'call_2', name: 'dummy', args: { x: 'test' } }],
206
- });
207
-
208
- mockEventDispatch([
209
- { toolCallId: 'call_2', content: 'Normal result', status: 'success' },
210
- ]);
211
-
212
- const result = await toolNode.invoke({ messages: [aiMsg] });
213
- const messages = (result as { messages: BaseMessage[] }).messages;
214
-
215
- expect(messages).toHaveLength(1);
216
- expect(messages[0]._getType()).toBe('tool');
217
- });
218
-
219
- it('passes MessageContentComplex[] content through without stringifying', async () => {
220
- const toolNode = new ToolNode({
221
- tools: [createDummyTool()],
222
- eventDrivenMode: true,
223
- agentId: 'test-agent',
224
- toolCallStepIds: new Map([['call_3', 'step_3']]),
225
- });
226
-
227
- const aiMsg = new AIMessage({
228
- content: '',
229
- tool_calls: [{ id: 'call_3', name: 'dummy', args: { x: 'test' } }],
230
- });
231
-
232
- const complexContent = [
233
- { type: 'text', text: 'Multi-part skill instructions' },
234
- { type: 'text', text: 'Second part of instructions' },
235
- ];
236
-
237
- mockEventDispatch([
238
- {
239
- toolCallId: 'call_3',
240
- content: '',
241
- status: 'success',
242
- injectedMessages: [
243
- {
244
- role: 'user' as const,
245
- content: complexContent,
246
- isMeta: true,
247
- source: 'skill' as const,
248
- skillName: 'complex-skill',
249
- },
250
- ],
251
- },
252
- ]);
253
-
254
- const result = await toolNode.invoke({ messages: [aiMsg] });
255
- const messages = (result as { messages: BaseMessage[] }).messages;
256
-
257
- expect(messages).toHaveLength(2);
258
- // ToolMessage first
259
- expect(messages[0]._getType()).toBe('tool');
260
- // Injected message second with array content preserved (not stringified)
261
- const injected = messages[1] as HumanMessage;
262
- expect(injected).toBeInstanceOf(HumanMessage);
263
- expect(Array.isArray(injected.content)).toBe(true);
264
- expect(injected.content).toEqual(complexContent);
265
- });
266
-
267
- it('aggregates injected messages from multiple tool calls', async () => {
268
- const toolNode = new ToolNode({
269
- tools: [createDummyTool('tool_a'), createDummyTool('tool_b')],
270
- eventDrivenMode: true,
271
- agentId: 'test-agent',
272
- toolCallStepIds: new Map([
273
- ['call_a', 'step_a'],
274
- ['call_b', 'step_b'],
275
- ]),
276
- });
277
-
278
- const aiMsg = new AIMessage({
279
- content: '',
280
- tool_calls: [
281
- { id: 'call_a', name: 'tool_a', args: { x: 'a' } },
282
- { id: 'call_b', name: 'tool_b', args: { x: 'b' } },
283
- ],
284
- });
285
-
286
- mockEventDispatch([
287
- {
288
- toolCallId: 'call_a',
289
- content: 'Result A',
290
- status: 'success',
291
- injectedMessages: [
292
- {
293
- role: 'user',
294
- content: 'Injected from A',
295
- isMeta: true,
296
- source: 'skill',
297
- skillName: 'skill-a',
298
- },
299
- ],
300
- },
301
- {
302
- toolCallId: 'call_b',
303
- content: 'Result B',
304
- status: 'success',
305
- injectedMessages: [
306
- {
307
- role: 'user',
308
- content: 'Injected from B',
309
- isMeta: true,
310
- source: 'skill',
311
- skillName: 'skill-b',
312
- },
313
- ],
314
- },
315
- ]);
316
-
317
- const result = await toolNode.invoke({ messages: [aiMsg] });
318
- const messages = (result as { messages: BaseMessage[] }).messages;
319
-
320
- // 2 ToolMessages + 2 injected messages
321
- expect(messages).toHaveLength(4);
322
- // ToolMessages come first
323
- expect(messages[0]._getType()).toBe('tool');
324
- expect(messages[1]._getType()).toBe('tool');
325
- // Injected messages come after all ToolMessages
326
- expect(messages[2]).toBeInstanceOf(HumanMessage);
327
- expect((messages[2] as HumanMessage).content).toBe('Injected from A');
328
- expect(messages[3]).toBeInstanceOf(HumanMessage);
329
- expect((messages[3] as HumanMessage).content).toBe('Injected from B');
330
- });
331
-
332
- it('handles mixed mode: direct tools + event-driven with injected messages', async () => {
333
- const directTool = tool(async () => 'direct result', {
334
- name: 'handoff_tool',
335
- description: 'A direct tool',
336
- schema: z.object({ target: z.string() }),
337
- }) as unknown as StructuredToolInterface;
338
-
339
- const eventTool = createDummyTool('event_tool');
340
-
341
- const toolNode = new ToolNode({
342
- tools: [directTool, eventTool],
343
- eventDrivenMode: true,
344
- agentId: 'test-agent',
345
- directToolNames: new Set(['handoff_tool']),
346
- toolCallStepIds: new Map([
347
- ['call_direct', 'step_direct'],
348
- ['call_event', 'step_event'],
349
- ]),
350
- });
351
-
352
- const aiMsg = new AIMessage({
353
- content: '',
354
- tool_calls: [
355
- {
356
- id: 'call_direct',
357
- name: 'handoff_tool',
358
- args: { target: 'agent-2' },
359
- },
360
- { id: 'call_event', name: 'event_tool', args: { x: 'hello' } },
361
- ],
362
- });
363
-
364
- mockEventDispatch([
365
- {
366
- toolCallId: 'call_event',
367
- content: 'Event result',
368
- status: 'success',
369
- injectedMessages: [
370
- {
371
- role: 'user',
372
- content: 'Skill body from event tool',
373
- isMeta: true,
374
- source: 'skill',
375
- skillName: 'my-skill',
376
- },
377
- ],
378
- },
379
- ]);
380
-
381
- const result = await toolNode.invoke({ messages: [aiMsg] });
382
- const messages = (result as { messages: BaseMessage[] }).messages;
383
-
384
- // directOutputs first, then eventResult.toolMessages, then eventResult.injected
385
- expect(messages.length).toBeGreaterThanOrEqual(3);
386
- // Direct tool result (ToolMessage from runTool)
387
- expect(messages[0]._getType()).toBe('tool');
388
- // Event tool result (ToolMessage from dispatchToolEvents)
389
- expect(messages[1]._getType()).toBe('tool');
390
- // Injected message last
391
- const last = messages[messages.length - 1] as HumanMessage;
392
- expect(last).toBeInstanceOf(HumanMessage);
393
- expect(last.content).toBe('Skill body from event tool');
394
- expect(last.additional_kwargs.skillName).toBe('my-skill');
395
- });
396
-
397
- it('includes injected messages even when tool result has error status', async () => {
398
- const toolNode = new ToolNode({
399
- tools: [createDummyTool()],
400
- eventDrivenMode: true,
401
- agentId: 'test-agent',
402
- toolCallStepIds: new Map([['call_err', 'step_err']]),
403
- });
404
-
405
- const aiMsg = new AIMessage({
406
- content: '',
407
- tool_calls: [{ id: 'call_err', name: 'dummy', args: { x: 'fail' } }],
408
- });
409
-
410
- mockEventDispatch([
411
- {
412
- toolCallId: 'call_err',
413
- content: '',
414
- status: 'error',
415
- errorMessage: 'Skill not found',
416
- injectedMessages: [
417
- {
418
- role: 'user',
419
- content: 'Partial context before failure',
420
- isMeta: true,
421
- source: 'skill',
422
- skillName: 'broken-skill',
423
- },
424
- ],
425
- },
426
- ]);
427
-
428
- const result = await toolNode.invoke({ messages: [aiMsg] });
429
- const messages = (result as { messages: BaseMessage[] }).messages;
430
-
431
- expect(messages).toHaveLength(2);
432
- // Error ToolMessage first
433
- expect(messages[0]._getType()).toBe('tool');
434
- expect(String(messages[0].content)).toContain('Skill not found');
435
- // Injected message still included
436
- const injected = messages[1] as HumanMessage;
437
- expect(injected).toBeInstanceOf(HumanMessage);
438
- expect(injected.content).toBe('Partial context before failure');
439
- expect(injected.additional_kwargs.skillName).toBe('broken-skill');
440
- });
441
- });
442
- });
@@ -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
- });