@outputai/llm 0.11.1-next.da26845.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +7 -3
  2. package/src/agent.js +7 -2
  3. package/src/generate.js +1 -0
  4. package/src/index.d.ts +14 -6
  5. package/src/utils/wrap.js +35 -2
  6. package/src/agent.spec.js +0 -595
  7. package/src/ai_provider.spec.js +0 -217
  8. package/src/ai_sdk_options.spec.js +0 -390
  9. package/src/fixtures/image_response_v7_google_vertex.js +0 -72
  10. package/src/fixtures/image_response_v7_openai.js +0 -77
  11. package/src/fixtures/models_api_light.json +0 -675
  12. package/src/fixtures/models_api_v7_subset.json +0 -259
  13. package/src/fixtures/stream_response_v7_anthropic.js +0 -389
  14. package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
  15. package/src/fixtures/stream_response_v7_openai.js +0 -353
  16. package/src/fixtures/stream_response_v7_perplexity.js +0 -671
  17. package/src/fixtures/text_response_v7_anthropic.js +0 -359
  18. package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
  19. package/src/fixtures/text_response_v7_google_vertex.js +0 -435
  20. package/src/fixtures/text_response_v7_openai.js +0 -347
  21. package/src/fixtures/text_response_v7_perplexity.js +0 -764
  22. package/src/generate.spec.js +0 -525
  23. package/src/prompt/content.spec.js +0 -208
  24. package/src/prompt/interpolations.spec.js +0 -109
  25. package/src/prompt/loader.full.spec.js +0 -309
  26. package/src/prompt/loader.spec.js +0 -258
  27. package/src/prompt/markup/attributes.spec.js +0 -132
  28. package/src/prompt/markup/nodes.spec.js +0 -333
  29. package/src/prompt/markup/tokenizer.spec.js +0 -149
  30. package/src/prompt/markup/tokens.spec.js +0 -250
  31. package/src/prompt/validations.spec.js +0 -812
  32. package/src/utils/cost.full.spec.js +0 -272
  33. package/src/utils/cost.spec.js +0 -290
  34. package/src/utils/error_handler.spec.js +0 -230
  35. package/src/utils/file.spec.js +0 -89
  36. package/src/utils/image.spec.js +0 -20
  37. package/src/utils/legacy_cost_attribute.spec.js +0 -278
  38. package/src/utils/models.spec.js +0 -119
  39. package/src/utils/models_pricing.spec.js +0 -162
  40. package/src/utils/skills.spec.js +0 -168
  41. package/src/utils/sources.spec.js +0 -122
  42. package/src/utils/stream.spec.js +0 -55
  43. package/src/utils/tools.spec.js +0 -167
  44. package/src/utils/usage.full.spec.js +0 -215
  45. package/src/utils/usage.spec.js +0 -246
  46. package/src/utils/wrap.spec.js +0 -467
  47. package/src/validations.spec.js +0 -631
package/src/agent.spec.js DELETED
@@ -1,595 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const aiMocks = vi.hoisted( () => ( {
4
- superConstructor: vi.fn(),
5
- superGenerate: vi.fn(),
6
- superStream: vi.fn()
7
- } ) );
8
-
9
- const validations = vi.hoisted( () => ( {
10
- parseAgentArgs: vi.fn(),
11
- parseAgentGenerateArgs: vi.fn(),
12
- parseAgentGenerateWithStreamingArgs: vi.fn(),
13
- parseAgentStreamArgs: vi.fn()
14
- } ) );
15
-
16
- const toPromptFileArgs = ( { prompt, ...rest } ) => ( {
17
- promptFile: prompt,
18
- ...rest
19
- } );
20
-
21
- const toAgentMethodArgs = ( args = {} ) => ( {
22
- messages: [],
23
- ...args
24
- } );
25
-
26
- const promptMocks = vi.hoisted( () => ( {
27
- loadPrompt: vi.fn()
28
- } ) );
29
-
30
- const skillMocks = vi.hoisted( () => ( {
31
- loadSkills: vi.fn()
32
- } ) );
33
-
34
- const optionMocks = vi.hoisted( () => ( {
35
- loadAiSdkTextOptions: vi.fn()
36
- } ) );
37
-
38
- const wrapMocks = vi.hoisted( () => ( {
39
- wrapGeneration: vi.fn(),
40
- wrapStream: vi.fn(),
41
- streamHooks: { onEndHook: vi.fn(), onErrorHook: vi.fn() }
42
- } ) );
43
-
44
- const streamMocks = vi.hoisted( () => ( {
45
- drainStream: vi.fn()
46
- } ) );
47
-
48
- const loggerMocks = vi.hoisted( () => ( {
49
- error: vi.fn()
50
- } ) );
51
-
52
- vi.mock( 'ai', () => {
53
- class MockToolLoopAgent {
54
- constructor( options ) {
55
- aiMocks.superConstructor( options );
56
- }
57
-
58
- async generate( ...args ) {
59
- return aiMocks.superGenerate( ...args );
60
- }
61
-
62
- stream( ...args ) {
63
- return aiMocks.superStream( ...args );
64
- }
65
- }
66
-
67
- return { ToolLoopAgent: MockToolLoopAgent };
68
- } );
69
-
70
- vi.mock( './validations.js', () => validations );
71
-
72
- vi.mock( '@outputai/core', () => ( {
73
- Logger: {
74
- error: ( ...args ) => loggerMocks.error( ...args )
75
- }
76
- } ) );
77
-
78
- vi.mock( './prompt/loader.js', () => ( {
79
- loadPrompt: ( ...args ) => promptMocks.loadPrompt( ...args )
80
- } ) );
81
-
82
- vi.mock( './utils/skills.js', () => ( {
83
- loadSkills: ( ...args ) => skillMocks.loadSkills( ...args )
84
- } ) );
85
-
86
- vi.mock( './ai_sdk_options.js', () => ( {
87
- loadAiSdkTextOptions: ( ...args ) => optionMocks.loadAiSdkTextOptions( ...args )
88
- } ) );
89
-
90
- vi.mock( './utils/wrap.js', () => ( {
91
- wrapGeneration: ( ...args ) => wrapMocks.wrapGeneration( ...args ),
92
- wrapStream: ( ...args ) => wrapMocks.wrapStream( ...args )
93
- } ) );
94
-
95
- vi.mock( './utils/stream.js', () => ( {
96
- drainStream: ( ...args ) => streamMocks.drainStream( ...args )
97
- } ) );
98
-
99
- const importSut = async () => import( './agent.js' );
100
-
101
- const loadedPrompt = {
102
- name: 'test@v1',
103
- config: { provider: 'openai', model: 'test-model', maxSteps: 10 },
104
- messages: [
105
- { role: 'system', content: 'You are concise.' },
106
- { role: 'user', content: 'Initial user message' }
107
- ]
108
- };
109
-
110
- const loadedSkills = [ { name: 'writer', description: 'Writes', instructions: 'Do it.' } ];
111
-
112
- const model = { id: 'MODEL' };
113
-
114
- const textOptions = {
115
- model,
116
- instructions: [ { role: 'system', content: 'You are concise.' } ],
117
- messages: [ { role: 'user', content: 'Initial user message' } ],
118
- providerOptions: { test: true },
119
- temperature: 0.3
120
- };
121
-
122
- const assistantMessage = { role: 'assistant', content: 'response' };
123
-
124
- const aiResponse = {
125
- text: 'response',
126
- finishReason: 'stop',
127
- responseMessages: [ assistantMessage ]
128
- };
129
-
130
- describe( 'Agent', () => {
131
- beforeEach( () => {
132
- aiMocks.superConstructor.mockReset();
133
- aiMocks.superGenerate.mockReset().mockResolvedValue( aiResponse );
134
- aiMocks.superStream.mockReset().mockReturnValue( { textStream: 'stream' } );
135
-
136
- validations.parseAgentArgs.mockReset().mockImplementation( toPromptFileArgs );
137
- validations.parseAgentGenerateArgs.mockReset().mockImplementation( toAgentMethodArgs );
138
- validations.parseAgentGenerateWithStreamingArgs.mockReset().mockImplementation( toAgentMethodArgs );
139
- validations.parseAgentStreamArgs.mockReset().mockImplementation( toAgentMethodArgs );
140
-
141
- promptMocks.loadPrompt.mockReset().mockReturnValue( loadedPrompt );
142
- skillMocks.loadSkills.mockReset().mockReturnValue( loadedSkills );
143
-
144
- optionMocks.loadAiSdkTextOptions.mockReset().mockReturnValue( textOptions );
145
-
146
- wrapMocks.wrapGeneration.mockReset().mockImplementation( async ( { fn } ) => fn() );
147
- wrapMocks.streamHooks = {
148
- onEndHook: vi.fn( async ( response, callback ) => callback?.( response ) ),
149
- onErrorHook: vi.fn( ( event, callback ) => callback?.( event.error ) )
150
- };
151
- wrapMocks.wrapStream.mockReset().mockImplementation( ( { fn } ) => fn( wrapMocks.streamHooks ) );
152
- streamMocks.drainStream.mockReset().mockResolvedValue( undefined );
153
- loggerMocks.error.mockReset();
154
- } );
155
-
156
- afterEach( async () => {
157
- await vi.resetModules();
158
- } );
159
-
160
- it( 'propagates parse errors before loading the prompt', async () => {
161
- const validationError = new Error( 'Invalid Agent() arguments' );
162
- validations.parseAgentArgs.mockImplementationOnce( () => {
163
- throw validationError;
164
- } );
165
- const { Agent } = await importSut();
166
-
167
- expect( () => new Agent( {} ) ).toThrow( validationError );
168
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
169
- expect( skillMocks.loadSkills ).not.toHaveBeenCalled();
170
- expect( aiMocks.superConstructor ).not.toHaveBeenCalled();
171
- } );
172
-
173
- it( 'parses args, loads prompt skills, and constructs ToolLoopAgent', async () => {
174
- const { Agent } = await importSut();
175
- const tools = { search: { description: 'Search' } };
176
- const output = { type: 'object' };
177
- const stopWhen = { type: 'custom-stop' };
178
- const messageStore = { getMessages() {}, addMessages() {} };
179
-
180
- new Agent( {
181
- prompt: 'test@v1',
182
- variables: { tone: 'brief' },
183
- promptDir: '/prompts',
184
- tools,
185
- output,
186
- stopWhen,
187
- messageStore
188
- } );
189
-
190
- expect( validations.parseAgentArgs ).toHaveBeenCalledWith( {
191
- prompt: 'test@v1',
192
- variables: { tone: 'brief' },
193
- promptDir: '/prompts',
194
- tools,
195
- output,
196
- stopWhen,
197
- messageStore
198
- } );
199
- expect( promptMocks.loadPrompt ).toHaveBeenCalledWith( 'test@v1', { tone: 'brief' }, '/prompts' );
200
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
201
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
202
- prompt: loadedPrompt,
203
- skills: loadedSkills,
204
- tools,
205
- output,
206
- stopWhen
207
- } );
208
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith( {
209
- model,
210
- providerOptions: { test: true },
211
- temperature: 0.3,
212
- instructions: [ { role: 'system', content: 'You are concise.' } ]
213
- } );
214
- } );
215
-
216
- it( 'uses a prompt object without loading a prompt file', async () => {
217
- validations.parseAgentArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
218
- const { Agent } = await importSut();
219
-
220
- new Agent( { prompt: loadedPrompt } );
221
-
222
- expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
223
- expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
224
- expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
225
- prompt: loadedPrompt,
226
- skills: loadedSkills,
227
- tools: undefined,
228
- output: undefined,
229
- stopWhen: undefined
230
- } );
231
- } );
232
-
233
- it( 'passes option tools through to ToolLoopAgent', async () => {
234
- const { Agent } = await importSut();
235
- optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
236
- ...textOptions,
237
- tools: { load_skill: { description: 'Load skill' } }
238
- } );
239
-
240
- new Agent( { prompt: 'test@v1' } );
241
-
242
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
243
- expect.objectContaining( {
244
- tools: { load_skill: { description: 'Load skill' } }
245
- } )
246
- );
247
- } );
248
-
249
- it( 'preserves per-message providerOptions on system messages passed as instructions', async () => {
250
- const { Agent } = await importSut();
251
- const systemMessage = {
252
- role: 'system',
253
- content: 'You are concise.',
254
- providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
255
- };
256
- optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
257
- model,
258
- instructions: [ systemMessage ],
259
- messages: [ { role: 'user', content: 'Hello' } ]
260
- } );
261
-
262
- new Agent( { prompt: 'test@v1' } );
263
-
264
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
265
- expect.objectContaining( {
266
- instructions: [ systemMessage ]
267
- } )
268
- );
269
- } );
270
-
271
- it( 'omits instructions when there is no system message', async () => {
272
- const { Agent } = await importSut();
273
- optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
274
- model,
275
- instructions: [],
276
- messages: [ { role: 'user', content: 'Hello' } ]
277
- } );
278
-
279
- new Agent( { prompt: 'test@v1' } );
280
-
281
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
282
- expect.not.objectContaining( { instructions: expect.anything() } )
283
- );
284
- } );
285
-
286
- it( 'keeps only user prompt messages as initial generate messages', async () => {
287
- const { Agent } = await importSut();
288
- const agent = new Agent( { prompt: 'test@v1' } );
289
-
290
- await agent.generate();
291
-
292
- expect( validations.parseAgentGenerateArgs ).toHaveBeenCalledWith( undefined );
293
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
294
- name: 'Agent.generate',
295
- prompt: loadedPrompt,
296
- fn: expect.any( Function )
297
- } );
298
- expect( aiMocks.superGenerate ).toHaveBeenCalledWith( {
299
- messages: [ { role: 'user', content: 'Initial user message' } ],
300
- allowSystemInMessages: true
301
- } );
302
- } );
303
-
304
- it( 'excludes authored assistant blocks from the initial generate messages', async () => {
305
- const { Agent } = await importSut();
306
- optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
307
- model,
308
- instructions: [ { role: 'system', content: 'You are concise.' } ],
309
- messages: [
310
- { role: 'user', content: 'Initial user message' },
311
- { role: 'assistant', content: 'Authored assistant block' }
312
- ]
313
- } );
314
- const agent = new Agent( { prompt: 'test@v1' } );
315
-
316
- await agent.generate();
317
-
318
- expect( aiMocks.superGenerate ).toHaveBeenCalledWith( {
319
- messages: [ { role: 'user', content: 'Initial user message' } ],
320
- allowSystemInMessages: true
321
- } );
322
- } );
323
-
324
- it( 'combines initial, stored, and caller messages and forwards generate options', async () => {
325
- const store = {
326
- getMessages: vi.fn( () => [
327
- { role: 'assistant', content: 'Stored reply' }
328
- ] ),
329
- addMessages: vi.fn()
330
- };
331
- const callerMessage = { role: 'user', content: 'New question' };
332
- const abortSignal = AbortSignal.abort();
333
- const { Agent } = await importSut();
334
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
335
-
336
- await agent.generate( {
337
- messages: [ callerMessage ],
338
- abortSignal,
339
- toolChoice: 'required'
340
- } );
341
-
342
- expect( validations.parseAgentGenerateArgs ).toHaveBeenCalledWith( {
343
- messages: [ callerMessage ],
344
- abortSignal,
345
- toolChoice: 'required'
346
- } );
347
- expect( aiMocks.superGenerate ).toHaveBeenCalledWith( {
348
- messages: [
349
- { role: 'user', content: 'Initial user message' },
350
- { role: 'assistant', content: 'Stored reply' },
351
- callerMessage
352
- ],
353
- allowSystemInMessages: true,
354
- abortSignal,
355
- toolChoice: 'required'
356
- } );
357
- } );
358
-
359
- it( 'wraps generate responses and stores the user and response messages', async () => {
360
- const store = {
361
- getMessages: vi.fn( () => [] ),
362
- addMessages: vi.fn()
363
- };
364
- const callerMessage = { role: 'user', content: 'New question' };
365
- const { Agent } = await importSut();
366
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
367
-
368
- const result = await agent.generate( { messages: [ callerMessage ] } );
369
-
370
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
371
- name: 'Agent.generate',
372
- prompt: loadedPrompt,
373
- fn: expect.any( Function )
374
- } );
375
- expect( store.addMessages ).toHaveBeenCalledWith( [
376
- callerMessage,
377
- assistantMessage
378
- ] );
379
- expect( result ).toBe( aiResponse );
380
- } );
381
-
382
- it( 'generates with streaming, stores messages, and returns the completed response', async () => {
383
- const store = {
384
- getMessages: vi.fn( () => [] ),
385
- addMessages: vi.fn()
386
- };
387
- const callerMessage = { role: 'user', content: 'New question' };
388
- const output = { summary: 'Structured result' };
389
- const chunk = { type: 'text-delta', text: 'response' };
390
- const onChunk = vi.fn();
391
- const abortSignal = AbortSignal.abort();
392
- const stream = { output: Promise.resolve( output ) };
393
- const response = { ...aiResponse };
394
- aiMocks.superStream.mockImplementationOnce( options => {
395
- options.onChunk( { chunk } );
396
- options.onEnd( response );
397
- return stream;
398
- } );
399
- const { Agent } = await importSut();
400
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
401
-
402
- const result = await agent.generateWithStreaming( {
403
- messages: [ callerMessage ],
404
- onChunk,
405
- abortSignal,
406
- toolChoice: 'required'
407
- } );
408
-
409
- expect( validations.parseAgentGenerateWithStreamingArgs ).toHaveBeenCalledWith( {
410
- messages: [ callerMessage ],
411
- onChunk,
412
- abortSignal,
413
- toolChoice: 'required'
414
- } );
415
- expect( wrapMocks.wrapGeneration ).toHaveBeenCalledWith( {
416
- name: 'Agent.generateWithStreaming',
417
- prompt: loadedPrompt,
418
- fn: expect.any( Function )
419
- } );
420
- expect( aiMocks.superStream ).toHaveBeenCalledWith( {
421
- messages: [
422
- { role: 'user', content: 'Initial user message' },
423
- callerMessage
424
- ],
425
- allowSystemInMessages: true,
426
- onChunk,
427
- abortSignal,
428
- toolChoice: 'required',
429
- onEnd: expect.any( Function ),
430
- onError: expect.any( Function )
431
- } );
432
- expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortSignal );
433
- expect( store.addMessages ).toHaveBeenCalledWith( [
434
- callerMessage,
435
- assistantMessage
436
- ] );
437
- expect( onChunk ).toHaveBeenCalledWith( { chunk } );
438
- expect( result ).toEqual( { ...aiResponse, output } );
439
- } );
440
-
441
- it( 'omits onChunk when generateWithStreaming does not receive it', async () => {
442
- const stream = { output: Promise.resolve( undefined ) };
443
- aiMocks.superStream.mockImplementationOnce( options => {
444
- options.onEnd( { ...aiResponse } );
445
- return stream;
446
- } );
447
- const { Agent } = await importSut();
448
- const agent = new Agent( { prompt: 'test@v1' } );
449
-
450
- await agent.generateWithStreaming();
451
- const callOptions = aiMocks.superStream.mock.calls[0][0];
452
-
453
- expect( callOptions ).not.toHaveProperty( 'onChunk' );
454
- } );
455
-
456
- it( 'rejects generateWithStreaming with the abort reason', async () => {
457
- const abortController = new AbortController();
458
- const abortReason = new Error( 'Cancelled by caller' );
459
- const stream = { output: Promise.resolve( undefined ) };
460
- abortController.abort( abortReason );
461
- streamMocks.drainStream.mockRejectedValueOnce( abortReason );
462
- aiMocks.superStream.mockReturnValueOnce( stream );
463
- const { Agent } = await importSut();
464
- const agent = new Agent( { prompt: 'test@v1' } );
465
-
466
- await expect( agent.generateWithStreaming( {
467
- abortSignal: abortController.signal
468
- } ) ).rejects.toBe( abortReason );
469
-
470
- expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortController.signal );
471
- } );
472
-
473
- it( 'streams with initial, stored, and caller messages and stores on end', async () => {
474
- const store = {
475
- getMessages: vi.fn( () => [
476
- { role: 'assistant', content: 'Stored reply' }
477
- ] ),
478
- addMessages: vi.fn()
479
- };
480
- const onEnd = vi.fn();
481
- const onError = vi.fn();
482
- const onChunk = vi.fn();
483
- const callerMessage = { role: 'user', content: 'New question' };
484
- const { Agent } = await importSut();
485
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
486
-
487
- const result = await agent.stream( {
488
- messages: [ callerMessage ],
489
- onEnd,
490
- onError,
491
- onChunk
492
- } );
493
-
494
- expect( validations.parseAgentStreamArgs ).toHaveBeenCalledWith( {
495
- messages: [ callerMessage ],
496
- onEnd,
497
- onError,
498
- onChunk
499
- } );
500
- expect( wrapMocks.wrapStream ).toHaveBeenCalledWith( {
501
- name: 'Agent.stream',
502
- prompt: loadedPrompt,
503
- fn: expect.any( Function )
504
- } );
505
- expect( aiMocks.superStream ).toHaveBeenCalledWith( {
506
- messages: [
507
- { role: 'user', content: 'Initial user message' },
508
- { role: 'assistant', content: 'Stored reply' },
509
- callerMessage
510
- ],
511
- allowSystemInMessages: true,
512
- onChunk,
513
- onEnd: expect.any( Function ),
514
- onError: expect.any( Function )
515
- } );
516
- const streamOptions = aiMocks.superStream.mock.calls[0][0];
517
- await streamOptions.onEnd( aiResponse );
518
- expect( wrapMocks.streamHooks.onEndHook ).toHaveBeenCalledWith( aiResponse, expect.any( Function ) );
519
- expect( store.addMessages ).toHaveBeenCalledWith( [
520
- callerMessage,
521
- assistantMessage
522
- ] );
523
- expect( onEnd ).toHaveBeenCalledWith( aiResponse );
524
- expect( result ).toEqual( { textStream: 'stream' } );
525
- } );
526
-
527
- it( 'logs stream persistence failures and still completes onEnd', async () => {
528
- const persistenceError = new Error( 'Store unavailable' );
529
- const store = {
530
- getMessages: vi.fn( () => [] ),
531
- addMessages: vi.fn().mockRejectedValue( persistenceError )
532
- };
533
- const onEnd = vi.fn();
534
- const { Agent } = await importSut();
535
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
536
-
537
- const result = await agent.stream( { onEnd } );
538
- const streamOptions = aiMocks.superStream.mock.calls[0][0];
539
-
540
- await expect( streamOptions.onEnd( aiResponse ) ).resolves.toBeUndefined();
541
-
542
- expect( loggerMocks.error ).toHaveBeenCalledWith(
543
- 'Agent.stream message store persistence failed',
544
- { namespace: 'LLM', error: persistenceError.message }
545
- );
546
- expect( onEnd ).toHaveBeenCalledWith( aiResponse );
547
- expect( result ).toEqual( { textStream: 'stream' } );
548
- } );
549
-
550
- it( 'does not store stream messages when finishReason is error', async () => {
551
- const store = {
552
- getMessages: vi.fn( () => [] ),
553
- addMessages: vi.fn()
554
- };
555
- const onEnd = vi.fn();
556
- const { Agent } = await importSut();
557
- const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
558
-
559
- await agent.stream( { onEnd } );
560
- const streamOptions = aiMocks.superStream.mock.calls[0][0];
561
- await streamOptions.onEnd( { ...aiResponse, finishReason: 'error' } );
562
-
563
- expect( store.addMessages ).not.toHaveBeenCalled();
564
- expect( onEnd ).toHaveBeenCalledWith( { ...aiResponse, finishReason: 'error' } );
565
- } );
566
-
567
- it( 'omits onChunk when stream does not receive it', async () => {
568
- const { Agent } = await importSut();
569
- const agent = new Agent( { prompt: 'test@v1' } );
570
-
571
- await agent.stream();
572
- const callOptions = aiMocks.superStream.mock.calls[0][0];
573
-
574
- expect( callOptions ).not.toHaveProperty( 'onChunk' );
575
- } );
576
-
577
- it( 'forwards stream onError through wrapStream with the mapped event payload', async () => {
578
- const onError = vi.fn();
579
- const error = new Error( 'Stream failed' );
580
- const mappedError = new Error( 'Mapped stream failed' );
581
- const { Agent } = await importSut();
582
- const agent = new Agent( { prompt: 'test@v1' } );
583
-
584
- await agent.stream( { onError } );
585
- const streamOptions = aiMocks.superStream.mock.calls[0][0];
586
- streamOptions.onError( { error, extra: true } );
587
-
588
- expect( wrapMocks.streamHooks.onErrorHook ).toHaveBeenCalledWith(
589
- { error, extra: true },
590
- expect.any( Function )
591
- );
592
- wrapMocks.streamHooks.onErrorHook.mock.calls[0][1]( mappedError );
593
- expect( onError ).toHaveBeenCalledWith( { error: mappedError, extra: true } );
594
- } );
595
- } );