@ank1015/providers 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -383
  3. package/dist/agent/conversation.d.ts +97 -0
  4. package/dist/agent/conversation.d.ts.map +1 -0
  5. package/dist/agent/conversation.js +328 -0
  6. package/dist/agent/conversation.js.map +1 -0
  7. package/dist/agent/runner.d.ts +37 -0
  8. package/dist/agent/runner.d.ts.map +1 -0
  9. package/dist/agent/runner.js +169 -0
  10. package/dist/agent/runner.js.map +1 -0
  11. package/dist/agent/tools/calculate.d.ts +15 -0
  12. package/dist/agent/tools/calculate.d.ts.map +1 -0
  13. package/dist/agent/tools/calculate.js +23 -0
  14. package/dist/agent/tools/calculate.js.map +1 -0
  15. package/dist/agent/tools/get-current-time.d.ts +15 -0
  16. package/dist/agent/tools/get-current-time.d.ts.map +1 -0
  17. package/dist/agent/tools/get-current-time.js +38 -0
  18. package/dist/agent/tools/get-current-time.js.map +1 -0
  19. package/dist/agent/tools/index.d.ts +3 -0
  20. package/dist/agent/tools/index.d.ts.map +1 -0
  21. package/dist/agent/tools/index.js +3 -0
  22. package/dist/agent/tools/index.js.map +1 -0
  23. package/dist/agent/types.d.ts +53 -31
  24. package/dist/agent/types.d.ts.map +1 -1
  25. package/dist/agent/types.js +1 -2
  26. package/dist/agent/utils.d.ts +14 -0
  27. package/dist/agent/utils.d.ts.map +1 -0
  28. package/dist/agent/utils.js +59 -0
  29. package/dist/agent/utils.js.map +1 -0
  30. package/dist/index.d.ts +16 -9
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +16 -28
  33. package/dist/index.js.map +1 -1
  34. package/dist/llm.d.ts +15 -0
  35. package/dist/llm.d.ts.map +1 -0
  36. package/dist/llm.js +92 -0
  37. package/dist/llm.js.map +1 -0
  38. package/dist/models.d.ts +8 -1
  39. package/dist/models.d.ts.map +1 -1
  40. package/dist/models.generated.d.ts +25 -112
  41. package/dist/models.generated.d.ts.map +1 -1
  42. package/dist/models.generated.js +72 -227
  43. package/dist/models.generated.js.map +1 -1
  44. package/dist/models.js +30 -32
  45. package/dist/models.js.map +1 -1
  46. package/dist/providers/google/complete.d.ts +3 -0
  47. package/dist/providers/google/complete.d.ts.map +1 -0
  48. package/dist/providers/google/complete.js +53 -0
  49. package/dist/providers/google/complete.js.map +1 -0
  50. package/dist/providers/google/index.d.ts +6 -0
  51. package/dist/providers/google/index.d.ts.map +1 -0
  52. package/dist/providers/google/index.js +6 -0
  53. package/dist/providers/google/index.js.map +1 -0
  54. package/dist/providers/google/stream.d.ts +3 -0
  55. package/dist/providers/google/stream.d.ts.map +1 -0
  56. package/dist/providers/{google.js → google/stream.js} +67 -231
  57. package/dist/providers/google/stream.js.map +1 -0
  58. package/dist/providers/google/types.d.ts +8 -0
  59. package/dist/providers/google/types.d.ts.map +1 -0
  60. package/dist/providers/google/types.js +2 -0
  61. package/dist/providers/google/types.js.map +1 -0
  62. package/dist/providers/google/utils.d.ts +30 -0
  63. package/dist/providers/google/utils.d.ts.map +1 -0
  64. package/dist/providers/google/utils.js +354 -0
  65. package/dist/providers/google/utils.js.map +1 -0
  66. package/dist/providers/openai/complete.d.ts +3 -0
  67. package/dist/providers/openai/complete.d.ts.map +1 -0
  68. package/dist/providers/openai/complete.js +57 -0
  69. package/dist/providers/openai/complete.js.map +1 -0
  70. package/dist/providers/openai/index.d.ts +4 -0
  71. package/dist/providers/openai/index.d.ts.map +1 -0
  72. package/dist/providers/openai/index.js +4 -0
  73. package/dist/providers/openai/index.js.map +1 -0
  74. package/dist/providers/openai/stream.d.ts +3 -0
  75. package/dist/providers/openai/stream.d.ts.map +1 -0
  76. package/dist/providers/{openai.js → openai/stream.js} +74 -152
  77. package/dist/providers/openai/stream.js.map +1 -0
  78. package/dist/providers/openai/types.d.ts +8 -0
  79. package/dist/providers/openai/types.d.ts.map +1 -0
  80. package/dist/providers/openai/types.js +2 -0
  81. package/dist/providers/openai/types.js.map +1 -0
  82. package/dist/providers/openai/utils.d.ts +13 -0
  83. package/dist/providers/openai/utils.d.ts.map +1 -0
  84. package/dist/providers/openai/utils.js +285 -0
  85. package/dist/providers/openai/utils.js.map +1 -0
  86. package/dist/types.d.ts +95 -87
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js +1 -9
  89. package/dist/types.js.map +1 -1
  90. package/dist/utils/event-stream.d.ts +2 -2
  91. package/dist/utils/event-stream.d.ts.map +1 -1
  92. package/dist/utils/event-stream.js +2 -7
  93. package/dist/utils/event-stream.js.map +1 -1
  94. package/dist/utils/json-parse.js +3 -6
  95. package/dist/utils/json-parse.js.map +1 -1
  96. package/dist/utils/overflow.d.ts +51 -0
  97. package/dist/utils/overflow.d.ts.map +1 -0
  98. package/dist/utils/overflow.js +106 -0
  99. package/dist/utils/overflow.js.map +1 -0
  100. package/dist/utils/sanitize-unicode.js +1 -4
  101. package/dist/utils/sanitize-unicode.js.map +1 -1
  102. package/dist/utils/uuid.d.ts +6 -0
  103. package/dist/utils/uuid.d.ts.map +1 -0
  104. package/dist/utils/uuid.js +9 -0
  105. package/dist/utils/uuid.js.map +1 -0
  106. package/dist/utils/validation.d.ts +10 -3
  107. package/dist/utils/validation.d.ts.map +1 -1
  108. package/dist/utils/validation.js +20 -12
  109. package/dist/utils/validation.js.map +1 -1
  110. package/package.json +47 -8
  111. package/biome.json +0 -43
  112. package/dist/agent/agent-loop.d.ts +0 -5
  113. package/dist/agent/agent-loop.d.ts.map +0 -1
  114. package/dist/agent/agent-loop.js +0 -219
  115. package/dist/agent/agent-loop.js.map +0 -1
  116. package/dist/providers/convert.d.ts +0 -6
  117. package/dist/providers/convert.d.ts.map +0 -1
  118. package/dist/providers/convert.js +0 -207
  119. package/dist/providers/convert.js.map +0 -1
  120. package/dist/providers/google.d.ts +0 -26
  121. package/dist/providers/google.d.ts.map +0 -1
  122. package/dist/providers/google.js.map +0 -1
  123. package/dist/providers/openai.d.ts +0 -17
  124. package/dist/providers/openai.d.ts.map +0 -1
  125. package/dist/providers/openai.js.map +0 -1
  126. package/dist/stream.d.ts +0 -4
  127. package/dist/stream.d.ts.map +0 -1
  128. package/dist/stream.js +0 -40
  129. package/dist/stream.js.map +0 -1
  130. package/dist/test-google-agent-loop.d.ts +0 -2
  131. package/dist/test-google-agent-loop.d.ts.map +0 -1
  132. package/dist/test-google-agent-loop.js +0 -186
  133. package/dist/test-google-agent-loop.js.map +0 -1
  134. package/dist/test-google.d.ts +0 -2
  135. package/dist/test-google.d.ts.map +0 -1
  136. package/dist/test-google.js +0 -41
  137. package/dist/test-google.js.map +0 -1
  138. package/src/agent/agent-loop.ts +0 -275
  139. package/src/agent/types.ts +0 -80
  140. package/src/index.ts +0 -72
  141. package/src/models.generated.ts +0 -314
  142. package/src/models.ts +0 -45
  143. package/src/providers/convert.ts +0 -222
  144. package/src/providers/google.ts +0 -496
  145. package/src/providers/openai.ts +0 -437
  146. package/src/stream.ts +0 -60
  147. package/src/types.ts +0 -198
  148. package/src/utils/event-stream.ts +0 -60
  149. package/src/utils/json-parse.ts +0 -28
  150. package/src/utils/sanitize-unicode.ts +0 -25
  151. package/src/utils/validation.ts +0 -69
  152. package/test/core/agent-loop.test.ts +0 -958
  153. package/test/core/stream.test.ts +0 -409
  154. package/test/data/red-circle.png +0 -0
  155. package/test/data/superintelligentwill.pdf +0 -0
  156. package/test/edge-cases/general.test.ts +0 -565
  157. package/test/integration/e2e.test.ts +0 -530
  158. package/test/models/cost.test.ts +0 -499
  159. package/test/models/registry.test.ts +0 -298
  160. package/test/providers/convert.test.ts +0 -846
  161. package/test/providers/google-schema.test.ts +0 -666
  162. package/test/providers/google-stream.test.ts +0 -369
  163. package/test/providers/openai-stream.test.ts +0 -251
  164. package/test/utils/event-stream.test.ts +0 -289
  165. package/test/utils/json-parse.test.ts +0 -344
  166. package/test/utils/sanitize-unicode.test.ts +0 -329
  167. package/test/utils/validation.test.ts +0 -614
  168. package/tsconfig.json +0 -21
  169. package/vitest.config.ts +0 -9
@@ -1,958 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { Type } from '@sinclair/typebox';
3
- import { agentLoop } from '../../src/agent/agent-loop';
4
- import { AgentContext, AgentLoopConfig, AgentTool, AgentEvent } from '../../src/agent/types';
5
- import { UserMessage, AssistantMessage, NativeOpenAIMessage, Model } from '../../src/types';
6
- import * as streamModule from '../../src/stream';
7
-
8
- // Mock the stream module
9
- vi.mock('../../src/stream');
10
-
11
- // Clear mocks before each test
12
- beforeEach(() => {
13
- vi.clearAllMocks();
14
- });
15
-
16
- const mockModel: Model<'openai'> = {
17
- id: 'test-model',
18
- name: 'Test Model',
19
- api: 'openai',
20
- baseUrl: 'https://api.openai.com',
21
- reasoning: false,
22
- input: ['text'],
23
- cost: { input: 0.01, output: 0.03, cacheRead: 0, cacheWrite: 0 },
24
- contextWindow: 128000,
25
- maxTokens: 4096,
26
- };
27
-
28
- // Helper to create mock stream
29
- function createMockStream(events: any[], result: any) {
30
- const eventStream = {
31
- async *[Symbol.asyncIterator]() {
32
- for (const event of events) {
33
- yield event;
34
- }
35
- },
36
- result: async () => result,
37
- };
38
- return eventStream as any;
39
- }
40
-
41
- // Helper to create mock assistant message
42
- function createMockAssistantMessage(
43
- content: AssistantMessage['content'],
44
- stopReason: AssistantMessage['stopReason'] = 'stop'
45
- ): AssistantMessage {
46
- return {
47
- role: 'assistant',
48
- content,
49
- api: 'openai',
50
- model: 'test-model',
51
- usage: {
52
- input: 10,
53
- output: 5,
54
- cacheRead: 0,
55
- cacheWrite: 0,
56
- totalTokens: 15,
57
- cost: {
58
- input: 0.0001,
59
- output: 0.00015,
60
- cacheRead: 0,
61
- cacheWrite: 0,
62
- total: 0.00025,
63
- },
64
- },
65
- stopReason,
66
- timestamp: Date.now(),
67
- };
68
- }
69
-
70
- // Helper to create native message
71
- function createNativeMessage(assistantMessage: AssistantMessage): NativeOpenAIMessage {
72
- return {
73
- role: 'assistant',
74
- _provider: 'openai',
75
- message: {
76
- output: [],
77
- id: 'resp_123',
78
- object: "response",
79
- created_at: 1740855869,
80
- output_text: '',
81
- status: "completed",
82
- incomplete_details: null,
83
- parallel_tool_calls: false,
84
- error: null,
85
- instructions: null,
86
- max_output_tokens: null,
87
- model: "gpt-4o-mini-2024-07-18",
88
- user: undefined,
89
- metadata: {},
90
- previous_response_id: null,
91
- temperature: 1,
92
- text: {},
93
- tool_choice: "auto",
94
- tools: [],
95
- top_p: 1,
96
- truncation: "disabled",
97
- usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15, input_tokens_details: {cached_tokens: 0}, output_tokens_details: {reasoning_tokens: 0} },
98
- },
99
- };
100
- }
101
-
102
- describe('agentLoop - Basic Flow', () => {
103
- it('should handle single turn without tool calls', async () => {
104
- const prompt: UserMessage = {
105
- role: 'user',
106
- content: [{ type: 'text', content: 'Hello!' }],
107
- timestamp: Date.now(),
108
- };
109
-
110
- const context: AgentContext = {
111
- messages: [],
112
- };
113
-
114
- const assistantMessage = createMockAssistantMessage([
115
- { type: 'text', text: 'Hello! How can I help you?' },
116
- ]);
117
-
118
- const nativeMessage = createNativeMessage(assistantMessage);
119
-
120
- const mockStream = createMockStream(
121
- [
122
- { type: 'start', partial: assistantMessage },
123
- { type: 'text_start', contentIndex: 0, partial: assistantMessage },
124
- { type: 'text_delta', contentIndex: 0, delta: 'Hello!', partial: assistantMessage },
125
- { type: 'text_end', contentIndex: 0, content: 'Hello! How can I help you?', partial: assistantMessage },
126
- { type: 'done', reason: 'stop', message: assistantMessage },
127
- ],
128
- nativeMessage
129
- );
130
-
131
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
132
-
133
- const config: AgentLoopConfig<'openai'> = {
134
- model: mockModel,
135
- providerOptions: {},
136
- };
137
-
138
- const eventStream = agentLoop(prompt, context, config);
139
- const events: AgentEvent[] = [];
140
-
141
- for await (const event of eventStream) {
142
- events.push(event);
143
- }
144
-
145
- const messages = await eventStream.result();
146
-
147
- // Verify events
148
- expect(events).toContainEqual({ type: 'agent_start' });
149
- expect(events).toContainEqual({ type: 'turn_start' });
150
- expect(events).toContainEqual(expect.objectContaining({ type: 'message_start' }));
151
- expect(events).toContainEqual(expect.objectContaining({ type: 'message_end' }));
152
- expect(events).toContainEqual(expect.objectContaining({ type: 'turn_end' }));
153
- expect(events).toContainEqual(expect.objectContaining({ type: 'agent_end', status: 'completed' }));
154
-
155
- // Verify messages
156
- expect(messages).toHaveLength(2); // prompt + assistant response
157
- expect(messages[0]).toEqual(prompt);
158
- expect(messages[1]).toEqual(nativeMessage);
159
- });
160
-
161
- it('should add initial prompt to messages', async () => {
162
- const prompt: UserMessage = {
163
- role: 'user',
164
- content: [{ type: 'text', content: 'Test prompt' }],
165
- timestamp: Date.now(),
166
- };
167
-
168
- const context: AgentContext = {
169
- messages: [],
170
- };
171
-
172
- const assistantMessage = createMockAssistantMessage([
173
- { type: 'text', text: 'Response' },
174
- ]);
175
-
176
- const mockStream = createMockStream(
177
- [
178
- { type: 'start', partial: assistantMessage },
179
- { type: 'done', reason: 'stop', message: assistantMessage },
180
- ],
181
- createNativeMessage(assistantMessage)
182
- );
183
-
184
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
185
-
186
- const config: AgentLoopConfig<'openai'> = {
187
- model: mockModel,
188
- providerOptions: {},
189
- };
190
-
191
- const eventStream = agentLoop(prompt, context, config);
192
- const messages = await eventStream.result();
193
-
194
- expect(messages[0]).toEqual(prompt);
195
- });
196
-
197
- it('should emit agent start and end events', async () => {
198
- const prompt: UserMessage = {
199
- role: 'user',
200
- content: [{ type: 'text', content: 'Hello' }],
201
- timestamp: Date.now(),
202
- };
203
-
204
- const context: AgentContext = {
205
- messages: [],
206
- };
207
-
208
- const assistantMessage = createMockAssistantMessage([
209
- { type: 'text', text: 'Hi!' },
210
- ]);
211
-
212
- const mockStream = createMockStream(
213
- [
214
- { type: 'start', partial: assistantMessage },
215
- { type: 'done', reason: 'stop', message: assistantMessage },
216
- ],
217
- createNativeMessage(assistantMessage)
218
- );
219
-
220
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
221
-
222
- const config: AgentLoopConfig<'openai'> = {
223
- model: mockModel,
224
- providerOptions: {},
225
- };
226
-
227
- const eventStream = agentLoop(prompt, context, config);
228
- const events: AgentEvent[] = [];
229
-
230
- for await (const event of eventStream) {
231
- events.push(event);
232
- }
233
-
234
- expect(events[0]).toEqual({ type: 'agent_start' });
235
- expect(events[events.length - 1]).toMatchObject({
236
- type: 'agent_end',
237
- status: 'completed',
238
- });
239
- });
240
-
241
- it('should emit turn start and end events', async () => {
242
- const prompt: UserMessage = {
243
- role: 'user',
244
- content: [{ type: 'text', content: 'Hello' }],
245
- timestamp: Date.now(),
246
- };
247
-
248
- const context: AgentContext = {
249
- messages: [],
250
- };
251
-
252
- const assistantMessage = createMockAssistantMessage([
253
- { type: 'text', text: 'Hi!' },
254
- ]);
255
-
256
- const mockStream = createMockStream(
257
- [
258
- { type: 'start', partial: assistantMessage },
259
- { type: 'done', reason: 'stop', message: assistantMessage },
260
- ],
261
- createNativeMessage(assistantMessage)
262
- );
263
-
264
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
265
-
266
- const config: AgentLoopConfig<'openai'> = {
267
- model: mockModel,
268
- providerOptions: {},
269
- };
270
-
271
- const eventStream = agentLoop(prompt, context, config);
272
- const events: AgentEvent[] = [];
273
-
274
- for await (const event of eventStream) {
275
- events.push(event);
276
- }
277
-
278
- const turnStarts = events.filter(e => e.type === 'turn_start');
279
- const turnEnds = events.filter(e => e.type === 'turn_end');
280
-
281
- expect(turnStarts).toHaveLength(1);
282
- expect(turnEnds).toHaveLength(1);
283
- });
284
-
285
- it('should preserve message order', async () => {
286
- const prompt: UserMessage = {
287
- role: 'user',
288
- content: [{ type: 'text', content: 'Hello' }],
289
- timestamp: Date.now(),
290
- };
291
-
292
- const context: AgentContext = {
293
- messages: [],
294
- };
295
-
296
- const assistantMessage = createMockAssistantMessage([
297
- { type: 'text', text: 'Response' },
298
- ]);
299
-
300
- const mockStream = createMockStream(
301
- [
302
- { type: 'start', partial: assistantMessage },
303
- { type: 'done', reason: 'stop', message: assistantMessage },
304
- ],
305
- createNativeMessage(assistantMessage)
306
- );
307
-
308
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
309
-
310
- const config: AgentLoopConfig<'openai'> = {
311
- model: mockModel,
312
- providerOptions: {},
313
- };
314
-
315
- const eventStream = agentLoop(prompt, context, config);
316
- const messages = await eventStream.result();
317
-
318
- expect(messages[0].role).toBe('user');
319
- expect(messages[1].role).toBe('assistant');
320
- });
321
- });
322
-
323
- describe('agentLoop - Tool Execution', () => {
324
- it('should execute single tool call', async () => {
325
- const calculatorTool: AgentTool = {
326
- name: 'calculator',
327
- description: 'Perform calculations',
328
- label: 'Calculator',
329
- parameters: Type.Object({
330
- expression: Type.String(),
331
- }),
332
- async execute(toolCallId, params) {
333
- return {
334
- content: [{ type: 'text', content: `Result: ${eval((params as any).expression)}` }],
335
- details: { result: eval((params as any).expression) },
336
- };
337
- },
338
- };
339
-
340
- const prompt: UserMessage = {
341
- role: 'user',
342
- content: [{ type: 'text', content: 'Calculate 2 + 2' }],
343
- timestamp: Date.now(),
344
- };
345
-
346
- const context: AgentContext = {
347
- messages: [],
348
- tools: [calculatorTool],
349
- };
350
-
351
- // First turn - assistant calls tool
352
- const assistantMessage1 = createMockAssistantMessage(
353
- [
354
- {
355
- type: 'toolCall',
356
- name: 'calculator',
357
- arguments: { expression: '2 + 2' },
358
- id: 'call_123',
359
- },
360
- ],
361
- 'toolUse'
362
- );
363
-
364
- // Second turn - assistant responds with result
365
- const assistantMessage2 = createMockAssistantMessage([
366
- { type: 'text', text: 'The result is 4' },
367
- ]);
368
-
369
- let callCount = 0;
370
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
371
- callCount++;
372
- if (callCount === 1) {
373
- return createMockStream(
374
- [
375
- { type: 'start', partial: assistantMessage1 },
376
- { type: 'done', reason: 'toolUse', message: assistantMessage1 },
377
- ],
378
- createNativeMessage(assistantMessage1)
379
- );
380
- } else {
381
- return createMockStream(
382
- [
383
- { type: 'start', partial: assistantMessage2 },
384
- { type: 'done', reason: 'stop', message: assistantMessage2 },
385
- ],
386
- createNativeMessage(assistantMessage2)
387
- );
388
- }
389
- });
390
-
391
- const config: AgentLoopConfig<'openai'> = {
392
- model: mockModel,
393
- providerOptions: {},
394
- };
395
-
396
- const eventStream = agentLoop(prompt, context, config);
397
- const events: AgentEvent[] = [];
398
-
399
- for await (const event of eventStream) {
400
- events.push(event);
401
- }
402
-
403
- const toolExecutionStarts = events.filter(e => e.type === 'tool_execution_start');
404
- const toolExecutionEnds = events.filter(e => e.type === 'tool_execution_end');
405
-
406
- expect(toolExecutionStarts).toHaveLength(1);
407
- expect(toolExecutionEnds).toHaveLength(1);
408
- expect(toolExecutionStarts[0]).toMatchObject({
409
- type: 'tool_execution_start',
410
- toolName: 'calculator',
411
- toolCallId: 'call_123',
412
- });
413
- });
414
-
415
- it('should add tool result to context', async () => {
416
- const tool: AgentTool = {
417
- name: 'test_tool',
418
- description: 'Test tool',
419
- label: 'Test',
420
- parameters: Type.Object({}),
421
- async execute() {
422
- return {
423
- content: [{ type: 'text', content: 'Tool result' }],
424
- details: {},
425
- };
426
- },
427
- };
428
-
429
- const prompt: UserMessage = {
430
- role: 'user',
431
- content: [{ type: 'text', content: 'Use the tool' }],
432
- timestamp: Date.now(),
433
- };
434
-
435
- const context: AgentContext = {
436
- messages: [],
437
- tools: [tool],
438
- };
439
-
440
- const assistantMessage1 = createMockAssistantMessage(
441
- [{ type: 'toolCall', name: 'test_tool', arguments: {}, id: 'call_1' }],
442
- 'toolUse'
443
- );
444
-
445
- const assistantMessage2 = createMockAssistantMessage([
446
- { type: 'text', text: 'Done' },
447
- ]);
448
-
449
- let callCount = 0;
450
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
451
- callCount++;
452
- if (callCount === 1) {
453
- return createMockStream(
454
- [{ type: 'done', reason: 'toolUse', message: assistantMessage1 }],
455
- createNativeMessage(assistantMessage1)
456
- );
457
- } else {
458
- return createMockStream(
459
- [{ type: 'done', reason: 'stop', message: assistantMessage2 }],
460
- createNativeMessage(assistantMessage2)
461
- );
462
- }
463
- });
464
-
465
- const config: AgentLoopConfig<'openai'> = {
466
- model: mockModel,
467
- providerOptions: {},
468
- };
469
-
470
- const eventStream = agentLoop(prompt, context, config);
471
- const messages = await eventStream.result();
472
-
473
- // Should have: prompt, assistant1, tool result, assistant2
474
- expect(messages).toHaveLength(4);
475
- expect(messages[2].role).toBe('toolResult');
476
- expect((messages[2] as any).toolName).toBe('test_tool');
477
- });
478
-
479
- it('should handle tool not found error', async () => {
480
- const prompt: UserMessage = {
481
- role: 'user',
482
- content: [{ type: 'text', content: 'Use unknown tool' }],
483
- timestamp: Date.now(),
484
- };
485
-
486
- const context: AgentContext = {
487
- messages: [],
488
- tools: [], // No tools defined
489
- };
490
-
491
- const assistantMessage1 = createMockAssistantMessage(
492
- [{ type: 'toolCall', name: 'unknown_tool', arguments: {}, id: 'call_1' }],
493
- 'toolUse'
494
- );
495
-
496
- const assistantMessage2 = createMockAssistantMessage([
497
- { type: 'text', text: 'Error handled' },
498
- ]);
499
-
500
- let callCount = 0;
501
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
502
- callCount++;
503
- if (callCount === 1) {
504
- return createMockStream(
505
- [{ type: 'done', reason: 'toolUse', message: assistantMessage1 }],
506
- createNativeMessage(assistantMessage1)
507
- );
508
- } else {
509
- return createMockStream(
510
- [{ type: 'done', reason: 'stop', message: assistantMessage2 }],
511
- createNativeMessage(assistantMessage2)
512
- );
513
- }
514
- });
515
-
516
- const config: AgentLoopConfig<'openai'> = {
517
- model: mockModel,
518
- providerOptions: {},
519
- };
520
-
521
- const eventStream = agentLoop(prompt, context, config);
522
- const events: AgentEvent[] = [];
523
-
524
- for await (const event of eventStream) {
525
- events.push(event);
526
- }
527
-
528
- const toolExecutionEnd = events.find(e => e.type === 'tool_execution_end') as any;
529
- expect(toolExecutionEnd.isError).toBe(true);
530
- expect(toolExecutionEnd.result).toContain('not found');
531
- });
532
-
533
- it('should handle tool execution error', async () => {
534
- const failingTool: AgentTool = {
535
- name: 'failing_tool',
536
- description: 'A tool that fails',
537
- label: 'Failing',
538
- parameters: Type.Object({}),
539
- async execute() {
540
- throw new Error('Tool execution failed');
541
- },
542
- };
543
-
544
- const prompt: UserMessage = {
545
- role: 'user',
546
- content: [{ type: 'text', content: 'Use failing tool' }],
547
- timestamp: Date.now(),
548
- };
549
-
550
- const context: AgentContext = {
551
- messages: [],
552
- tools: [failingTool],
553
- };
554
-
555
- const assistantMessage1 = createMockAssistantMessage(
556
- [{ type: 'toolCall', name: 'failing_tool', arguments: {}, id: 'call_1' }],
557
- 'toolUse'
558
- );
559
-
560
- const assistantMessage2 = createMockAssistantMessage([
561
- { type: 'text', text: 'Error handled' },
562
- ]);
563
-
564
- let callCount = 0;
565
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
566
- callCount++;
567
- if (callCount === 1) {
568
- return createMockStream(
569
- [{ type: 'done', reason: 'toolUse', message: assistantMessage1 }],
570
- createNativeMessage(assistantMessage1)
571
- );
572
- } else {
573
- return createMockStream(
574
- [{ type: 'done', reason: 'stop', message: assistantMessage2 }],
575
- createNativeMessage(assistantMessage2)
576
- );
577
- }
578
- });
579
-
580
- const config: AgentLoopConfig<'openai'> = {
581
- model: mockModel,
582
- providerOptions: {},
583
- };
584
-
585
- const eventStream = agentLoop(prompt, context, config);
586
- const events: AgentEvent[] = [];
587
-
588
- for await (const event of eventStream) {
589
- events.push(event);
590
- }
591
-
592
- const toolExecutionEnd = events.find(e => e.type === 'tool_execution_end') as any;
593
- expect(toolExecutionEnd.isError).toBe(true);
594
- expect(toolExecutionEnd.result).toBe('Tool execution failed');
595
- });
596
-
597
- it('should create tool result with error details', async () => {
598
- const failingTool: AgentTool = {
599
- name: 'error_tool',
600
- description: 'Tool with error',
601
- label: 'Error',
602
- parameters: Type.Object({}),
603
- async execute() {
604
- const error = new Error('Custom error');
605
- error.name = 'CustomError';
606
- throw error;
607
- },
608
- };
609
-
610
- const prompt: UserMessage = {
611
- role: 'user',
612
- content: [{ type: 'text', content: 'Test' }],
613
- timestamp: Date.now(),
614
- };
615
-
616
- const context: AgentContext = {
617
- messages: [],
618
- tools: [failingTool],
619
- };
620
-
621
- const assistantMessage1 = createMockAssistantMessage(
622
- [{ type: 'toolCall', name: 'error_tool', arguments: {}, id: 'call_1' }],
623
- 'toolUse'
624
- );
625
-
626
- const assistantMessage2 = createMockAssistantMessage([
627
- { type: 'text', text: 'Done' },
628
- ]);
629
-
630
- let callCount = 0;
631
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
632
- callCount++;
633
- if (callCount === 1) {
634
- return createMockStream(
635
- [{ type: 'done', reason: 'toolUse', message: assistantMessage1 }],
636
- createNativeMessage(assistantMessage1)
637
- );
638
- } else {
639
- return createMockStream(
640
- [{ type: 'done', reason: 'stop', message: assistantMessage2 }],
641
- createNativeMessage(assistantMessage2)
642
- );
643
- }
644
- });
645
-
646
- const config: AgentLoopConfig<'openai'> = {
647
- model: mockModel,
648
- providerOptions: {},
649
- };
650
-
651
- const eventStream = agentLoop(prompt, context, config);
652
- const messages = await eventStream.result();
653
-
654
- const toolResult = messages.find(m => m.role === 'toolResult') as any;
655
- expect(toolResult.isError).toBe(true);
656
- expect(toolResult.error).toMatchObject({
657
- message: 'Custom error',
658
- name: 'CustomError',
659
- });
660
- expect(toolResult.error.stack).toBeDefined();
661
- });
662
- });
663
-
664
- describe('agentLoop - Multi-turn Conversation', () => {
665
- it('should handle multiple tool-using turns', async () => {
666
- const tool: AgentTool = {
667
- name: 'count',
668
- description: 'Count',
669
- label: 'Count',
670
- parameters: Type.Object({}),
671
- async execute() {
672
- return {
673
- content: [{ type: 'text', content: 'counted' }],
674
- details: {},
675
- };
676
- },
677
- };
678
-
679
- const prompt: UserMessage = {
680
- role: 'user',
681
- content: [{ type: 'text', content: 'Count twice' }],
682
- timestamp: Date.now(),
683
- };
684
-
685
- const context: AgentContext = {
686
- messages: [],
687
- tools: [tool],
688
- };
689
-
690
- const messages = [
691
- createMockAssistantMessage(
692
- [{ type: 'toolCall', name: 'count', arguments: {}, id: 'call_1' }],
693
- 'toolUse'
694
- ),
695
- createMockAssistantMessage(
696
- [{ type: 'toolCall', name: 'count', arguments: {}, id: 'call_2' }],
697
- 'toolUse'
698
- ),
699
- createMockAssistantMessage([{ type: 'text', text: 'Done counting twice' }]),
700
- ];
701
-
702
- let callCount = 0;
703
- vi.spyOn(streamModule, 'stream').mockImplementation(() => {
704
- const msg = messages[callCount];
705
- callCount++;
706
- return createMockStream(
707
- [{ type: 'done', reason: msg.stopReason, message: msg }],
708
- createNativeMessage(msg)
709
- );
710
- });
711
-
712
- const config: AgentLoopConfig<'openai'> = {
713
- model: mockModel,
714
- providerOptions: {},
715
- };
716
-
717
- const eventStream = agentLoop(prompt, context, config);
718
- const result = await eventStream.result();
719
-
720
- // prompt + assistant1 + tool1 + assistant2 + tool2 + assistant3 = 6
721
- expect(result).toHaveLength(6);
722
- expect(streamModule.stream).toHaveBeenCalledTimes(3);
723
- });
724
-
725
- it('should accumulate context across turns', async () => {
726
- const tool: AgentTool = {
727
- name: 'test',
728
- description: 'Test',
729
- label: 'Test',
730
- parameters: Type.Object({}),
731
- async execute() {
732
- return {
733
- content: [{ type: 'text', content: 'result' }],
734
- details: {},
735
- };
736
- },
737
- };
738
-
739
- const prompt: UserMessage = {
740
- role: 'user',
741
- content: [{ type: 'text', content: 'Test' }],
742
- timestamp: Date.now(),
743
- };
744
-
745
- const context: AgentContext = {
746
- messages: [],
747
- tools: [tool],
748
- };
749
-
750
- const assistantMessage1 = createMockAssistantMessage(
751
- [{ type: 'toolCall', name: 'test', arguments: {}, id: 'call_1' }],
752
- 'toolUse'
753
- );
754
-
755
- const assistantMessage2 = createMockAssistantMessage([
756
- { type: 'text', text: 'Final response' },
757
- ]);
758
-
759
- const streamSpy = vi.spyOn(streamModule, 'stream');
760
-
761
- // Use mockReturnValueOnce for sequential calls
762
- streamSpy
763
- .mockReturnValueOnce(
764
- createMockStream(
765
- [{ type: 'done', reason: 'toolUse', message: assistantMessage1 }],
766
- createNativeMessage(assistantMessage1)
767
- )
768
- )
769
- .mockReturnValueOnce(
770
- createMockStream(
771
- [{ type: 'done', reason: 'stop', message: assistantMessage2 }],
772
- createNativeMessage(assistantMessage2)
773
- )
774
- );
775
-
776
- const config: AgentLoopConfig<'openai'> = {
777
- model: mockModel,
778
- providerOptions: {},
779
- };
780
-
781
- const eventStream = agentLoop(prompt, context, config);
782
- await eventStream.result();
783
-
784
- // Verify the stream was called twice
785
- expect(streamSpy).toHaveBeenCalledTimes(2);
786
-
787
- // Verify first call had prompt only
788
- expect(streamSpy.mock.calls[0][1].messages).toHaveLength(1);
789
-
790
- // Verify second call had prompt + assistant1 + tool result
791
- expect(streamSpy.mock.calls[1][1].messages).toHaveLength(3);
792
- });
793
- });
794
-
795
- describe('agentLoop - Error Handling', () => {
796
- it('should handle abort signal during streaming', async () => {
797
- const prompt: UserMessage = {
798
- role: 'user',
799
- content: [{ type: 'text', content: 'Test' }],
800
- timestamp: Date.now(),
801
- };
802
-
803
- const context: AgentContext = {
804
- messages: [],
805
- };
806
-
807
- const assistantMessage = createMockAssistantMessage([], 'aborted');
808
-
809
- const mockStream = createMockStream(
810
- [{ type: 'error', reason: 'aborted', error: assistantMessage }],
811
- createNativeMessage(assistantMessage)
812
- );
813
-
814
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
815
-
816
- const config: AgentLoopConfig<'openai'> = {
817
- model: mockModel,
818
- providerOptions: {},
819
- };
820
-
821
- const abortController = new AbortController();
822
- const eventStream = agentLoop(prompt, context, config, abortController.signal);
823
-
824
- const events: AgentEvent[] = [];
825
- for await (const event of eventStream) {
826
- events.push(event);
827
- }
828
-
829
- expect(events).toContainEqual(
830
- expect.objectContaining({ type: 'agent_end', status: 'aborted' })
831
- );
832
- });
833
-
834
- it('should handle error stop reason', async () => {
835
- const prompt: UserMessage = {
836
- role: 'user',
837
- content: [{ type: 'text', content: 'Test' }],
838
- timestamp: Date.now(),
839
- };
840
-
841
- const context: AgentContext = {
842
- messages: [],
843
- };
844
-
845
- const assistantMessage = createMockAssistantMessage([], 'error');
846
- assistantMessage.errorMessage = 'API Error';
847
-
848
- const mockStream = createMockStream(
849
- [{ type: 'error', reason: 'error', error: assistantMessage }],
850
- createNativeMessage(assistantMessage)
851
- );
852
-
853
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
854
-
855
- const config: AgentLoopConfig<'openai'> = {
856
- model: mockModel,
857
- providerOptions: {},
858
- };
859
-
860
- const eventStream = agentLoop(prompt, context, config);
861
- const events: AgentEvent[] = [];
862
-
863
- for await (const event of eventStream) {
864
- events.push(event);
865
- }
866
-
867
- expect(events).toContainEqual(
868
- expect.objectContaining({ type: 'agent_end', status: 'error' })
869
- );
870
- });
871
- });
872
-
873
- describe('agentLoop - Message Events', () => {
874
- it('should emit message_start and message_end for prompt', async () => {
875
- const prompt: UserMessage = {
876
- role: 'user',
877
- content: [{ type: 'text', content: 'Hello' }],
878
- timestamp: Date.now(),
879
- };
880
-
881
- const context: AgentContext = {
882
- messages: [],
883
- };
884
-
885
- const assistantMessage = createMockAssistantMessage([
886
- { type: 'text', text: 'Hi' },
887
- ]);
888
-
889
- const mockStream = createMockStream(
890
- [{ type: 'done', reason: 'stop', message: assistantMessage }],
891
- createNativeMessage(assistantMessage)
892
- );
893
-
894
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
895
-
896
- const config: AgentLoopConfig<'openai'> = {
897
- model: mockModel,
898
- providerOptions: {},
899
- };
900
-
901
- const eventStream = agentLoop(prompt, context, config);
902
- const events: AgentEvent[] = [];
903
-
904
- for await (const event of eventStream) {
905
- events.push(event);
906
- }
907
-
908
- const messageStarts = events.filter(e => e.type === 'message_start');
909
- const messageEnds = events.filter(e => e.type === 'message_end');
910
-
911
- expect(messageStarts.length).toBeGreaterThan(0);
912
- expect(messageEnds.length).toBeGreaterThan(0);
913
- });
914
-
915
- it('should emit message_update events during streaming', async () => {
916
- const prompt: UserMessage = {
917
- role: 'user',
918
- content: [{ type: 'text', content: 'Hello' }],
919
- timestamp: Date.now(),
920
- };
921
-
922
- const context: AgentContext = {
923
- messages: [],
924
- };
925
-
926
- const assistantMessage = createMockAssistantMessage([
927
- { type: 'text', text: 'Response' },
928
- ]);
929
-
930
- const mockStream = createMockStream(
931
- [
932
- { type: 'start', partial: assistantMessage },
933
- { type: 'text_start', contentIndex: 0, partial: assistantMessage },
934
- { type: 'text_delta', contentIndex: 0, delta: 'R', partial: assistantMessage },
935
- { type: 'text_delta', contentIndex: 0, delta: 'e', partial: assistantMessage },
936
- { type: 'done', reason: 'stop', message: assistantMessage },
937
- ],
938
- createNativeMessage(assistantMessage)
939
- );
940
-
941
- vi.spyOn(streamModule, 'stream').mockReturnValue(mockStream);
942
-
943
- const config: AgentLoopConfig<'openai'> = {
944
- model: mockModel,
945
- providerOptions: {},
946
- };
947
-
948
- const eventStream = agentLoop(prompt, context, config);
949
- const events: AgentEvent[] = [];
950
-
951
- for await (const event of eventStream) {
952
- events.push(event);
953
- }
954
-
955
- const messageUpdates = events.filter(e => e.type === 'message_update');
956
- expect(messageUpdates.length).toBeGreaterThan(0);
957
- });
958
- });