@charming_groot/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.d.ts +46 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +139 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/message-manager.d.ts +46 -0
- package/dist/message-manager.d.ts.map +1 -0
- package/dist/message-manager.js +152 -0
- package/dist/message-manager.js.map +1 -0
- package/dist/permission.d.ts +37 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +62 -0
- package/dist/permission.js.map +1 -0
- package/dist/session-manager.d.ts +31 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +101 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/skill-tool.d.ts +38 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +112 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-tool.d.ts +39 -0
- package/dist/sub-agent-tool.d.ts.map +1 -0
- package/dist/sub-agent-tool.js +80 -0
- package/dist/sub-agent-tool.js.map +1 -0
- package/dist/token-counter.d.ts +16 -0
- package/dist/token-counter.d.ts.map +1 -0
- package/dist/token-counter.js +69 -0
- package/dist/token-counter.js.map +1 -0
- package/dist/tool-dispatcher.d.ts +15 -0
- package/dist/tool-dispatcher.d.ts.map +1 -0
- package/dist/tool-dispatcher.js +94 -0
- package/dist/tool-dispatcher.js.map +1 -0
- package/package.json +34 -0
- package/src/agent-loop.ts +210 -0
- package/src/index.ts +19 -0
- package/src/message-manager.ts +184 -0
- package/src/permission.ts +104 -0
- package/src/session-manager.ts +121 -0
- package/src/skill-tool.ts +155 -0
- package/src/sub-agent-tool.ts +122 -0
- package/src/token-counter.ts +79 -0
- package/src/tool-dispatcher.ts +124 -0
- package/tests/agent-loop.test.ts +372 -0
- package/tests/message-manager-new.test.ts +204 -0
- package/tests/message-manager.test.ts +195 -0
- package/tests/permission.test.ts +148 -0
- package/tests/session-manager.test.ts +106 -0
- package/tests/skill-tool.test.ts +119 -0
- package/tests/sub-agent-tool.test.ts +198 -0
- package/tests/token-counter.test.ts +77 -0
- package/tests/tool-dispatcher.test.ts +181 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { SubAgentTool } from '../src/sub-agent-tool.js';
|
|
3
|
+
import type { ILlmProvider, ITool, LlmResponse, AgentConfig } from '@charming_groot/core';
|
|
4
|
+
import { Registry, RunContext } from '@charming_groot/core';
|
|
5
|
+
|
|
6
|
+
const TEST_CONFIG: AgentConfig = {
|
|
7
|
+
provider: { providerId: 'test', model: 'test', auth: { type: 'api-key' as const, apiKey: 'test' }, maxTokens: 4096, temperature: 0.7 },
|
|
8
|
+
maxIterations: 10,
|
|
9
|
+
workingDirectory: '/tmp',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function createMockProvider(content: string): ILlmProvider {
|
|
13
|
+
const response: LlmResponse = {
|
|
14
|
+
content,
|
|
15
|
+
stopReason: 'end_turn',
|
|
16
|
+
toolCalls: [],
|
|
17
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
providerId: 'mock',
|
|
21
|
+
chat: vi.fn().mockResolvedValue(response),
|
|
22
|
+
stream: vi.fn() as unknown as ILlmProvider['stream'],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('SubAgentTool', () => {
|
|
27
|
+
it('should describe itself with a task parameter', () => {
|
|
28
|
+
const tool = new SubAgentTool({
|
|
29
|
+
name: 'researcher',
|
|
30
|
+
description: 'Research a topic',
|
|
31
|
+
provider: createMockProvider(''),
|
|
32
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const desc = tool.describe();
|
|
36
|
+
expect(desc.name).toBe('researcher');
|
|
37
|
+
expect(desc.parameters).toHaveLength(1);
|
|
38
|
+
expect(desc.parameters[0]?.name).toBe('task');
|
|
39
|
+
expect(desc.parameters[0]?.required).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should execute sub-agent and return result', async () => {
|
|
43
|
+
const provider = createMockProvider('Research complete: found 3 papers');
|
|
44
|
+
const tool = new SubAgentTool({
|
|
45
|
+
name: 'researcher',
|
|
46
|
+
description: 'Research a topic',
|
|
47
|
+
provider,
|
|
48
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
49
|
+
systemPrompt: 'You are a researcher.',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const context = new RunContext(TEST_CONFIG);
|
|
53
|
+
const result = await tool.execute({ task: 'Find papers on transformers' }, context);
|
|
54
|
+
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.output).toBe('Research complete: found 3 papers');
|
|
57
|
+
expect(provider.chat).toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return error for missing task parameter', async () => {
|
|
61
|
+
const tool = new SubAgentTool({
|
|
62
|
+
name: 'researcher',
|
|
63
|
+
description: 'Research',
|
|
64
|
+
provider: createMockProvider(''),
|
|
65
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const context = new RunContext(TEST_CONFIG);
|
|
69
|
+
const result = await tool.execute({}, context);
|
|
70
|
+
expect(result.success).toBe(false);
|
|
71
|
+
expect(result.error).toContain('Missing or empty "task"');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return error for empty task', async () => {
|
|
75
|
+
const tool = new SubAgentTool({
|
|
76
|
+
name: 'researcher',
|
|
77
|
+
description: 'Research',
|
|
78
|
+
provider: createMockProvider(''),
|
|
79
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const context = new RunContext(TEST_CONFIG);
|
|
83
|
+
const result = await tool.execute({ task: ' ' }, context);
|
|
84
|
+
expect(result.success).toBe(false);
|
|
85
|
+
expect(result.error).toContain('Missing or empty "task"');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should catch sub-agent errors gracefully', async () => {
|
|
89
|
+
const provider: ILlmProvider = {
|
|
90
|
+
providerId: 'mock',
|
|
91
|
+
chat: vi.fn().mockRejectedValue(new Error('API rate limit')),
|
|
92
|
+
stream: vi.fn() as unknown as ILlmProvider['stream'],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const tool = new SubAgentTool({
|
|
96
|
+
name: 'researcher',
|
|
97
|
+
description: 'Research',
|
|
98
|
+
provider,
|
|
99
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const context = new RunContext(TEST_CONFIG);
|
|
103
|
+
const result = await tool.execute({ task: 'do something' }, context);
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
expect(result.error).toContain('API rate limit');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should propagate parent abort to child', async () => {
|
|
109
|
+
// Provider that takes a while — simulated by never resolving until abort
|
|
110
|
+
let chatCallCount = 0;
|
|
111
|
+
const provider: ILlmProvider = {
|
|
112
|
+
providerId: 'mock',
|
|
113
|
+
chat: vi.fn().mockImplementation(async () => {
|
|
114
|
+
chatCallCount++;
|
|
115
|
+
// First call returns tool_use to trigger iteration loop
|
|
116
|
+
if (chatCallCount === 1) {
|
|
117
|
+
return {
|
|
118
|
+
content: 'Working...',
|
|
119
|
+
stopReason: 'end_turn',
|
|
120
|
+
toolCalls: [],
|
|
121
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: 'Done',
|
|
126
|
+
stopReason: 'end_turn',
|
|
127
|
+
toolCalls: [],
|
|
128
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
129
|
+
};
|
|
130
|
+
}),
|
|
131
|
+
stream: vi.fn() as unknown as ILlmProvider['stream'],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const tool = new SubAgentTool({
|
|
135
|
+
name: 'worker',
|
|
136
|
+
description: 'Do work',
|
|
137
|
+
provider,
|
|
138
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const context = new RunContext(TEST_CONFIG);
|
|
142
|
+
// Sub-agent completes before abort in this case — just verify no crash
|
|
143
|
+
const result = await tool.execute({ task: 'work' }, context);
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should use parent workingDirectory', async () => {
|
|
148
|
+
const provider = createMockProvider('done');
|
|
149
|
+
const tool = new SubAgentTool({
|
|
150
|
+
name: 'worker',
|
|
151
|
+
description: 'Work',
|
|
152
|
+
provider,
|
|
153
|
+
toolRegistry: new Registry<ITool>('Tool'),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const config: AgentConfig = { ...TEST_CONFIG, workingDirectory: '/custom/dir' };
|
|
157
|
+
const context = new RunContext(config);
|
|
158
|
+
const result = await tool.execute({ task: 'check dir' }, context);
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should respect custom maxIterations', async () => {
|
|
163
|
+
// Create a provider that always returns tool calls to exhaust iterations
|
|
164
|
+
const toolResponse: LlmResponse = {
|
|
165
|
+
content: 'calling tool',
|
|
166
|
+
stopReason: 'tool_use',
|
|
167
|
+
toolCalls: [{ id: 'tc-1', name: 'noop', arguments: '{}' }],
|
|
168
|
+
usage: { inputTokens: 10, outputTokens: 10 },
|
|
169
|
+
};
|
|
170
|
+
const provider: ILlmProvider = {
|
|
171
|
+
providerId: 'mock',
|
|
172
|
+
chat: vi.fn().mockResolvedValue(toolResponse),
|
|
173
|
+
stream: vi.fn() as unknown as ILlmProvider['stream'],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const toolRegistry = new Registry<ITool>('Tool');
|
|
177
|
+
const noopTool: ITool = {
|
|
178
|
+
name: 'noop',
|
|
179
|
+
requiresPermission: false,
|
|
180
|
+
describe: () => ({ name: 'noop', description: 'noop', parameters: [] }),
|
|
181
|
+
execute: vi.fn().mockResolvedValue({ success: true, output: 'ok' }),
|
|
182
|
+
};
|
|
183
|
+
toolRegistry.register('noop', noopTool);
|
|
184
|
+
|
|
185
|
+
const tool = new SubAgentTool({
|
|
186
|
+
name: 'worker',
|
|
187
|
+
description: 'Work',
|
|
188
|
+
provider,
|
|
189
|
+
toolRegistry,
|
|
190
|
+
maxIterations: 3,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const context = new RunContext(TEST_CONFIG);
|
|
194
|
+
const result = await tool.execute({ task: 'loop' }, context);
|
|
195
|
+
// Should stop after 3 iterations (maxIterations)
|
|
196
|
+
expect(provider.chat).toHaveBeenCalledTimes(3);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
countTextTokens,
|
|
4
|
+
countMessageTokens,
|
|
5
|
+
countHistoryTokens,
|
|
6
|
+
} from '../src/token-counter.js';
|
|
7
|
+
import type { Message } from '@charming_groot/core';
|
|
8
|
+
|
|
9
|
+
describe('countTextTokens', () => {
|
|
10
|
+
it('빈 문자열은 0을 반환한다', () => {
|
|
11
|
+
expect(countTextTokens('')).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('짧은 텍스트에서 양수를 반환한다', () => {
|
|
15
|
+
expect(countTextTokens('hello world')).toBeGreaterThan(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('긴 텍스트일수록 토큰 수가 많다', () => {
|
|
19
|
+
const short = countTextTokens('hello');
|
|
20
|
+
const long = countTextTokens('hello world this is a longer sentence with many more tokens in it');
|
|
21
|
+
expect(long).toBeGreaterThan(short);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('동일한 입력에 대해 일관된 결과를 반환한다', () => {
|
|
25
|
+
const text = 'consistent token count test';
|
|
26
|
+
expect(countTextTokens(text)).toBe(countTextTokens(text));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('countMessageTokens', () => {
|
|
31
|
+
it('content가 있는 user 메시지를 카운트한다', () => {
|
|
32
|
+
const msg: Message = { role: 'user', content: 'hello there' };
|
|
33
|
+
expect(countMessageTokens(msg)).toBeGreaterThan(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('tool call이 있는 assistant 메시지는 더 많은 토큰을 가진다', () => {
|
|
37
|
+
const withoutTools: Message = {
|
|
38
|
+
role: 'assistant',
|
|
39
|
+
content: 'I will call a tool',
|
|
40
|
+
};
|
|
41
|
+
const withTools: Message = {
|
|
42
|
+
role: 'assistant',
|
|
43
|
+
content: 'I will call a tool',
|
|
44
|
+
toolCalls: [{ id: 'tc1', name: 'get_schema', arguments: '{}' }],
|
|
45
|
+
};
|
|
46
|
+
expect(countMessageTokens(withTools)).toBeGreaterThan(countMessageTokens(withoutTools));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('tool result가 있는 메시지를 카운트한다', () => {
|
|
50
|
+
const msg: Message = {
|
|
51
|
+
role: 'user',
|
|
52
|
+
content: '',
|
|
53
|
+
toolResults: [{ toolCallId: 'tc1', content: '{"table": "users"}' }],
|
|
54
|
+
};
|
|
55
|
+
expect(countMessageTokens(msg)).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('overhead(4)가 항상 포함된다', () => {
|
|
59
|
+
const empty: Message = { role: 'user', content: '' };
|
|
60
|
+
expect(countMessageTokens(empty)).toBeGreaterThanOrEqual(4);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('countHistoryTokens', () => {
|
|
65
|
+
it('빈 배열은 reply overhead(3)만 반환한다', () => {
|
|
66
|
+
expect(countHistoryTokens([])).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('메시지가 많을수록 토큰 수가 많다', () => {
|
|
70
|
+
const msgs: Message[] = [
|
|
71
|
+
{ role: 'user', content: 'first message with some content' },
|
|
72
|
+
{ role: 'assistant', content: 'response to the first message' },
|
|
73
|
+
{ role: 'user', content: 'second message with more content' },
|
|
74
|
+
];
|
|
75
|
+
expect(countHistoryTokens(msgs)).toBeGreaterThan(countHistoryTokens([msgs[0]]));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ToolDispatcher } from '../src/tool-dispatcher.js';
|
|
3
|
+
import { PermissionManager } from '../src/permission.js';
|
|
4
|
+
import type { ITool, ToolCall, AgentConfig } from '@charming_groot/core';
|
|
5
|
+
import { Registry, RunContext, PermissionDeniedError } from '@charming_groot/core';
|
|
6
|
+
|
|
7
|
+
const TEST_CONFIG: AgentConfig = {
|
|
8
|
+
provider: { providerId: 'test', model: 'test', auth: { type: 'api-key' as const, apiKey: 'test' }, maxTokens: 4096, temperature: 0.7 },
|
|
9
|
+
maxIterations: 50,
|
|
10
|
+
workingDirectory: '/tmp',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function createMockTool(name: string, requiresPermission = false): ITool {
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
requiresPermission,
|
|
17
|
+
describe: () => ({ name, description: 'Mock tool', parameters: [] }),
|
|
18
|
+
execute: vi.fn().mockResolvedValue({ success: true, output: `${name} result` }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('ToolDispatcher', () => {
|
|
23
|
+
let toolRegistry: Registry<ITool>;
|
|
24
|
+
let permissionManager: PermissionManager;
|
|
25
|
+
let dispatcher: ToolDispatcher;
|
|
26
|
+
let context: RunContext;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
toolRegistry = new Registry<ITool>('Tool');
|
|
30
|
+
permissionManager = new PermissionManager();
|
|
31
|
+
dispatcher = new ToolDispatcher(toolRegistry, permissionManager);
|
|
32
|
+
context = new RunContext(TEST_CONFIG);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should dispatch a tool call', async () => {
|
|
36
|
+
const tool = createMockTool('file_read');
|
|
37
|
+
toolRegistry.register('file_read', tool);
|
|
38
|
+
|
|
39
|
+
const toolCall: ToolCall = {
|
|
40
|
+
id: 'tc-1',
|
|
41
|
+
name: 'file_read',
|
|
42
|
+
arguments: '{"path":"test.txt"}',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = await dispatcher.dispatch(toolCall, context);
|
|
46
|
+
expect(result.success).toBe(true);
|
|
47
|
+
expect(result.output).toBe('file_read result');
|
|
48
|
+
expect(tool.execute).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return error for unknown tool', async () => {
|
|
52
|
+
const toolCall: ToolCall = {
|
|
53
|
+
id: 'tc-1',
|
|
54
|
+
name: 'unknown_tool',
|
|
55
|
+
arguments: '{}',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = await dispatcher.dispatch(toolCall, context);
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
expect(result.error).toContain('Unknown tool');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return error for invalid JSON arguments', async () => {
|
|
64
|
+
const tool = createMockTool('file_read');
|
|
65
|
+
toolRegistry.register('file_read', tool);
|
|
66
|
+
|
|
67
|
+
const toolCall: ToolCall = {
|
|
68
|
+
id: 'tc-1',
|
|
69
|
+
name: 'file_read',
|
|
70
|
+
arguments: 'not json',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await dispatcher.dispatch(toolCall, context);
|
|
74
|
+
expect(result.success).toBe(false);
|
|
75
|
+
expect(result.error).toContain('Invalid tool arguments');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should throw PermissionDeniedError when denied', async () => {
|
|
79
|
+
const handler = vi.fn().mockResolvedValue(false);
|
|
80
|
+
const pm = new PermissionManager(handler);
|
|
81
|
+
const d = new ToolDispatcher(toolRegistry, pm);
|
|
82
|
+
|
|
83
|
+
const tool = createMockTool('shell_exec', true);
|
|
84
|
+
toolRegistry.register('shell_exec', tool);
|
|
85
|
+
|
|
86
|
+
const toolCall: ToolCall = {
|
|
87
|
+
id: 'tc-1',
|
|
88
|
+
name: 'shell_exec',
|
|
89
|
+
arguments: '{"command":"ls"}',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await expect(d.dispatch(toolCall, context)).rejects.toThrow(PermissionDeniedError);
|
|
93
|
+
// Handler should receive parsed params
|
|
94
|
+
expect(handler).toHaveBeenCalledWith('shell_exec', { command: 'ls' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should dispatch all tool calls', async () => {
|
|
98
|
+
toolRegistry.register('tool_a', createMockTool('tool_a'));
|
|
99
|
+
toolRegistry.register('tool_b', createMockTool('tool_b'));
|
|
100
|
+
|
|
101
|
+
const toolCalls: ToolCall[] = [
|
|
102
|
+
{ id: 'tc-1', name: 'tool_a', arguments: '{}' },
|
|
103
|
+
{ id: 'tc-2', name: 'tool_b', arguments: '{}' },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const results = await dispatcher.dispatchAll(toolCalls, context);
|
|
107
|
+
expect(results.size).toBe(2);
|
|
108
|
+
expect(results.get('tc-1')?.success).toBe(true);
|
|
109
|
+
expect(results.get('tc-2')?.success).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should emit tool:start and tool:end events', async () => {
|
|
113
|
+
const tool = createMockTool('file_read');
|
|
114
|
+
toolRegistry.register('file_read', tool);
|
|
115
|
+
|
|
116
|
+
const startHandler = vi.fn();
|
|
117
|
+
const endHandler = vi.fn();
|
|
118
|
+
context.eventBus.on('tool:start', startHandler);
|
|
119
|
+
context.eventBus.on('tool:end', endHandler);
|
|
120
|
+
|
|
121
|
+
const toolCall: ToolCall = {
|
|
122
|
+
id: 'tc-1',
|
|
123
|
+
name: 'file_read',
|
|
124
|
+
arguments: '{}',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
await dispatcher.dispatch(toolCall, context);
|
|
128
|
+
expect(startHandler).toHaveBeenCalledOnce();
|
|
129
|
+
expect(endHandler).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should truncate oversized tool output', async () => {
|
|
133
|
+
const bigOutput = 'x'.repeat(100_000);
|
|
134
|
+
const tool: ITool = {
|
|
135
|
+
name: 'big_tool',
|
|
136
|
+
requiresPermission: false,
|
|
137
|
+
describe: () => ({ name: 'big_tool', description: 'test', parameters: [] }),
|
|
138
|
+
execute: vi.fn().mockResolvedValue({ success: true, output: bigOutput }),
|
|
139
|
+
};
|
|
140
|
+
toolRegistry.register('big_tool', tool);
|
|
141
|
+
|
|
142
|
+
const toolCall: ToolCall = { id: 'tc-1', name: 'big_tool', arguments: '{}' };
|
|
143
|
+
const result = await dispatcher.dispatch(toolCall, context);
|
|
144
|
+
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
expect(result.output.length).toBeLessThan(bigOutput.length);
|
|
147
|
+
expect(result.output).toContain('output truncated');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should not truncate output within limit', async () => {
|
|
151
|
+
const normalOutput = 'x'.repeat(1000);
|
|
152
|
+
const tool: ITool = {
|
|
153
|
+
name: 'small_tool',
|
|
154
|
+
requiresPermission: false,
|
|
155
|
+
describe: () => ({ name: 'small_tool', description: 'test', parameters: [] }),
|
|
156
|
+
execute: vi.fn().mockResolvedValue({ success: true, output: normalOutput }),
|
|
157
|
+
};
|
|
158
|
+
toolRegistry.register('small_tool', tool);
|
|
159
|
+
|
|
160
|
+
const toolCall: ToolCall = { id: 'tc-1', name: 'small_tool', arguments: '{}' };
|
|
161
|
+
const result = await dispatcher.dispatch(toolCall, context);
|
|
162
|
+
|
|
163
|
+
expect(result.output).toBe(normalOutput);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should abort remaining tools when context is aborted', async () => {
|
|
167
|
+
toolRegistry.register('tool_a', createMockTool('tool_a'));
|
|
168
|
+
toolRegistry.register('tool_b', createMockTool('tool_b'));
|
|
169
|
+
|
|
170
|
+
context.abort();
|
|
171
|
+
|
|
172
|
+
const toolCalls: ToolCall[] = [
|
|
173
|
+
{ id: 'tc-1', name: 'tool_a', arguments: '{}' },
|
|
174
|
+
{ id: 'tc-2', name: 'tool_b', arguments: '{}' },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const results = await dispatcher.dispatchAll(toolCalls, context);
|
|
178
|
+
expect(results.get('tc-1')?.error).toContain('aborted');
|
|
179
|
+
expect(results.get('tc-2')?.error).toContain('aborted');
|
|
180
|
+
});
|
|
181
|
+
});
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
'@charming_groot/core': resolve(__dirname, '../core/src/index.ts'),
|
|
8
|
+
'@charming_groot/providers': resolve(__dirname, '../providers/src/index.ts'),
|
|
9
|
+
'@charming_groot/tools': resolve(__dirname, '../tools/src/index.ts'),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
test: {
|
|
13
|
+
globals: true,
|
|
14
|
+
environment: 'node',
|
|
15
|
+
include: ['tests/**/*.test.ts'],
|
|
16
|
+
},
|
|
17
|
+
});
|