@mohanscodex/spectra-agent 0.4.6 → 0.4.9
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/__tests__/agent-edge-cases.test.d.ts +2 -0
- package/dist/__tests__/agent-edge-cases.test.d.ts.map +1 -0
- package/dist/__tests__/agent-edge-cases.test.js +302 -0
- package/dist/__tests__/agent-edge-cases.test.js.map +1 -0
- package/dist/__tests__/agent-features.test.js +42 -42
- package/dist/__tests__/agent-features.test.js.map +1 -1
- package/dist/__tests__/agent.test.js +43 -38
- package/dist/__tests__/agent.test.js.map +1 -1
- package/dist/__tests__/e2e.test.js +263 -282
- package/dist/__tests__/e2e.test.js.map +1 -1
- package/dist/agent.d.ts +5 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +139 -83
- package/dist/agent.js.map +1 -1
- package/dist/define-tool.d.ts +2 -2
- package/dist/define-tool.d.ts.map +1 -1
- package/dist/define-tool.js +4 -4
- package/dist/define-tool.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +23 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
2
|
-
import { Agent } from
|
|
3
|
-
import { defineTool } from
|
|
4
|
-
import { z } from
|
|
5
|
-
import { AssistantMessageEventStream, registerProvider } from
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Agent } from '../agent.js';
|
|
3
|
+
import { defineTool } from '../define-tool.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { AssistantMessageEventStream, registerProvider } from '@mohanscodex/spectra-ai';
|
|
6
6
|
// Test model
|
|
7
7
|
const testModel = {
|
|
8
|
-
id:
|
|
9
|
-
name:
|
|
10
|
-
provider:
|
|
11
|
-
api:
|
|
8
|
+
id: 'claude-sonnet-4-20250514',
|
|
9
|
+
name: 'Claude Sonnet 4',
|
|
10
|
+
provider: 'test-provider',
|
|
11
|
+
api: 'test',
|
|
12
12
|
};
|
|
13
13
|
// Helper to create a mock provider that returns specific events
|
|
14
14
|
function createMockProvider(name, responseSequence) {
|
|
@@ -21,35 +21,35 @@ function createMockProvider(name, responseSequence) {
|
|
|
21
21
|
callIndex++;
|
|
22
22
|
setTimeout(() => {
|
|
23
23
|
const partial = {
|
|
24
|
-
role:
|
|
24
|
+
role: 'assistant',
|
|
25
25
|
content: [],
|
|
26
26
|
provider: model.provider,
|
|
27
27
|
model: model.id,
|
|
28
28
|
usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 },
|
|
29
|
-
stopReason:
|
|
29
|
+
stopReason: 'stop',
|
|
30
30
|
timestamp: Date.now(),
|
|
31
31
|
};
|
|
32
|
-
stream.push({ type:
|
|
32
|
+
stream.push({ type: 'start', partial });
|
|
33
33
|
// Stream text content
|
|
34
34
|
for (let i = 0; i < responses.length; i++) {
|
|
35
35
|
const msg = responses[i];
|
|
36
36
|
for (const block of msg.content) {
|
|
37
|
-
if (block.type ===
|
|
37
|
+
if (block.type === 'text') {
|
|
38
38
|
stream.push({
|
|
39
|
-
type:
|
|
39
|
+
type: 'text_delta',
|
|
40
40
|
contentIndex: i,
|
|
41
41
|
delta: block.text,
|
|
42
42
|
partial: { ...partial, content: [block] },
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
else if (block.type ===
|
|
45
|
+
else if (block.type === 'toolCall') {
|
|
46
46
|
stream.push({
|
|
47
|
-
type:
|
|
47
|
+
type: 'toolcall_start',
|
|
48
48
|
contentIndex: i,
|
|
49
49
|
partial: { ...partial, content: [block] },
|
|
50
50
|
});
|
|
51
51
|
stream.push({
|
|
52
|
-
type:
|
|
52
|
+
type: 'toolcall_end',
|
|
53
53
|
contentIndex: i,
|
|
54
54
|
toolCall: block,
|
|
55
55
|
partial: { ...partial, content: [block] },
|
|
@@ -60,7 +60,7 @@ function createMockProvider(name, responseSequence) {
|
|
|
60
60
|
// Use the last response's stop reason
|
|
61
61
|
const lastResponse = responses[responses.length - 1] || partial;
|
|
62
62
|
stream.push({
|
|
63
|
-
type:
|
|
63
|
+
type: 'done',
|
|
64
64
|
reason: lastResponse.stopReason,
|
|
65
65
|
message: lastResponse,
|
|
66
66
|
});
|
|
@@ -71,12 +71,12 @@ function createMockProvider(name, responseSequence) {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
// Helper to create text-only assistant message
|
|
74
|
-
function createTextMessage(text, stopReason =
|
|
74
|
+
function createTextMessage(text, stopReason = 'stop') {
|
|
75
75
|
return {
|
|
76
|
-
role:
|
|
77
|
-
content: [{ type:
|
|
78
|
-
provider:
|
|
79
|
-
model:
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
content: [{ type: 'text', text }],
|
|
78
|
+
provider: 'test-provider',
|
|
79
|
+
model: 'test-model',
|
|
80
80
|
usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 },
|
|
81
81
|
stopReason,
|
|
82
82
|
timestamp: Date.now(),
|
|
@@ -85,109 +85,102 @@ function createTextMessage(text, stopReason = "stop") {
|
|
|
85
85
|
// Helper to create tool call message
|
|
86
86
|
function createToolCallMessage(toolCalls) {
|
|
87
87
|
return {
|
|
88
|
-
role:
|
|
88
|
+
role: 'assistant',
|
|
89
89
|
content: toolCalls,
|
|
90
|
-
provider:
|
|
91
|
-
model:
|
|
90
|
+
provider: 'test-provider',
|
|
91
|
+
model: 'test-model',
|
|
92
92
|
usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 },
|
|
93
|
-
stopReason:
|
|
93
|
+
stopReason: 'toolUse',
|
|
94
94
|
timestamp: Date.now(),
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
describe(
|
|
97
|
+
describe('Agent E2E - Basic Conversation', () => {
|
|
98
98
|
beforeEach(() => {
|
|
99
99
|
// Clear and re-register mock provider
|
|
100
100
|
});
|
|
101
|
-
it(
|
|
102
|
-
const mockProvider = createMockProvider(
|
|
103
|
-
[createTextMessage("Hello! How can I help you?")],
|
|
104
|
-
]);
|
|
101
|
+
it('should run simple conversation without tools', async () => {
|
|
102
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Hello! How can I help you?')]]);
|
|
105
103
|
registerProvider(mockProvider);
|
|
106
104
|
const agent = new Agent({
|
|
107
105
|
model: testModel,
|
|
108
|
-
systemPrompt:
|
|
106
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
109
107
|
});
|
|
110
108
|
const events = [];
|
|
111
|
-
for await (const event of agent.run(
|
|
109
|
+
for await (const event of agent.run('Hi!')) {
|
|
112
110
|
events.push(event);
|
|
113
111
|
}
|
|
114
112
|
// Should have agent_start, message_start, message_end, turn_start, message_start, message_update(s), message_end, turn_end, agent_end
|
|
115
113
|
expect(events.length).toBeGreaterThan(0);
|
|
116
114
|
const eventTypes = events.map((e) => e.type);
|
|
117
|
-
expect(eventTypes).toContain(
|
|
118
|
-
expect(eventTypes).toContain(
|
|
119
|
-
expect(eventTypes).toContain(
|
|
120
|
-
expect(eventTypes).toContain(
|
|
115
|
+
expect(eventTypes).toContain('agent_start');
|
|
116
|
+
expect(eventTypes).toContain('agent_end');
|
|
117
|
+
expect(eventTypes).toContain('turn_start');
|
|
118
|
+
expect(eventTypes).toContain('turn_end');
|
|
121
119
|
});
|
|
122
|
-
it(
|
|
123
|
-
const responses = [
|
|
124
|
-
|
|
125
|
-
[createTextMessage("Second response")],
|
|
126
|
-
];
|
|
127
|
-
const mockProvider = createMockProvider("test-provider", responses);
|
|
120
|
+
it('should maintain message history across turns', async () => {
|
|
121
|
+
const responses = [[createTextMessage('First response')], [createTextMessage('Second response')]];
|
|
122
|
+
const mockProvider = createMockProvider('test-provider', responses);
|
|
128
123
|
registerProvider(mockProvider);
|
|
129
124
|
const agent = new Agent({
|
|
130
125
|
model: testModel,
|
|
131
126
|
});
|
|
132
127
|
// First run
|
|
133
|
-
for await (const _ of agent.run(
|
|
128
|
+
for await (const _ of agent.run('Message 1')) {
|
|
134
129
|
// consume
|
|
135
130
|
}
|
|
136
131
|
const firstHistoryLength = agent.messages.length;
|
|
137
132
|
// Second run
|
|
138
|
-
for await (const _ of agent.run(
|
|
133
|
+
for await (const _ of agent.run('Message 2')) {
|
|
139
134
|
// consume
|
|
140
135
|
}
|
|
141
136
|
const secondHistoryLength = agent.messages.length;
|
|
142
137
|
// History should accumulate
|
|
143
138
|
expect(secondHistoryLength).toBeGreaterThan(firstHistoryLength);
|
|
144
139
|
});
|
|
145
|
-
it(
|
|
146
|
-
const mockProvider = createMockProvider(
|
|
147
|
-
[createTextMessage("Test response")],
|
|
148
|
-
]);
|
|
140
|
+
it('should emit message events with correct structure', async () => {
|
|
141
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Test response')]]);
|
|
149
142
|
registerProvider(mockProvider);
|
|
150
143
|
const agent = new Agent({
|
|
151
144
|
model: testModel,
|
|
152
145
|
});
|
|
153
146
|
const events = [];
|
|
154
|
-
for await (const event of agent.run(
|
|
147
|
+
for await (const event of agent.run('Test')) {
|
|
155
148
|
events.push(event);
|
|
156
149
|
}
|
|
157
|
-
const messageStart = events.find((e) => e.type ===
|
|
158
|
-
const messageEnd = events.find((e) => e.type ===
|
|
150
|
+
const messageStart = events.find((e) => e.type === 'message_start');
|
|
151
|
+
const messageEnd = events.find((e) => e.type === 'message_end');
|
|
159
152
|
expect(messageStart).toBeDefined();
|
|
160
153
|
expect(messageEnd).toBeDefined();
|
|
161
154
|
expect(messageStart.message.role).toBeDefined();
|
|
162
155
|
expect(messageEnd.message.role).toBeDefined();
|
|
163
156
|
});
|
|
164
157
|
});
|
|
165
|
-
describe(
|
|
166
|
-
it(
|
|
158
|
+
describe('Agent E2E - Tool Execution', () => {
|
|
159
|
+
it('should execute single tool call', async () => {
|
|
167
160
|
const tool = defineTool({
|
|
168
|
-
name:
|
|
169
|
-
description:
|
|
161
|
+
name: 'get_weather',
|
|
162
|
+
description: 'Get weather information',
|
|
170
163
|
parameters: z.object({
|
|
171
|
-
location: z.string().describe(
|
|
164
|
+
location: z.string().describe('The location'),
|
|
172
165
|
}),
|
|
173
166
|
execute: async (args) => {
|
|
174
167
|
return {
|
|
175
|
-
content: [{ type:
|
|
168
|
+
content: [{ type: 'text', text: `Weather in ${args.location}: Sunny` }],
|
|
176
169
|
};
|
|
177
170
|
},
|
|
178
171
|
});
|
|
179
|
-
const mockProvider = createMockProvider(
|
|
172
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
180
173
|
[
|
|
181
174
|
createToolCallMessage([
|
|
182
175
|
{
|
|
183
|
-
type:
|
|
184
|
-
id:
|
|
185
|
-
name:
|
|
186
|
-
arguments: { location:
|
|
176
|
+
type: 'toolCall',
|
|
177
|
+
id: 'call_1',
|
|
178
|
+
name: 'get_weather',
|
|
179
|
+
arguments: { location: 'NYC' },
|
|
187
180
|
},
|
|
188
181
|
]),
|
|
189
182
|
],
|
|
190
|
-
[createTextMessage(
|
|
183
|
+
[createTextMessage('The weather in NYC is Sunny')],
|
|
191
184
|
]);
|
|
192
185
|
registerProvider(mockProvider);
|
|
193
186
|
const agent = new Agent({
|
|
@@ -198,86 +191,86 @@ describe("Agent E2E - Tool Execution", () => {
|
|
|
198
191
|
for await (const event of agent.run("What's the weather?")) {
|
|
199
192
|
events.push(event);
|
|
200
193
|
}
|
|
201
|
-
const toolStart = events.find((e) => e.type ===
|
|
202
|
-
const toolEnd = events.find((e) => e.type ===
|
|
194
|
+
const toolStart = events.find((e) => e.type === 'tool_execution_start');
|
|
195
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
203
196
|
expect(toolStart).toBeDefined();
|
|
204
197
|
expect(toolEnd).toBeDefined();
|
|
205
|
-
expect(toolStart.toolName).toBe(
|
|
206
|
-
expect(toolEnd.result.content[0].text).toBe(
|
|
198
|
+
expect(toolStart.toolName).toBe('get_weather');
|
|
199
|
+
expect(toolEnd.result.content[0].text).toBe('Weather in NYC: Sunny');
|
|
207
200
|
expect(toolEnd.isError).toBe(false);
|
|
208
201
|
});
|
|
209
|
-
it(
|
|
202
|
+
it('should execute multiple tools in parallel', async () => {
|
|
210
203
|
const tool1 = defineTool({
|
|
211
|
-
name:
|
|
212
|
-
description:
|
|
204
|
+
name: 'get_weather',
|
|
205
|
+
description: 'Get weather',
|
|
213
206
|
parameters: z.object({ location: z.string() }),
|
|
214
207
|
execute: async (args) => ({
|
|
215
|
-
content: [{ type:
|
|
208
|
+
content: [{ type: 'text', text: `Weather: ${args.location}` }],
|
|
216
209
|
}),
|
|
217
210
|
});
|
|
218
211
|
const tool2 = defineTool({
|
|
219
|
-
name:
|
|
220
|
-
description:
|
|
212
|
+
name: 'get_time',
|
|
213
|
+
description: 'Get time',
|
|
221
214
|
parameters: z.object({ timezone: z.string() }),
|
|
222
215
|
execute: async (args) => ({
|
|
223
|
-
content: [{ type:
|
|
216
|
+
content: [{ type: 'text', text: `Time: ${args.timezone}` }],
|
|
224
217
|
}),
|
|
225
218
|
});
|
|
226
|
-
const mockProvider = createMockProvider(
|
|
219
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
227
220
|
[
|
|
228
221
|
createToolCallMessage([
|
|
229
222
|
{
|
|
230
|
-
type:
|
|
231
|
-
id:
|
|
232
|
-
name:
|
|
233
|
-
arguments: { location:
|
|
223
|
+
type: 'toolCall',
|
|
224
|
+
id: 'call_1',
|
|
225
|
+
name: 'get_weather',
|
|
226
|
+
arguments: { location: 'NYC' },
|
|
234
227
|
},
|
|
235
228
|
{
|
|
236
|
-
type:
|
|
237
|
-
id:
|
|
238
|
-
name:
|
|
239
|
-
arguments: { timezone:
|
|
229
|
+
type: 'toolCall',
|
|
230
|
+
id: 'call_2',
|
|
231
|
+
name: 'get_time',
|
|
232
|
+
arguments: { timezone: 'EST' },
|
|
240
233
|
},
|
|
241
234
|
]),
|
|
242
235
|
],
|
|
243
|
-
[createTextMessage(
|
|
236
|
+
[createTextMessage('Done')],
|
|
244
237
|
]);
|
|
245
238
|
registerProvider(mockProvider);
|
|
246
239
|
const agent = new Agent({
|
|
247
240
|
model: testModel,
|
|
248
241
|
tools: [tool1, tool2],
|
|
249
|
-
toolExecution:
|
|
242
|
+
toolExecution: 'parallel',
|
|
250
243
|
});
|
|
251
244
|
const events = [];
|
|
252
|
-
for await (const event of agent.run(
|
|
245
|
+
for await (const event of agent.run('Get info')) {
|
|
253
246
|
events.push(event);
|
|
254
247
|
}
|
|
255
|
-
const toolStarts = events.filter((e) => e.type ===
|
|
256
|
-
const toolEnds = events.filter((e) => e.type ===
|
|
248
|
+
const toolStarts = events.filter((e) => e.type === 'tool_execution_start');
|
|
249
|
+
const toolEnds = events.filter((e) => e.type === 'tool_execution_end');
|
|
257
250
|
expect(toolStarts).toHaveLength(2);
|
|
258
251
|
expect(toolEnds).toHaveLength(2);
|
|
259
252
|
});
|
|
260
|
-
it(
|
|
253
|
+
it('should handle tool execution errors', async () => {
|
|
261
254
|
const failingTool = defineTool({
|
|
262
|
-
name:
|
|
263
|
-
description:
|
|
255
|
+
name: 'fail_tool',
|
|
256
|
+
description: 'Always fails',
|
|
264
257
|
parameters: z.object({}),
|
|
265
258
|
execute: async () => {
|
|
266
|
-
throw new Error(
|
|
259
|
+
throw new Error('Tool execution failed');
|
|
267
260
|
},
|
|
268
261
|
});
|
|
269
|
-
const mockProvider = createMockProvider(
|
|
262
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
270
263
|
[
|
|
271
264
|
createToolCallMessage([
|
|
272
265
|
{
|
|
273
|
-
type:
|
|
274
|
-
id:
|
|
275
|
-
name:
|
|
266
|
+
type: 'toolCall',
|
|
267
|
+
id: 'call_1',
|
|
268
|
+
name: 'fail_tool',
|
|
276
269
|
arguments: {},
|
|
277
270
|
},
|
|
278
271
|
]),
|
|
279
272
|
],
|
|
280
|
-
[createTextMessage(
|
|
273
|
+
[createTextMessage('Sorry, the tool failed.')],
|
|
281
274
|
]);
|
|
282
275
|
registerProvider(mockProvider);
|
|
283
276
|
const agent = new Agent({
|
|
@@ -285,22 +278,22 @@ describe("Agent E2E - Tool Execution", () => {
|
|
|
285
278
|
tools: [failingTool],
|
|
286
279
|
});
|
|
287
280
|
const events = [];
|
|
288
|
-
for await (const event of agent.run(
|
|
281
|
+
for await (const event of agent.run('Use failing tool')) {
|
|
289
282
|
events.push(event);
|
|
290
283
|
}
|
|
291
|
-
const toolEnd = events.find((e) => e.type ===
|
|
284
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
292
285
|
expect(toolEnd).toBeDefined();
|
|
293
286
|
expect(toolEnd.isError).toBe(true);
|
|
294
|
-
expect(toolEnd.result.content[0].text).toContain(
|
|
287
|
+
expect(toolEnd.result.content[0].text).toContain('Tool execution failed');
|
|
295
288
|
});
|
|
296
|
-
it(
|
|
297
|
-
const mockProvider = createMockProvider(
|
|
289
|
+
it('should handle unknown tool calls', async () => {
|
|
290
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
298
291
|
[
|
|
299
292
|
createToolCallMessage([
|
|
300
293
|
{
|
|
301
|
-
type:
|
|
302
|
-
id:
|
|
303
|
-
name:
|
|
294
|
+
type: 'toolCall',
|
|
295
|
+
id: 'call_1',
|
|
296
|
+
name: 'unknown_tool',
|
|
304
297
|
arguments: {},
|
|
305
298
|
},
|
|
306
299
|
]),
|
|
@@ -313,154 +306,152 @@ describe("Agent E2E - Tool Execution", () => {
|
|
|
313
306
|
tools: [], // No tools registered
|
|
314
307
|
});
|
|
315
308
|
const events = [];
|
|
316
|
-
for await (const event of agent.run(
|
|
309
|
+
for await (const event of agent.run('Call unknown tool')) {
|
|
317
310
|
events.push(event);
|
|
318
311
|
}
|
|
319
|
-
const toolEnd = events.find((e) => e.type ===
|
|
312
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
320
313
|
expect(toolEnd).toBeDefined();
|
|
321
314
|
expect(toolEnd.isError).toBe(true);
|
|
322
315
|
expect(toolEnd.result.content[0].text).toContain('Unknown tool "unknown_tool"');
|
|
323
316
|
});
|
|
324
317
|
});
|
|
325
|
-
describe(
|
|
326
|
-
it(
|
|
318
|
+
describe('Agent E2E - Advanced Features', () => {
|
|
319
|
+
it('should support beforeToolCall hook', async () => {
|
|
327
320
|
const tool = defineTool({
|
|
328
|
-
name:
|
|
329
|
-
description:
|
|
321
|
+
name: 'sensitive_tool',
|
|
322
|
+
description: 'Sensitive operation',
|
|
330
323
|
parameters: z.object({}),
|
|
331
324
|
execute: async () => ({
|
|
332
|
-
content: [{ type:
|
|
325
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
333
326
|
}),
|
|
334
327
|
});
|
|
335
|
-
const mockProvider = createMockProvider(
|
|
328
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
336
329
|
[
|
|
337
330
|
createToolCallMessage([
|
|
338
331
|
{
|
|
339
|
-
type:
|
|
340
|
-
id:
|
|
341
|
-
name:
|
|
332
|
+
type: 'toolCall',
|
|
333
|
+
id: 'call_1',
|
|
334
|
+
name: 'sensitive_tool',
|
|
342
335
|
arguments: {},
|
|
343
336
|
},
|
|
344
337
|
]),
|
|
345
338
|
],
|
|
346
|
-
[createTextMessage(
|
|
339
|
+
[createTextMessage('Blocked')],
|
|
347
340
|
]);
|
|
348
341
|
registerProvider(mockProvider);
|
|
349
|
-
const beforeHook = vi.fn().mockResolvedValue({ block: true, reason:
|
|
342
|
+
const beforeHook = vi.fn().mockResolvedValue({ block: true, reason: 'Not allowed' });
|
|
350
343
|
const agent = new Agent({
|
|
351
344
|
model: testModel,
|
|
352
345
|
tools: [tool],
|
|
353
346
|
beforeToolCall: beforeHook,
|
|
354
347
|
});
|
|
355
|
-
for await (const _ of agent.run(
|
|
348
|
+
for await (const _ of agent.run('Use sensitive tool')) {
|
|
356
349
|
// consume
|
|
357
350
|
}
|
|
358
351
|
expect(beforeHook).toHaveBeenCalled();
|
|
359
352
|
// The tool should have been blocked
|
|
360
|
-
const toolEnd = agent.messages.find((m) => m.role ===
|
|
353
|
+
const toolEnd = agent.messages.find((m) => m.role === 'toolResult' && m.toolName === 'sensitive_tool');
|
|
361
354
|
expect(toolEnd).toBeDefined();
|
|
362
|
-
if (toolEnd?.role ===
|
|
355
|
+
if (toolEnd?.role === 'toolResult') {
|
|
363
356
|
expect(toolEnd.isError).toBe(true);
|
|
364
357
|
}
|
|
365
358
|
});
|
|
366
|
-
it(
|
|
359
|
+
it('should support afterToolCall hook', async () => {
|
|
367
360
|
const tool = defineTool({
|
|
368
|
-
name:
|
|
369
|
-
description:
|
|
361
|
+
name: 'data_tool',
|
|
362
|
+
description: 'Get data',
|
|
370
363
|
parameters: z.object({}),
|
|
371
364
|
execute: async () => ({
|
|
372
|
-
content: [{ type:
|
|
365
|
+
content: [{ type: 'text', text: 'Raw data' }],
|
|
373
366
|
}),
|
|
374
367
|
});
|
|
375
|
-
const mockProvider = createMockProvider(
|
|
368
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
376
369
|
[
|
|
377
370
|
createToolCallMessage([
|
|
378
371
|
{
|
|
379
|
-
type:
|
|
380
|
-
id:
|
|
381
|
-
name:
|
|
372
|
+
type: 'toolCall',
|
|
373
|
+
id: 'call_1',
|
|
374
|
+
name: 'data_tool',
|
|
382
375
|
arguments: {},
|
|
383
376
|
},
|
|
384
377
|
]),
|
|
385
378
|
],
|
|
386
|
-
[createTextMessage(
|
|
379
|
+
[createTextMessage('Processed')],
|
|
387
380
|
]);
|
|
388
381
|
registerProvider(mockProvider);
|
|
389
382
|
const afterHook = vi.fn().mockResolvedValue({
|
|
390
|
-
content: [{ type:
|
|
383
|
+
content: [{ type: 'text', text: 'Modified data' }],
|
|
391
384
|
});
|
|
392
385
|
const agent = new Agent({
|
|
393
386
|
model: testModel,
|
|
394
387
|
tools: [tool],
|
|
395
388
|
afterToolCall: afterHook,
|
|
396
389
|
});
|
|
397
|
-
for await (const _ of agent.run(
|
|
390
|
+
for await (const _ of agent.run('Get data')) {
|
|
398
391
|
// consume
|
|
399
392
|
}
|
|
400
393
|
expect(afterHook).toHaveBeenCalled();
|
|
401
394
|
});
|
|
402
|
-
it(
|
|
395
|
+
it('should support sequential tool execution', async () => {
|
|
403
396
|
const executionOrder = [];
|
|
404
397
|
const tool1 = defineTool({
|
|
405
|
-
name:
|
|
406
|
-
description:
|
|
398
|
+
name: 'tool_a',
|
|
399
|
+
description: 'Tool A',
|
|
407
400
|
parameters: z.object({}),
|
|
408
401
|
execute: async () => {
|
|
409
|
-
executionOrder.push(
|
|
402
|
+
executionOrder.push('A');
|
|
410
403
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
411
|
-
return { content: [{ type:
|
|
404
|
+
return { content: [{ type: 'text', text: 'A' }] };
|
|
412
405
|
},
|
|
413
406
|
});
|
|
414
407
|
const tool2 = defineTool({
|
|
415
|
-
name:
|
|
416
|
-
description:
|
|
408
|
+
name: 'tool_b',
|
|
409
|
+
description: 'Tool B',
|
|
417
410
|
parameters: z.object({}),
|
|
418
411
|
execute: async () => {
|
|
419
|
-
executionOrder.push(
|
|
420
|
-
return { content: [{ type:
|
|
412
|
+
executionOrder.push('B');
|
|
413
|
+
return { content: [{ type: 'text', text: 'B' }] };
|
|
421
414
|
},
|
|
422
415
|
});
|
|
423
|
-
const mockProvider = createMockProvider(
|
|
416
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
424
417
|
[
|
|
425
418
|
createToolCallMessage([
|
|
426
419
|
{
|
|
427
|
-
type:
|
|
428
|
-
id:
|
|
429
|
-
name:
|
|
420
|
+
type: 'toolCall',
|
|
421
|
+
id: 'call_1',
|
|
422
|
+
name: 'tool_a',
|
|
430
423
|
arguments: {},
|
|
431
424
|
},
|
|
432
425
|
{
|
|
433
|
-
type:
|
|
434
|
-
id:
|
|
435
|
-
name:
|
|
426
|
+
type: 'toolCall',
|
|
427
|
+
id: 'call_2',
|
|
428
|
+
name: 'tool_b',
|
|
436
429
|
arguments: {},
|
|
437
430
|
},
|
|
438
431
|
]),
|
|
439
432
|
],
|
|
440
|
-
[createTextMessage(
|
|
433
|
+
[createTextMessage('Done')],
|
|
441
434
|
]);
|
|
442
435
|
registerProvider(mockProvider);
|
|
443
436
|
const agent = new Agent({
|
|
444
437
|
model: testModel,
|
|
445
438
|
tools: [tool1, tool2],
|
|
446
|
-
toolExecution:
|
|
439
|
+
toolExecution: 'sequential',
|
|
447
440
|
});
|
|
448
|
-
for await (const _ of agent.run(
|
|
441
|
+
for await (const _ of agent.run('Sequential test')) {
|
|
449
442
|
// consume
|
|
450
443
|
}
|
|
451
444
|
// In sequential mode, B should execute after A completes
|
|
452
|
-
expect(executionOrder).toEqual([
|
|
445
|
+
expect(executionOrder).toEqual(['A', 'B']);
|
|
453
446
|
});
|
|
454
|
-
it(
|
|
455
|
-
const mockProvider = createMockProvider(
|
|
456
|
-
[createTextMessage("Slow response")],
|
|
457
|
-
]);
|
|
447
|
+
it('should handle abort signal', async () => {
|
|
448
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Slow response')]]);
|
|
458
449
|
registerProvider(mockProvider);
|
|
459
450
|
const agent = new Agent({
|
|
460
451
|
model: testModel,
|
|
461
452
|
});
|
|
462
453
|
// Start the agent
|
|
463
|
-
const generator = agent.run(
|
|
454
|
+
const generator = agent.run('Test abort');
|
|
464
455
|
// Get first event
|
|
465
456
|
const firstEvent = await generator.next();
|
|
466
457
|
expect(firstEvent.done).toBe(false);
|
|
@@ -474,35 +465,31 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
474
465
|
// Should have completed (not hang)
|
|
475
466
|
expect(agent.isStreaming).toBe(false);
|
|
476
467
|
});
|
|
477
|
-
it(
|
|
478
|
-
const mockProvider = createMockProvider(
|
|
479
|
-
[createTextMessage("Response")],
|
|
480
|
-
]);
|
|
468
|
+
it('should not hang when transformContext hook throws unexpectedly', async () => {
|
|
469
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Response')]]);
|
|
481
470
|
registerProvider(mockProvider);
|
|
482
471
|
const agent = new Agent({
|
|
483
472
|
model: testModel,
|
|
484
473
|
transformContext: () => {
|
|
485
|
-
throw new Error(
|
|
474
|
+
throw new Error('Unexpected transformation failure');
|
|
486
475
|
},
|
|
487
476
|
});
|
|
488
477
|
const events = [];
|
|
489
|
-
for await (const event of agent.run(
|
|
478
|
+
for await (const event of agent.run('Test')) {
|
|
490
479
|
events.push(event);
|
|
491
480
|
}
|
|
492
481
|
// Must have completed without hanging
|
|
493
|
-
const agentEnd = events.find((e) => e.type ===
|
|
482
|
+
const agentEnd = events.find((e) => e.type === 'agent_end');
|
|
494
483
|
expect(agentEnd).toBeDefined();
|
|
495
484
|
expect(agent.isStreaming).toBe(false);
|
|
496
485
|
});
|
|
497
|
-
it(
|
|
498
|
-
const mockProvider = createMockProvider(
|
|
499
|
-
[createTextMessage("Response")],
|
|
500
|
-
]);
|
|
486
|
+
it('should reset state correctly', async () => {
|
|
487
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Response')]]);
|
|
501
488
|
registerProvider(mockProvider);
|
|
502
489
|
const agent = new Agent({
|
|
503
490
|
model: testModel,
|
|
504
491
|
});
|
|
505
|
-
for await (const _ of agent.run(
|
|
492
|
+
for await (const _ of agent.run('Test')) {
|
|
506
493
|
// consume
|
|
507
494
|
}
|
|
508
495
|
expect(agent.messages.length).toBeGreaterThan(0);
|
|
@@ -512,10 +499,8 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
512
499
|
expect(agent.streamingMessage).toBeUndefined();
|
|
513
500
|
expect(agent.pendingToolCalls.size).toBe(0);
|
|
514
501
|
});
|
|
515
|
-
it(
|
|
516
|
-
const mockProvider = createMockProvider(
|
|
517
|
-
[createTextMessage("Hello")],
|
|
518
|
-
]);
|
|
502
|
+
it('should emit agent events to subscribers', async () => {
|
|
503
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Hello')]]);
|
|
519
504
|
registerProvider(mockProvider);
|
|
520
505
|
const agent = new Agent({
|
|
521
506
|
model: testModel,
|
|
@@ -524,152 +509,150 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
524
509
|
const unsubscribe = agent.subscribe((event) => {
|
|
525
510
|
subscriberEvents.push(event);
|
|
526
511
|
});
|
|
527
|
-
for await (const _ of agent.run(
|
|
512
|
+
for await (const _ of agent.run('Test')) {
|
|
528
513
|
// consume
|
|
529
514
|
}
|
|
530
515
|
expect(subscriberEvents.length).toBeGreaterThan(0);
|
|
531
|
-
expect(subscriberEvents.some((e) => e.type ===
|
|
532
|
-
expect(subscriberEvents.some((e) => e.type ===
|
|
516
|
+
expect(subscriberEvents.some((e) => e.type === 'agent_start')).toBe(true);
|
|
517
|
+
expect(subscriberEvents.some((e) => e.type === 'agent_end')).toBe(true);
|
|
533
518
|
unsubscribe();
|
|
534
519
|
});
|
|
535
|
-
it(
|
|
520
|
+
it('should handle beforeToolCall hook that throws without hanging', async () => {
|
|
536
521
|
const tool = defineTool({
|
|
537
|
-
name:
|
|
538
|
-
description:
|
|
522
|
+
name: 'normal_tool',
|
|
523
|
+
description: 'A normal tool',
|
|
539
524
|
parameters: z.object({}),
|
|
540
525
|
execute: async () => ({
|
|
541
|
-
content: [{ type:
|
|
526
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
542
527
|
}),
|
|
543
528
|
});
|
|
544
|
-
const mockProvider = createMockProvider(
|
|
529
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
545
530
|
[
|
|
546
531
|
createToolCallMessage([
|
|
547
532
|
{
|
|
548
|
-
type:
|
|
549
|
-
id:
|
|
550
|
-
name:
|
|
533
|
+
type: 'toolCall',
|
|
534
|
+
id: 'call_1',
|
|
535
|
+
name: 'normal_tool',
|
|
551
536
|
arguments: {},
|
|
552
537
|
},
|
|
553
538
|
]),
|
|
554
539
|
],
|
|
555
|
-
[createTextMessage(
|
|
540
|
+
[createTextMessage('Recovered')],
|
|
556
541
|
]);
|
|
557
542
|
registerProvider(mockProvider);
|
|
558
543
|
const agent = new Agent({
|
|
559
544
|
model: testModel,
|
|
560
545
|
tools: [tool],
|
|
561
546
|
beforeToolCall: () => {
|
|
562
|
-
throw new Error(
|
|
547
|
+
throw new Error('Hook crash!');
|
|
563
548
|
},
|
|
564
549
|
});
|
|
565
550
|
const events = [];
|
|
566
|
-
for await (const event of agent.run(
|
|
551
|
+
for await (const event of agent.run('Test')) {
|
|
567
552
|
events.push(event);
|
|
568
553
|
}
|
|
569
554
|
// Must finish without hanging
|
|
570
|
-
const agentEnd = events.find((e) => e.type ===
|
|
555
|
+
const agentEnd = events.find((e) => e.type === 'agent_end');
|
|
571
556
|
expect(agentEnd).toBeDefined();
|
|
572
557
|
// The tool should be marked as blocked with the hook error
|
|
573
|
-
const toolEnd = events.find((e) => e.type ===
|
|
558
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
574
559
|
expect(toolEnd).toBeDefined();
|
|
575
560
|
expect(toolEnd.isError).toBe(true);
|
|
576
561
|
});
|
|
577
|
-
it(
|
|
562
|
+
it('should handle afterToolCall hook that throws without hanging', async () => {
|
|
578
563
|
const tool = defineTool({
|
|
579
|
-
name:
|
|
580
|
-
description:
|
|
564
|
+
name: 'normal_tool',
|
|
565
|
+
description: 'A normal tool',
|
|
581
566
|
parameters: z.object({}),
|
|
582
567
|
execute: async () => ({
|
|
583
|
-
content: [{ type:
|
|
568
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
584
569
|
}),
|
|
585
570
|
});
|
|
586
|
-
const mockProvider = createMockProvider(
|
|
571
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
587
572
|
[
|
|
588
573
|
createToolCallMessage([
|
|
589
574
|
{
|
|
590
|
-
type:
|
|
591
|
-
id:
|
|
592
|
-
name:
|
|
575
|
+
type: 'toolCall',
|
|
576
|
+
id: 'call_1',
|
|
577
|
+
name: 'normal_tool',
|
|
593
578
|
arguments: {},
|
|
594
579
|
},
|
|
595
580
|
]),
|
|
596
581
|
],
|
|
597
|
-
[createTextMessage(
|
|
582
|
+
[createTextMessage('Recovered')],
|
|
598
583
|
]);
|
|
599
584
|
registerProvider(mockProvider);
|
|
600
585
|
const agent = new Agent({
|
|
601
586
|
model: testModel,
|
|
602
587
|
tools: [tool],
|
|
603
588
|
afterToolCall: () => {
|
|
604
|
-
throw new Error(
|
|
589
|
+
throw new Error('Hook crash!');
|
|
605
590
|
},
|
|
606
591
|
});
|
|
607
592
|
const events = [];
|
|
608
|
-
for await (const event of agent.run(
|
|
593
|
+
for await (const event of agent.run('Test')) {
|
|
609
594
|
events.push(event);
|
|
610
595
|
}
|
|
611
596
|
// Must finish without hanging
|
|
612
|
-
const agentEnd = events.find((e) => e.type ===
|
|
597
|
+
const agentEnd = events.find((e) => e.type === 'agent_end');
|
|
613
598
|
expect(agentEnd).toBeDefined();
|
|
614
599
|
// The tool result should still be the original (not overridden by failed hook)
|
|
615
|
-
const toolEnd = events.find((e) => e.type ===
|
|
600
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
616
601
|
expect(toolEnd).toBeDefined();
|
|
617
602
|
expect(toolEnd.isError).toBe(false);
|
|
618
|
-
expect(toolEnd.result.content[0].text).toBe(
|
|
603
|
+
expect(toolEnd.result.content[0].text).toBe('Done');
|
|
619
604
|
});
|
|
620
|
-
it(
|
|
621
|
-
const mockProvider = createMockProvider(
|
|
622
|
-
[createTextMessage("Hello")],
|
|
623
|
-
]);
|
|
605
|
+
it('should isolate listener errors so one failing subscriber does not break others', async () => {
|
|
606
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Hello')]]);
|
|
624
607
|
registerProvider(mockProvider);
|
|
625
608
|
const agent = new Agent({
|
|
626
609
|
model: testModel,
|
|
627
610
|
});
|
|
628
611
|
const goodEvents = [];
|
|
629
612
|
const badListener = vi.fn().mockImplementation(() => {
|
|
630
|
-
throw new Error(
|
|
613
|
+
throw new Error('Listener crash!');
|
|
631
614
|
});
|
|
632
615
|
const goodListener = vi.fn().mockImplementation((event) => {
|
|
633
616
|
goodEvents.push(event);
|
|
634
617
|
});
|
|
635
618
|
agent.subscribe(badListener);
|
|
636
619
|
agent.subscribe(goodListener);
|
|
637
|
-
for await (const _ of agent.run(
|
|
620
|
+
for await (const _ of agent.run('Test')) {
|
|
638
621
|
// consume
|
|
639
622
|
}
|
|
640
623
|
// Bad listener should have been called and thrown
|
|
641
624
|
expect(badListener).toHaveBeenCalled();
|
|
642
625
|
// Good listener should still have received events despite bad listener crashing
|
|
643
626
|
expect(goodListener).toHaveBeenCalled();
|
|
644
|
-
expect(goodEvents.some((e) => e.type ===
|
|
645
|
-
expect(goodEvents.some((e) => e.type ===
|
|
627
|
+
expect(goodEvents.some((e) => e.type === 'agent_start')).toBe(true);
|
|
628
|
+
expect(goodEvents.some((e) => e.type === 'agent_end')).toBe(true);
|
|
646
629
|
});
|
|
647
|
-
it(
|
|
630
|
+
it('should emit tool_execution_update events to generator consumers', async () => {
|
|
648
631
|
const updatingTool = defineTool({
|
|
649
|
-
name:
|
|
650
|
-
description:
|
|
632
|
+
name: 'progress_tool',
|
|
633
|
+
description: 'Reports progress via onUpdate',
|
|
651
634
|
parameters: z.object({}),
|
|
652
635
|
execute: async (_args, { onUpdate }) => {
|
|
653
636
|
if (onUpdate) {
|
|
654
|
-
onUpdate({ content: [{ type:
|
|
655
|
-
onUpdate({ content: [{ type:
|
|
656
|
-
onUpdate({ content: [{ type:
|
|
637
|
+
onUpdate({ content: [{ type: 'text', text: '25%' }] });
|
|
638
|
+
onUpdate({ content: [{ type: 'text', text: '50%' }] });
|
|
639
|
+
onUpdate({ content: [{ type: 'text', text: '75%' }] });
|
|
657
640
|
}
|
|
658
|
-
return { content: [{ type:
|
|
641
|
+
return { content: [{ type: 'text', text: '100%' }] };
|
|
659
642
|
},
|
|
660
643
|
});
|
|
661
|
-
const mockProvider = createMockProvider(
|
|
644
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
662
645
|
[
|
|
663
646
|
createToolCallMessage([
|
|
664
647
|
{
|
|
665
|
-
type:
|
|
666
|
-
id:
|
|
667
|
-
name:
|
|
648
|
+
type: 'toolCall',
|
|
649
|
+
id: 'call_1',
|
|
650
|
+
name: 'progress_tool',
|
|
668
651
|
arguments: {},
|
|
669
652
|
},
|
|
670
653
|
]),
|
|
671
654
|
],
|
|
672
|
-
[createTextMessage(
|
|
655
|
+
[createTextMessage('All done')],
|
|
673
656
|
]);
|
|
674
657
|
registerProvider(mockProvider);
|
|
675
658
|
const agent = new Agent({
|
|
@@ -677,41 +660,41 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
677
660
|
tools: [updatingTool],
|
|
678
661
|
});
|
|
679
662
|
const events = [];
|
|
680
|
-
for await (const event of agent.run(
|
|
663
|
+
for await (const event of agent.run('Run progress tool')) {
|
|
681
664
|
events.push(event);
|
|
682
665
|
}
|
|
683
|
-
const updateEvents = events.filter((e) => e.type ===
|
|
666
|
+
const updateEvents = events.filter((e) => e.type === 'tool_execution_update');
|
|
684
667
|
expect(updateEvents.length).toBe(3);
|
|
685
|
-
expect(updateEvents[0].partialResult.content[0].text).toBe(
|
|
686
|
-
expect(updateEvents[1].partialResult.content[0].text).toBe(
|
|
687
|
-
expect(updateEvents[2].partialResult.content[0].text).toBe(
|
|
688
|
-
const agentEnd = events.find((e) => e.type ===
|
|
668
|
+
expect(updateEvents[0].partialResult.content[0].text).toBe('25%');
|
|
669
|
+
expect(updateEvents[1].partialResult.content[0].text).toBe('50%');
|
|
670
|
+
expect(updateEvents[2].partialResult.content[0].text).toBe('75%');
|
|
671
|
+
const agentEnd = events.find((e) => e.type === 'agent_end');
|
|
689
672
|
expect(agentEnd).toBeDefined();
|
|
690
673
|
});
|
|
691
|
-
it(
|
|
674
|
+
it('should also emit tool_execution_update events to subscriber listeners', async () => {
|
|
692
675
|
const updatingTool = defineTool({
|
|
693
|
-
name:
|
|
694
|
-
description:
|
|
676
|
+
name: 'progress_tool',
|
|
677
|
+
description: 'Reports progress via onUpdate',
|
|
695
678
|
parameters: z.object({}),
|
|
696
679
|
execute: async (_args, { onUpdate }) => {
|
|
697
680
|
if (onUpdate) {
|
|
698
|
-
onUpdate({ content: [{ type:
|
|
681
|
+
onUpdate({ content: [{ type: 'text', text: 'step 1' }] });
|
|
699
682
|
}
|
|
700
|
-
return { content: [{ type:
|
|
683
|
+
return { content: [{ type: 'text', text: 'done' }] };
|
|
701
684
|
},
|
|
702
685
|
});
|
|
703
|
-
const mockProvider = createMockProvider(
|
|
686
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
704
687
|
[
|
|
705
688
|
createToolCallMessage([
|
|
706
689
|
{
|
|
707
|
-
type:
|
|
708
|
-
id:
|
|
709
|
-
name:
|
|
690
|
+
type: 'toolCall',
|
|
691
|
+
id: 'call_1',
|
|
692
|
+
name: 'progress_tool',
|
|
710
693
|
arguments: {},
|
|
711
694
|
},
|
|
712
695
|
]),
|
|
713
696
|
],
|
|
714
|
-
[createTextMessage(
|
|
697
|
+
[createTextMessage('All done')],
|
|
715
698
|
]);
|
|
716
699
|
registerProvider(mockProvider);
|
|
717
700
|
const agent = new Agent({
|
|
@@ -720,27 +703,25 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
720
703
|
});
|
|
721
704
|
const subscriberUpdates = [];
|
|
722
705
|
agent.subscribe((event) => {
|
|
723
|
-
if (event.type ===
|
|
706
|
+
if (event.type === 'tool_execution_update') {
|
|
724
707
|
subscriberUpdates.push(event);
|
|
725
708
|
}
|
|
726
709
|
});
|
|
727
|
-
for await (const _ of agent.run(
|
|
710
|
+
for await (const _ of agent.run('Run progress tool')) {
|
|
728
711
|
// consume
|
|
729
712
|
}
|
|
730
713
|
expect(subscriberUpdates.length).toBe(1);
|
|
731
|
-
expect(subscriberUpdates[0].partialResult.content[0].text).toBe(
|
|
714
|
+
expect(subscriberUpdates[0].partialResult.content[0].text).toBe('step 1');
|
|
732
715
|
});
|
|
733
|
-
it(
|
|
734
|
-
const mockProvider = createMockProvider(
|
|
735
|
-
[createTextMessage("Transformed")],
|
|
736
|
-
]);
|
|
716
|
+
it('should support transformContext hook', async () => {
|
|
717
|
+
const mockProvider = createMockProvider('test-provider', [[createTextMessage('Transformed')]]);
|
|
737
718
|
registerProvider(mockProvider);
|
|
738
719
|
const transformFn = vi.fn().mockImplementation((messages) => {
|
|
739
720
|
return [
|
|
740
721
|
...messages,
|
|
741
722
|
{
|
|
742
|
-
role:
|
|
743
|
-
content:
|
|
723
|
+
role: 'user',
|
|
724
|
+
content: '[Transformed]',
|
|
744
725
|
timestamp: Date.now(),
|
|
745
726
|
},
|
|
746
727
|
];
|
|
@@ -749,34 +730,34 @@ describe("Agent E2E - Advanced Features", () => {
|
|
|
749
730
|
model: testModel,
|
|
750
731
|
transformContext: transformFn,
|
|
751
732
|
});
|
|
752
|
-
for await (const _ of agent.run(
|
|
733
|
+
for await (const _ of agent.run('Test')) {
|
|
753
734
|
// consume
|
|
754
735
|
}
|
|
755
736
|
expect(transformFn).toHaveBeenCalled();
|
|
756
737
|
});
|
|
757
738
|
});
|
|
758
|
-
describe(
|
|
759
|
-
it(
|
|
739
|
+
describe('Agent E2E - Complex Scenarios', () => {
|
|
740
|
+
it('should handle multi-turn conversation with tools', async () => {
|
|
760
741
|
const calculator = defineTool({
|
|
761
|
-
name:
|
|
762
|
-
description:
|
|
742
|
+
name: 'calculate',
|
|
743
|
+
description: 'Calculate',
|
|
763
744
|
parameters: z.object({ expression: z.string() }),
|
|
764
745
|
execute: async (args) => ({
|
|
765
|
-
content: [{ type:
|
|
746
|
+
content: [{ type: 'text', text: `Result: ${args.expression}` }],
|
|
766
747
|
}),
|
|
767
748
|
});
|
|
768
|
-
const mockProvider = createMockProvider(
|
|
749
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
769
750
|
[
|
|
770
751
|
createToolCallMessage([
|
|
771
752
|
{
|
|
772
|
-
type:
|
|
773
|
-
id:
|
|
774
|
-
name:
|
|
775
|
-
arguments: { expression:
|
|
753
|
+
type: 'toolCall',
|
|
754
|
+
id: 'call_1',
|
|
755
|
+
name: 'calculate',
|
|
756
|
+
arguments: { expression: '2+2' },
|
|
776
757
|
},
|
|
777
758
|
]),
|
|
778
759
|
],
|
|
779
|
-
[createTextMessage(
|
|
760
|
+
[createTextMessage('The result is 4')],
|
|
780
761
|
]);
|
|
781
762
|
registerProvider(mockProvider);
|
|
782
763
|
const agent = new Agent({
|
|
@@ -784,58 +765,58 @@ describe("Agent E2E - Complex Scenarios", () => {
|
|
|
784
765
|
tools: [calculator],
|
|
785
766
|
});
|
|
786
767
|
const events = [];
|
|
787
|
-
for await (const event of agent.run(
|
|
768
|
+
for await (const event of agent.run('Calculate 2+2')) {
|
|
788
769
|
events.push(event);
|
|
789
770
|
}
|
|
790
771
|
// Should have tool execution events
|
|
791
|
-
const toolEvents = events.filter((e) => e.type ===
|
|
772
|
+
const toolEvents = events.filter((e) => e.type === 'tool_execution_start' || e.type === 'tool_execution_end');
|
|
792
773
|
expect(toolEvents.length).toBe(2);
|
|
793
774
|
// Final message should be the assistant's response
|
|
794
775
|
const finalMessages = agent.messages;
|
|
795
|
-
const assistantMessages = finalMessages.filter((m) => m.role ===
|
|
776
|
+
const assistantMessages = finalMessages.filter((m) => m.role === 'assistant');
|
|
796
777
|
expect(assistantMessages.length).toBeGreaterThan(0);
|
|
797
778
|
});
|
|
798
|
-
it(
|
|
799
|
-
const mockProvider = createMockProvider(
|
|
779
|
+
it('should handle empty tool calls gracefully', async () => {
|
|
780
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
800
781
|
[createToolCallMessage([])], // Empty tool calls
|
|
801
|
-
[createTextMessage(
|
|
782
|
+
[createTextMessage('No tools needed')],
|
|
802
783
|
]);
|
|
803
784
|
registerProvider(mockProvider);
|
|
804
785
|
const agent = new Agent({
|
|
805
786
|
model: testModel,
|
|
806
787
|
});
|
|
807
788
|
const events = [];
|
|
808
|
-
for await (const event of agent.run(
|
|
789
|
+
for await (const event of agent.run('Test')) {
|
|
809
790
|
events.push(event);
|
|
810
791
|
}
|
|
811
792
|
// Should complete without errors
|
|
812
|
-
const agentEnd = events.find((e) => e.type ===
|
|
793
|
+
const agentEnd = events.find((e) => e.type === 'agent_end');
|
|
813
794
|
expect(agentEnd).toBeDefined();
|
|
814
795
|
});
|
|
815
|
-
it(
|
|
796
|
+
it('should validate tool arguments with Zod schema', async () => {
|
|
816
797
|
const tool = defineTool({
|
|
817
|
-
name:
|
|
818
|
-
description:
|
|
798
|
+
name: 'strict_tool',
|
|
799
|
+
description: 'Requires specific args',
|
|
819
800
|
parameters: z.object({
|
|
820
801
|
count: z.number().min(1).max(10),
|
|
821
802
|
name: z.string().min(1),
|
|
822
803
|
}),
|
|
823
804
|
execute: async (args) => ({
|
|
824
|
-
content: [{ type:
|
|
805
|
+
content: [{ type: 'text', text: `Count: ${args.count}, Name: ${args.name}` }],
|
|
825
806
|
}),
|
|
826
807
|
});
|
|
827
|
-
const mockProvider = createMockProvider(
|
|
808
|
+
const mockProvider = createMockProvider('test-provider', [
|
|
828
809
|
[
|
|
829
810
|
createToolCallMessage([
|
|
830
811
|
{
|
|
831
|
-
type:
|
|
832
|
-
id:
|
|
833
|
-
name:
|
|
834
|
-
arguments: { count: 5, name:
|
|
812
|
+
type: 'toolCall',
|
|
813
|
+
id: 'call_1',
|
|
814
|
+
name: 'strict_tool',
|
|
815
|
+
arguments: { count: 5, name: 'test' },
|
|
835
816
|
},
|
|
836
817
|
]),
|
|
837
818
|
],
|
|
838
|
-
[createTextMessage(
|
|
819
|
+
[createTextMessage('Done')],
|
|
839
820
|
]);
|
|
840
821
|
registerProvider(mockProvider);
|
|
841
822
|
const agent = new Agent({
|
|
@@ -843,10 +824,10 @@ describe("Agent E2E - Complex Scenarios", () => {
|
|
|
843
824
|
tools: [tool],
|
|
844
825
|
});
|
|
845
826
|
const events = [];
|
|
846
|
-
for await (const event of agent.run(
|
|
827
|
+
for await (const event of agent.run('Use strict tool')) {
|
|
847
828
|
events.push(event);
|
|
848
829
|
}
|
|
849
|
-
const toolEnd = events.find((e) => e.type ===
|
|
830
|
+
const toolEnd = events.find((e) => e.type === 'tool_execution_end');
|
|
850
831
|
expect(toolEnd).toBeDefined();
|
|
851
832
|
expect(toolEnd.isError).toBe(false);
|
|
852
833
|
});
|