@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.
- package/package.json +7 -3
- package/src/agent.js +7 -2
- package/src/generate.js +1 -0
- package/src/index.d.ts +14 -6
- package/src/utils/wrap.js +35 -2
- package/src/agent.spec.js +0 -595
- package/src/ai_provider.spec.js +0 -217
- package/src/ai_sdk_options.spec.js +0 -390
- package/src/fixtures/image_response_v7_google_vertex.js +0 -72
- package/src/fixtures/image_response_v7_openai.js +0 -77
- package/src/fixtures/models_api_light.json +0 -675
- package/src/fixtures/models_api_v7_subset.json +0 -259
- package/src/fixtures/stream_response_v7_anthropic.js +0 -389
- package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
- package/src/fixtures/stream_response_v7_openai.js +0 -353
- package/src/fixtures/stream_response_v7_perplexity.js +0 -671
- package/src/fixtures/text_response_v7_anthropic.js +0 -359
- package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
- package/src/fixtures/text_response_v7_google_vertex.js +0 -435
- package/src/fixtures/text_response_v7_openai.js +0 -347
- package/src/fixtures/text_response_v7_perplexity.js +0 -764
- package/src/generate.spec.js +0 -525
- package/src/prompt/content.spec.js +0 -208
- package/src/prompt/interpolations.spec.js +0 -109
- package/src/prompt/loader.full.spec.js +0 -309
- package/src/prompt/loader.spec.js +0 -258
- package/src/prompt/markup/attributes.spec.js +0 -132
- package/src/prompt/markup/nodes.spec.js +0 -333
- package/src/prompt/markup/tokenizer.spec.js +0 -149
- package/src/prompt/markup/tokens.spec.js +0 -250
- package/src/prompt/validations.spec.js +0 -812
- package/src/utils/cost.full.spec.js +0 -272
- package/src/utils/cost.spec.js +0 -290
- package/src/utils/error_handler.spec.js +0 -230
- package/src/utils/file.spec.js +0 -89
- package/src/utils/image.spec.js +0 -20
- package/src/utils/legacy_cost_attribute.spec.js +0 -278
- package/src/utils/models.spec.js +0 -119
- package/src/utils/models_pricing.spec.js +0 -162
- package/src/utils/skills.spec.js +0 -168
- package/src/utils/sources.spec.js +0 -122
- package/src/utils/stream.spec.js +0 -55
- package/src/utils/tools.spec.js +0 -167
- package/src/utils/usage.full.spec.js +0 -215
- package/src/utils/usage.spec.js +0 -246
- package/src/utils/wrap.spec.js +0 -467
- 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
|
-
} );
|