@mohanscodex/spectra-agent 0.4.6 → 0.4.7

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.
@@ -1,14 +1,14 @@
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";
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: "claude-sonnet-4-20250514",
9
- name: "Claude Sonnet 4",
10
- provider: "test-provider",
11
- api: "test",
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: "assistant",
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: "stop",
29
+ stopReason: 'stop',
30
30
  timestamp: Date.now(),
31
31
  };
32
- stream.push({ type: "start", partial });
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 === "text") {
37
+ if (block.type === 'text') {
38
38
  stream.push({
39
- type: "text_delta",
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 === "toolCall") {
45
+ else if (block.type === 'toolCall') {
46
46
  stream.push({
47
- type: "toolcall_start",
47
+ type: 'toolcall_start',
48
48
  contentIndex: i,
49
49
  partial: { ...partial, content: [block] },
50
50
  });
51
51
  stream.push({
52
- type: "toolcall_end",
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: "done",
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 = "stop") {
74
+ function createTextMessage(text, stopReason = 'stop') {
75
75
  return {
76
- role: "assistant",
77
- content: [{ type: "text", text }],
78
- provider: "test-provider",
79
- model: "test-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: "assistant",
88
+ role: 'assistant',
89
89
  content: toolCalls,
90
- provider: "test-provider",
91
- model: "test-model",
90
+ provider: 'test-provider',
91
+ model: 'test-model',
92
92
  usage: { input: 10, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 30 },
93
- stopReason: "toolUse",
93
+ stopReason: 'toolUse',
94
94
  timestamp: Date.now(),
95
95
  };
96
96
  }
97
- describe("Agent E2E - Basic Conversation", () => {
97
+ describe('Agent E2E - Basic Conversation', () => {
98
98
  beforeEach(() => {
99
99
  // Clear and re-register mock provider
100
100
  });
101
- it("should run simple conversation without tools", async () => {
102
- const mockProvider = createMockProvider("test-provider", [
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: "You are a helpful assistant.",
106
+ systemPrompt: 'You are a helpful assistant.',
109
107
  });
110
108
  const events = [];
111
- for await (const event of agent.run("Hi!")) {
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("agent_start");
118
- expect(eventTypes).toContain("agent_end");
119
- expect(eventTypes).toContain("turn_start");
120
- expect(eventTypes).toContain("turn_end");
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("should maintain message history across turns", async () => {
123
- const responses = [
124
- [createTextMessage("First response")],
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("Message 1")) {
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("Message 2")) {
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("should emit message events with correct structure", async () => {
146
- const mockProvider = createMockProvider("test-provider", [
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("Test")) {
147
+ for await (const event of agent.run('Test')) {
155
148
  events.push(event);
156
149
  }
157
- const messageStart = events.find((e) => e.type === "message_start");
158
- const messageEnd = events.find((e) => e.type === "message_end");
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("Agent E2E - Tool Execution", () => {
166
- it("should execute single tool call", async () => {
158
+ describe('Agent E2E - Tool Execution', () => {
159
+ it('should execute single tool call', async () => {
167
160
  const tool = defineTool({
168
- name: "get_weather",
169
- description: "Get weather information",
161
+ name: 'get_weather',
162
+ description: 'Get weather information',
170
163
  parameters: z.object({
171
- location: z.string().describe("The location"),
164
+ location: z.string().describe('The location'),
172
165
  }),
173
166
  execute: async (args) => {
174
167
  return {
175
- content: [{ type: "text", text: `Weather in ${args.location}: Sunny` }],
168
+ content: [{ type: 'text', text: `Weather in ${args.location}: Sunny` }],
176
169
  };
177
170
  },
178
171
  });
179
- const mockProvider = createMockProvider("test-provider", [
172
+ const mockProvider = createMockProvider('test-provider', [
180
173
  [
181
174
  createToolCallMessage([
182
175
  {
183
- type: "toolCall",
184
- id: "call_1",
185
- name: "get_weather",
186
- arguments: { location: "NYC" },
176
+ type: 'toolCall',
177
+ id: 'call_1',
178
+ name: 'get_weather',
179
+ arguments: { location: 'NYC' },
187
180
  },
188
181
  ]),
189
182
  ],
190
- [createTextMessage("The weather in NYC is Sunny")],
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 === "tool_execution_start");
202
- const toolEnd = events.find((e) => e.type === "tool_execution_end");
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("get_weather");
206
- expect(toolEnd.result.content[0].text).toBe("Weather in NYC: Sunny");
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("should execute multiple tools in parallel", async () => {
202
+ it('should execute multiple tools in parallel', async () => {
210
203
  const tool1 = defineTool({
211
- name: "get_weather",
212
- description: "Get weather",
204
+ name: 'get_weather',
205
+ description: 'Get weather',
213
206
  parameters: z.object({ location: z.string() }),
214
207
  execute: async (args) => ({
215
- content: [{ type: "text", text: `Weather: ${args.location}` }],
208
+ content: [{ type: 'text', text: `Weather: ${args.location}` }],
216
209
  }),
217
210
  });
218
211
  const tool2 = defineTool({
219
- name: "get_time",
220
- description: "Get time",
212
+ name: 'get_time',
213
+ description: 'Get time',
221
214
  parameters: z.object({ timezone: z.string() }),
222
215
  execute: async (args) => ({
223
- content: [{ type: "text", text: `Time: ${args.timezone}` }],
216
+ content: [{ type: 'text', text: `Time: ${args.timezone}` }],
224
217
  }),
225
218
  });
226
- const mockProvider = createMockProvider("test-provider", [
219
+ const mockProvider = createMockProvider('test-provider', [
227
220
  [
228
221
  createToolCallMessage([
229
222
  {
230
- type: "toolCall",
231
- id: "call_1",
232
- name: "get_weather",
233
- arguments: { location: "NYC" },
223
+ type: 'toolCall',
224
+ id: 'call_1',
225
+ name: 'get_weather',
226
+ arguments: { location: 'NYC' },
234
227
  },
235
228
  {
236
- type: "toolCall",
237
- id: "call_2",
238
- name: "get_time",
239
- arguments: { timezone: "EST" },
229
+ type: 'toolCall',
230
+ id: 'call_2',
231
+ name: 'get_time',
232
+ arguments: { timezone: 'EST' },
240
233
  },
241
234
  ]),
242
235
  ],
243
- [createTextMessage("Done")],
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: "parallel",
242
+ toolExecution: 'parallel',
250
243
  });
251
244
  const events = [];
252
- for await (const event of agent.run("Get info")) {
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 === "tool_execution_start");
256
- const toolEnds = events.filter((e) => e.type === "tool_execution_end");
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("should handle tool execution errors", async () => {
253
+ it('should handle tool execution errors', async () => {
261
254
  const failingTool = defineTool({
262
- name: "fail_tool",
263
- description: "Always fails",
255
+ name: 'fail_tool',
256
+ description: 'Always fails',
264
257
  parameters: z.object({}),
265
258
  execute: async () => {
266
- throw new Error("Tool execution failed");
259
+ throw new Error('Tool execution failed');
267
260
  },
268
261
  });
269
- const mockProvider = createMockProvider("test-provider", [
262
+ const mockProvider = createMockProvider('test-provider', [
270
263
  [
271
264
  createToolCallMessage([
272
265
  {
273
- type: "toolCall",
274
- id: "call_1",
275
- name: "fail_tool",
266
+ type: 'toolCall',
267
+ id: 'call_1',
268
+ name: 'fail_tool',
276
269
  arguments: {},
277
270
  },
278
271
  ]),
279
272
  ],
280
- [createTextMessage("Sorry, the tool failed.")],
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("Use failing tool")) {
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 === "tool_execution_end");
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("Tool execution failed");
287
+ expect(toolEnd.result.content[0].text).toContain('Tool execution failed');
295
288
  });
296
- it("should handle unknown tool calls", async () => {
297
- const mockProvider = createMockProvider("test-provider", [
289
+ it('should handle unknown tool calls', async () => {
290
+ const mockProvider = createMockProvider('test-provider', [
298
291
  [
299
292
  createToolCallMessage([
300
293
  {
301
- type: "toolCall",
302
- id: "call_1",
303
- name: "unknown_tool",
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("Call unknown tool")) {
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 === "tool_execution_end");
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("Agent E2E - Advanced Features", () => {
326
- it("should support beforeToolCall hook", async () => {
318
+ describe('Agent E2E - Advanced Features', () => {
319
+ it('should support beforeToolCall hook', async () => {
327
320
  const tool = defineTool({
328
- name: "sensitive_tool",
329
- description: "Sensitive operation",
321
+ name: 'sensitive_tool',
322
+ description: 'Sensitive operation',
330
323
  parameters: z.object({}),
331
324
  execute: async () => ({
332
- content: [{ type: "text", text: "Done" }],
325
+ content: [{ type: 'text', text: 'Done' }],
333
326
  }),
334
327
  });
335
- const mockProvider = createMockProvider("test-provider", [
328
+ const mockProvider = createMockProvider('test-provider', [
336
329
  [
337
330
  createToolCallMessage([
338
331
  {
339
- type: "toolCall",
340
- id: "call_1",
341
- name: "sensitive_tool",
332
+ type: 'toolCall',
333
+ id: 'call_1',
334
+ name: 'sensitive_tool',
342
335
  arguments: {},
343
336
  },
344
337
  ]),
345
338
  ],
346
- [createTextMessage("Blocked")],
339
+ [createTextMessage('Blocked')],
347
340
  ]);
348
341
  registerProvider(mockProvider);
349
- const beforeHook = vi.fn().mockResolvedValue({ block: true, reason: "Not allowed" });
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("Use sensitive tool")) {
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 === "toolResult" && m.toolName === "sensitive_tool");
353
+ const toolEnd = agent.messages.find((m) => m.role === 'toolResult' && m.toolName === 'sensitive_tool');
361
354
  expect(toolEnd).toBeDefined();
362
- if (toolEnd?.role === "toolResult") {
355
+ if (toolEnd?.role === 'toolResult') {
363
356
  expect(toolEnd.isError).toBe(true);
364
357
  }
365
358
  });
366
- it("should support afterToolCall hook", async () => {
359
+ it('should support afterToolCall hook', async () => {
367
360
  const tool = defineTool({
368
- name: "data_tool",
369
- description: "Get data",
361
+ name: 'data_tool',
362
+ description: 'Get data',
370
363
  parameters: z.object({}),
371
364
  execute: async () => ({
372
- content: [{ type: "text", text: "Raw data" }],
365
+ content: [{ type: 'text', text: 'Raw data' }],
373
366
  }),
374
367
  });
375
- const mockProvider = createMockProvider("test-provider", [
368
+ const mockProvider = createMockProvider('test-provider', [
376
369
  [
377
370
  createToolCallMessage([
378
371
  {
379
- type: "toolCall",
380
- id: "call_1",
381
- name: "data_tool",
372
+ type: 'toolCall',
373
+ id: 'call_1',
374
+ name: 'data_tool',
382
375
  arguments: {},
383
376
  },
384
377
  ]),
385
378
  ],
386
- [createTextMessage("Processed")],
379
+ [createTextMessage('Processed')],
387
380
  ]);
388
381
  registerProvider(mockProvider);
389
382
  const afterHook = vi.fn().mockResolvedValue({
390
- content: [{ type: "text", text: "Modified data" }],
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("Get data")) {
390
+ for await (const _ of agent.run('Get data')) {
398
391
  // consume
399
392
  }
400
393
  expect(afterHook).toHaveBeenCalled();
401
394
  });
402
- it("should support sequential tool execution", async () => {
395
+ it('should support sequential tool execution', async () => {
403
396
  const executionOrder = [];
404
397
  const tool1 = defineTool({
405
- name: "tool_a",
406
- description: "Tool A",
398
+ name: 'tool_a',
399
+ description: 'Tool A',
407
400
  parameters: z.object({}),
408
401
  execute: async () => {
409
- executionOrder.push("A");
402
+ executionOrder.push('A');
410
403
  await new Promise((resolve) => setTimeout(resolve, 50));
411
- return { content: [{ type: "text", text: "A" }] };
404
+ return { content: [{ type: 'text', text: 'A' }] };
412
405
  },
413
406
  });
414
407
  const tool2 = defineTool({
415
- name: "tool_b",
416
- description: "Tool B",
408
+ name: 'tool_b',
409
+ description: 'Tool B',
417
410
  parameters: z.object({}),
418
411
  execute: async () => {
419
- executionOrder.push("B");
420
- return { content: [{ type: "text", text: "B" }] };
412
+ executionOrder.push('B');
413
+ return { content: [{ type: 'text', text: 'B' }] };
421
414
  },
422
415
  });
423
- const mockProvider = createMockProvider("test-provider", [
416
+ const mockProvider = createMockProvider('test-provider', [
424
417
  [
425
418
  createToolCallMessage([
426
419
  {
427
- type: "toolCall",
428
- id: "call_1",
429
- name: "tool_a",
420
+ type: 'toolCall',
421
+ id: 'call_1',
422
+ name: 'tool_a',
430
423
  arguments: {},
431
424
  },
432
425
  {
433
- type: "toolCall",
434
- id: "call_2",
435
- name: "tool_b",
426
+ type: 'toolCall',
427
+ id: 'call_2',
428
+ name: 'tool_b',
436
429
  arguments: {},
437
430
  },
438
431
  ]),
439
432
  ],
440
- [createTextMessage("Done")],
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: "sequential",
439
+ toolExecution: 'sequential',
447
440
  });
448
- for await (const _ of agent.run("Sequential test")) {
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(["A", "B"]);
445
+ expect(executionOrder).toEqual(['A', 'B']);
453
446
  });
454
- it("should handle abort signal", async () => {
455
- const mockProvider = createMockProvider("test-provider", [
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("Test abort");
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("should not hang when transformContext hook throws unexpectedly", async () => {
478
- const mockProvider = createMockProvider("test-provider", [
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("Unexpected transformation failure");
474
+ throw new Error('Unexpected transformation failure');
486
475
  },
487
476
  });
488
477
  const events = [];
489
- for await (const event of agent.run("Test")) {
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 === "agent_end");
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("should reset state correctly", async () => {
498
- const mockProvider = createMockProvider("test-provider", [
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("Test")) {
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("should emit agent events to subscribers", async () => {
516
- const mockProvider = createMockProvider("test-provider", [
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("Test")) {
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 === "agent_start")).toBe(true);
532
- expect(subscriberEvents.some((e) => e.type === "agent_end")).toBe(true);
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("should handle beforeToolCall hook that throws without hanging", async () => {
520
+ it('should handle beforeToolCall hook that throws without hanging', async () => {
536
521
  const tool = defineTool({
537
- name: "normal_tool",
538
- description: "A normal tool",
522
+ name: 'normal_tool',
523
+ description: 'A normal tool',
539
524
  parameters: z.object({}),
540
525
  execute: async () => ({
541
- content: [{ type: "text", text: "Done" }],
526
+ content: [{ type: 'text', text: 'Done' }],
542
527
  }),
543
528
  });
544
- const mockProvider = createMockProvider("test-provider", [
529
+ const mockProvider = createMockProvider('test-provider', [
545
530
  [
546
531
  createToolCallMessage([
547
532
  {
548
- type: "toolCall",
549
- id: "call_1",
550
- name: "normal_tool",
533
+ type: 'toolCall',
534
+ id: 'call_1',
535
+ name: 'normal_tool',
551
536
  arguments: {},
552
537
  },
553
538
  ]),
554
539
  ],
555
- [createTextMessage("Recovered")],
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("Hook crash!");
547
+ throw new Error('Hook crash!');
563
548
  },
564
549
  });
565
550
  const events = [];
566
- for await (const event of agent.run("Test")) {
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 === "agent_end");
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 === "tool_execution_end");
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("should handle afterToolCall hook that throws without hanging", async () => {
562
+ it('should handle afterToolCall hook that throws without hanging', async () => {
578
563
  const tool = defineTool({
579
- name: "normal_tool",
580
- description: "A normal tool",
564
+ name: 'normal_tool',
565
+ description: 'A normal tool',
581
566
  parameters: z.object({}),
582
567
  execute: async () => ({
583
- content: [{ type: "text", text: "Done" }],
568
+ content: [{ type: 'text', text: 'Done' }],
584
569
  }),
585
570
  });
586
- const mockProvider = createMockProvider("test-provider", [
571
+ const mockProvider = createMockProvider('test-provider', [
587
572
  [
588
573
  createToolCallMessage([
589
574
  {
590
- type: "toolCall",
591
- id: "call_1",
592
- name: "normal_tool",
575
+ type: 'toolCall',
576
+ id: 'call_1',
577
+ name: 'normal_tool',
593
578
  arguments: {},
594
579
  },
595
580
  ]),
596
581
  ],
597
- [createTextMessage("Recovered")],
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("Hook crash!");
589
+ throw new Error('Hook crash!');
605
590
  },
606
591
  });
607
592
  const events = [];
608
- for await (const event of agent.run("Test")) {
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 === "agent_end");
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 === "tool_execution_end");
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("Done");
603
+ expect(toolEnd.result.content[0].text).toBe('Done');
619
604
  });
620
- it("should isolate listener errors so one failing subscriber does not break others", async () => {
621
- const mockProvider = createMockProvider("test-provider", [
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("Listener crash!");
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("Test")) {
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 === "agent_start")).toBe(true);
645
- expect(goodEvents.some((e) => e.type === "agent_end")).toBe(true);
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("should emit tool_execution_update events to generator consumers", async () => {
630
+ it('should emit tool_execution_update events to generator consumers', async () => {
648
631
  const updatingTool = defineTool({
649
- name: "progress_tool",
650
- description: "Reports progress via onUpdate",
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: "text", text: "25%" }] });
655
- onUpdate({ content: [{ type: "text", text: "50%" }] });
656
- onUpdate({ content: [{ type: "text", text: "75%" }] });
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: "text", text: "100%" }] };
641
+ return { content: [{ type: 'text', text: '100%' }] };
659
642
  },
660
643
  });
661
- const mockProvider = createMockProvider("test-provider", [
644
+ const mockProvider = createMockProvider('test-provider', [
662
645
  [
663
646
  createToolCallMessage([
664
647
  {
665
- type: "toolCall",
666
- id: "call_1",
667
- name: "progress_tool",
648
+ type: 'toolCall',
649
+ id: 'call_1',
650
+ name: 'progress_tool',
668
651
  arguments: {},
669
652
  },
670
653
  ]),
671
654
  ],
672
- [createTextMessage("All done")],
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("Run progress tool")) {
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 === "tool_execution_update");
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("25%");
686
- expect(updateEvents[1].partialResult.content[0].text).toBe("50%");
687
- expect(updateEvents[2].partialResult.content[0].text).toBe("75%");
688
- const agentEnd = events.find((e) => e.type === "agent_end");
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("should also emit tool_execution_update events to subscriber listeners", async () => {
674
+ it('should also emit tool_execution_update events to subscriber listeners', async () => {
692
675
  const updatingTool = defineTool({
693
- name: "progress_tool",
694
- description: "Reports progress via onUpdate",
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: "text", text: "step 1" }] });
681
+ onUpdate({ content: [{ type: 'text', text: 'step 1' }] });
699
682
  }
700
- return { content: [{ type: "text", text: "done" }] };
683
+ return { content: [{ type: 'text', text: 'done' }] };
701
684
  },
702
685
  });
703
- const mockProvider = createMockProvider("test-provider", [
686
+ const mockProvider = createMockProvider('test-provider', [
704
687
  [
705
688
  createToolCallMessage([
706
689
  {
707
- type: "toolCall",
708
- id: "call_1",
709
- name: "progress_tool",
690
+ type: 'toolCall',
691
+ id: 'call_1',
692
+ name: 'progress_tool',
710
693
  arguments: {},
711
694
  },
712
695
  ]),
713
696
  ],
714
- [createTextMessage("All done")],
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 === "tool_execution_update") {
706
+ if (event.type === 'tool_execution_update') {
724
707
  subscriberUpdates.push(event);
725
708
  }
726
709
  });
727
- for await (const _ of agent.run("Run progress tool")) {
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("step 1");
714
+ expect(subscriberUpdates[0].partialResult.content[0].text).toBe('step 1');
732
715
  });
733
- it("should support transformContext hook", async () => {
734
- const mockProvider = createMockProvider("test-provider", [
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: "user",
743
- content: "[Transformed]",
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("Test")) {
733
+ for await (const _ of agent.run('Test')) {
753
734
  // consume
754
735
  }
755
736
  expect(transformFn).toHaveBeenCalled();
756
737
  });
757
738
  });
758
- describe("Agent E2E - Complex Scenarios", () => {
759
- it("should handle multi-turn conversation with tools", async () => {
739
+ describe('Agent E2E - Complex Scenarios', () => {
740
+ it('should handle multi-turn conversation with tools', async () => {
760
741
  const calculator = defineTool({
761
- name: "calculate",
762
- description: "Calculate",
742
+ name: 'calculate',
743
+ description: 'Calculate',
763
744
  parameters: z.object({ expression: z.string() }),
764
745
  execute: async (args) => ({
765
- content: [{ type: "text", text: `Result: ${args.expression}` }],
746
+ content: [{ type: 'text', text: `Result: ${args.expression}` }],
766
747
  }),
767
748
  });
768
- const mockProvider = createMockProvider("test-provider", [
749
+ const mockProvider = createMockProvider('test-provider', [
769
750
  [
770
751
  createToolCallMessage([
771
752
  {
772
- type: "toolCall",
773
- id: "call_1",
774
- name: "calculate",
775
- arguments: { expression: "2+2" },
753
+ type: 'toolCall',
754
+ id: 'call_1',
755
+ name: 'calculate',
756
+ arguments: { expression: '2+2' },
776
757
  },
777
758
  ]),
778
759
  ],
779
- [createTextMessage("The result is 4")],
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("Calculate 2+2")) {
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 === "tool_execution_start" || e.type === "tool_execution_end");
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 === "assistant");
776
+ const assistantMessages = finalMessages.filter((m) => m.role === 'assistant');
796
777
  expect(assistantMessages.length).toBeGreaterThan(0);
797
778
  });
798
- it("should handle empty tool calls gracefully", async () => {
799
- const mockProvider = createMockProvider("test-provider", [
779
+ it('should handle empty tool calls gracefully', async () => {
780
+ const mockProvider = createMockProvider('test-provider', [
800
781
  [createToolCallMessage([])], // Empty tool calls
801
- [createTextMessage("No tools needed")],
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("Test")) {
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 === "agent_end");
793
+ const agentEnd = events.find((e) => e.type === 'agent_end');
813
794
  expect(agentEnd).toBeDefined();
814
795
  });
815
- it("should validate tool arguments with Zod schema", async () => {
796
+ it('should validate tool arguments with Zod schema', async () => {
816
797
  const tool = defineTool({
817
- name: "strict_tool",
818
- description: "Requires specific args",
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: "text", text: `Count: ${args.count}, Name: ${args.name}` }],
805
+ content: [{ type: 'text', text: `Count: ${args.count}, Name: ${args.name}` }],
825
806
  }),
826
807
  });
827
- const mockProvider = createMockProvider("test-provider", [
808
+ const mockProvider = createMockProvider('test-provider', [
828
809
  [
829
810
  createToolCallMessage([
830
811
  {
831
- type: "toolCall",
832
- id: "call_1",
833
- name: "strict_tool",
834
- arguments: { count: 5, name: "test" },
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("Done")],
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("Use strict tool")) {
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 === "tool_execution_end");
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
  });