@outputai/llm 0.11.0 → 0.11.1-next.2223fa5.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 (69) hide show
  1. package/package.json +2 -2
  2. package/src/agent.js +105 -83
  3. package/src/agent.spec.js +312 -225
  4. package/src/ai_provider.js +2 -2
  5. package/src/ai_sdk_options.js +93 -39
  6. package/src/ai_sdk_options.spec.js +314 -154
  7. package/src/consts.js +6 -0
  8. package/src/generate.js +81 -0
  9. package/src/generate.spec.js +466 -0
  10. package/src/index.d.ts +222 -242
  11. package/src/index.js +3 -4
  12. package/src/prompt/content.js +70 -0
  13. package/src/prompt/content.spec.js +208 -0
  14. package/src/prompt/{escape.js → interpolations.js} +11 -18
  15. package/src/prompt/interpolations.spec.js +109 -0
  16. package/src/prompt/loader.full.spec.js +293 -0
  17. package/src/prompt/loader.js +45 -39
  18. package/src/prompt/loader.spec.js +186 -289
  19. package/src/prompt/markup/attributes.js +52 -0
  20. package/src/prompt/markup/attributes.spec.js +132 -0
  21. package/src/prompt/markup/nodes.js +71 -0
  22. package/src/prompt/markup/nodes.spec.js +333 -0
  23. package/src/prompt/markup/tokenizer.js +3 -0
  24. package/src/prompt/markup/tokenizer.spec.js +149 -0
  25. package/src/prompt/markup/tokens.js +26 -0
  26. package/src/prompt/markup/tokens.spec.js +250 -0
  27. package/src/prompt/validations.js +98 -67
  28. package/src/prompt/validations.spec.js +204 -47
  29. package/src/{prompt/load_content.js → utils/file.js} +12 -15
  30. package/src/utils/file.spec.js +89 -0
  31. package/src/utils/models.js +15 -0
  32. package/src/utils/models.spec.js +119 -0
  33. package/src/utils/skills.js +74 -0
  34. package/src/utils/skills.spec.js +168 -0
  35. package/src/utils/sources.js +48 -0
  36. package/src/utils/sources.spec.js +122 -0
  37. package/src/utils/stream.js +22 -0
  38. package/src/utils/stream.spec.js +55 -0
  39. package/src/utils/tools.js +47 -0
  40. package/src/utils/tools.spec.js +167 -0
  41. package/src/utils/wrap.js +148 -0
  42. package/src/utils/wrap.spec.js +359 -0
  43. package/src/validations.js +149 -27
  44. package/src/validations.spec.js +451 -53
  45. package/src/ai_model.js +0 -60
  46. package/src/ai_model.spec.js +0 -259
  47. package/src/ai_sdk.js +0 -92
  48. package/src/ai_sdk.spec.js +0 -564
  49. package/src/prompt/block_options.js +0 -58
  50. package/src/prompt/block_options.spec.js +0 -71
  51. package/src/prompt/blocks.js +0 -47
  52. package/src/prompt/blocks.spec.js +0 -63
  53. package/src/prompt/escape.spec.js +0 -159
  54. package/src/prompt/load_content.spec.js +0 -83
  55. package/src/prompt/loader_validation.spec.js +0 -128
  56. package/src/prompt/parser.js +0 -16
  57. package/src/prompt/parser.spec.js +0 -186
  58. package/src/prompt/prepare_text.js +0 -27
  59. package/src/prompt/prepare_text.spec.js +0 -141
  60. package/src/prompt/skill.js +0 -128
  61. package/src/prompt/skill.spec.js +0 -172
  62. package/src/utils/message.js +0 -3
  63. package/src/utils/message.spec.js +0 -29
  64. package/src/utils/response_wrappers.js +0 -100
  65. package/src/utils/response_wrappers.spec.js +0 -240
  66. package/src/utils/source_extraction.js +0 -53
  67. package/src/utils/source_extraction.spec.js +0 -194
  68. package/src/utils/trace.js +0 -19
  69. package/src/utils/trace.spec.js +0 -112
@@ -1,101 +1,310 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { ValidationError } from '@outputai/core';
3
- import { validateGenerateTextArgs, validateStreamTextArgs, validateGenerateImageArgs } from './validations.js';
3
+ import {
4
+ parseGenerateTextArgs,
5
+ parseGenerateTextWithStreamingArgs,
6
+ parseStreamTextArgs,
7
+ parseGenerateImageArgs,
8
+ parseAgentArgs,
9
+ parseAgentGenerateArgs,
10
+ parseAgentGenerateWithStreamingArgs,
11
+ parseAgentStreamArgs
12
+ } from './validations.js';
4
13
 
5
- describe( 'validateGenerateTextArgs', () => {
6
- it( 'accepts a prompt with optional variables', () => {
7
- expect( () => validateGenerateTextArgs( {
14
+ const makeOutput = () => ( { name: 'object' } );
15
+
16
+ const searchTool = { description: 'Search' };
17
+ const stopWhen = () => false;
18
+
19
+ describe( 'parseGenerateTextArgs', () => {
20
+ it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
21
+ const abortSignal = AbortSignal.abort();
22
+ const tools = { search: searchTool };
23
+ const output = makeOutput();
24
+
25
+ expect( parseGenerateTextArgs( {
8
26
  prompt: 'summary@v1',
9
- variables: { topic: 'testing' },
27
+ variables: { topic: 'testing', count: 2, draft: false },
10
28
  promptDir: '/prompts',
11
- skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ],
12
- maxSteps: 4
13
- } ) ).not.toThrow();
29
+ tools,
30
+ output,
31
+ toolChoice: { type: 'tool', toolName: 'search' },
32
+ stopWhen,
33
+ abortSignal
34
+ } ) ).toEqual( {
35
+ promptFile: 'summary@v1',
36
+ variables: { topic: 'testing', count: 2, draft: false },
37
+ promptDir: '/prompts',
38
+ tools,
39
+ output,
40
+ toolChoice: { type: 'tool', toolName: 'search' },
41
+ stopWhen,
42
+ abortSignal
43
+ } );
14
44
  } );
15
45
 
16
- it( 'accepts a dynamic skills function', () => {
17
- expect( () => validateGenerateTextArgs( {
46
+ it( 'accepts an array of stopWhen functions', () => {
47
+ const parsed = parseGenerateTextArgs( {
18
48
  prompt: 'summary@v1',
19
- skills: () => [ { name: 'style', description: 'Style', instructions: '# Style' } ]
20
- } ) ).not.toThrow();
49
+ stopWhen: [ stopWhen ]
50
+ } );
51
+
52
+ expect( parsed.stopWhen ).toEqual( [ stopWhen ] );
21
53
  } );
22
54
 
23
55
  it( 'throws ValidationError with generateText prefix for invalid args', () => {
24
- expect( () => validateGenerateTextArgs( {
56
+ expect( () => parseGenerateTextArgs( {
25
57
  variables: { topic: 'testing' }
26
58
  } ) ).toThrow( ValidationError );
27
59
 
28
- expect( () => validateGenerateTextArgs( {
60
+ expect( () => parseGenerateTextArgs( {
29
61
  prompt: 123
30
62
  } ) ).toThrow( /Invalid generateText\(\) arguments/ );
31
- } );
32
63
 
33
- it( 'throws ValidationError for invalid promptDir, skills, or maxSteps', () => {
34
- expect( () => validateGenerateTextArgs( {
64
+ expect( () => parseGenerateTextArgs( {
35
65
  prompt: 'summary@v1',
36
66
  promptDir: ''
37
67
  } ) ).toThrow( ValidationError );
38
68
 
39
- expect( () => validateGenerateTextArgs( {
69
+ expect( () => parseGenerateTextArgs( {
40
70
  prompt: 'summary@v1',
41
- skills: [ { name: '', description: 'Style', instructions: '# Style' } ]
71
+ abortSignal: {}
42
72
  } ) ).toThrow( ValidationError );
73
+ } );
43
74
 
44
- expect( () => validateGenerateTextArgs( {
75
+ it( 'does not keep prompt-owned call arguments on the parsed result', () => {
76
+ expect( () => parseGenerateTextArgs( {
45
77
  prompt: 'summary@v1',
46
- maxSteps: 0
78
+ skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
79
+ } ) ).toThrow( /skills must be set in the prompt file/ );
80
+
81
+ expect( () => parseGenerateTextArgs( {
82
+ prompt: 'summary@v1',
83
+ maxSteps: 4,
84
+ skills: [ './writer.md' ]
85
+ } ) ).toThrow( /must be set in the prompt file/ );
86
+ } );
87
+
88
+ it( 'throws ValidationError for unrecognized call arguments', () => {
89
+ expect( () => parseGenerateTextArgs( {
90
+ prompt: 'summary@v1',
91
+ temperature: 0.2
92
+ } ) ).toThrow( /unrecognized key/i );
93
+
94
+ expect( () => parseGenerateTextArgs( {
95
+ prompt: 'summary@v1',
96
+ maxRetries: 2
97
+ } ) ).toThrow( /unrecognized key/i );
98
+
99
+ expect( () => parseGenerateTextArgs( {
100
+ prompt: 'summary@v1',
101
+ onChunk: () => {}
102
+ } ) ).toThrow( /unrecognized key/i );
103
+ } );
104
+
105
+ it( 'accepts nested objects and arrays as variables', () => {
106
+ const variables = {
107
+ company: { name: 'Acme', industries: [ 'SaaS', 'AI' ] },
108
+ criteria: [ { name: 'Accuracy', required: true } ]
109
+ };
110
+
111
+ expect( parseGenerateTextArgs( {
112
+ prompt: 'summary@v1',
113
+ variables
114
+ } ).variables ).toEqual( variables );
115
+ } );
116
+
117
+ it( 'throws ValidationError for invalid allowlisted field shapes', () => {
118
+ expect( () => parseGenerateTextArgs( {
119
+ prompt: 'summary@v1',
120
+ variables: [ 'not a record' ]
121
+ } ) ).toThrow( ValidationError );
122
+
123
+ expect( () => parseGenerateTextArgs( {
124
+ prompt: 'summary@v1',
125
+ tools: { search: true }
126
+ } ) ).toThrow( ValidationError );
127
+
128
+ expect( () => parseGenerateTextArgs( {
129
+ prompt: 'summary@v1',
130
+ output: true
131
+ } ) ).toThrow( ValidationError );
132
+
133
+ expect( () => parseGenerateTextArgs( {
134
+ prompt: 'summary@v1',
135
+ stopWhen: { type: 'custom-stop' }
136
+ } ) ).toThrow( ValidationError );
137
+
138
+ expect( () => parseGenerateTextArgs( {
139
+ prompt: 'summary@v1',
140
+ toolChoice: 'maybe'
47
141
  } ) ).toThrow( ValidationError );
48
142
  } );
49
- } );
50
143
 
51
- describe( 'validateStreamTextArgs', () => {
52
- it( 'accepts a prompt with optional variables', () => {
53
- expect( () => validateStreamTextArgs( {
144
+ it( 'accepts opaque tools and output objects', () => {
145
+ expect( parseGenerateTextArgs( {
54
146
  prompt: 'summary@v1',
55
- variables: [ 'arrays are accepted by the current schema' ],
56
- promptDir: '/prompts',
57
- skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ],
58
- maxSteps: 4
59
- } ) ).not.toThrow();
147
+ tools: { search: { description: 'Search' } },
148
+ output: { type: 'object' }
149
+ } ) ).toMatchObject( {
150
+ tools: { search: { description: 'Search' } },
151
+ output: { type: 'object' }
152
+ } );
60
153
  } );
61
154
 
62
- it( 'accepts a dynamic skills function', () => {
63
- expect( () => validateStreamTextArgs( {
155
+ it( 'preserves class-based tool and output identities', () => {
156
+ class TestTool {
157
+ #result = 'searched';
158
+
159
+ execute() {
160
+ return this.#result;
161
+ }
162
+ }
163
+
164
+ class TestOutput {
165
+ #result = 'parsed';
166
+
167
+ parseCompleteOutput() {
168
+ return this.#result;
169
+ }
170
+ }
171
+
172
+ const tool = new TestTool();
173
+ const output = new TestOutput();
174
+ const parsed = parseGenerateTextArgs( {
64
175
  prompt: 'summary@v1',
65
- skills: () => [ { name: 'style', description: 'Style', instructions: '# Style' } ]
66
- } ) ).not.toThrow();
176
+ tools: { search: tool },
177
+ output
178
+ } );
179
+
180
+ expect( parsed.tools.search ).toBe( tool );
181
+ expect( parsed.tools.search.execute() ).toBe( 'searched' );
182
+ expect( parsed.output ).toBe( output );
183
+ expect( parsed.output.parseCompleteOutput() ).toBe( 'parsed' );
67
184
  } );
185
+ } );
68
186
 
69
- it( 'throws ValidationError with streamText prefix for invalid args', () => {
70
- expect( () => validateStreamTextArgs( {} ) ).toThrow( ValidationError );
71
- expect( () => validateStreamTextArgs( { prompt: null } ) ).toThrow( /Invalid streamText\(\) arguments/ );
187
+ describe( 'parseStreamTextArgs', () => {
188
+ it( 'keeps stream callbacks on the parsed result', () => {
189
+ const parsed = parseStreamTextArgs( {
190
+ prompt: 'summary@v1',
191
+ variables: { topic: 'testing' },
192
+ promptDir: '/prompts',
193
+ onFinish: () => {},
194
+ onError: () => {},
195
+ onChunk: () => {}
196
+ } );
197
+
198
+ expect( parsed.promptFile ).toBe( 'summary@v1' );
199
+ expect( parsed.variables ).toEqual( { topic: 'testing' } );
200
+ expect( parsed.promptDir ).toBe( '/prompts' );
201
+ expect( parsed.onFinish ).toBeTypeOf( 'function' );
202
+ expect( parsed.onError ).toBeTypeOf( 'function' );
203
+ expect( parsed.onChunk ).toBeTypeOf( 'function' );
72
204
  } );
73
205
 
74
- it( 'throws ValidationError for invalid promptDir, skills, or maxSteps', () => {
75
- expect( () => validateStreamTextArgs( {
206
+ it( 'throws ValidationError with streamText prefix for invalid args', () => {
207
+ expect( () => parseStreamTextArgs( {} ) ).toThrow( ValidationError );
208
+ expect( () => parseStreamTextArgs( { prompt: null } ) ).toThrow( /Invalid streamText\(\) arguments/ );
209
+
210
+ expect( () => parseStreamTextArgs( {
76
211
  prompt: 'summary@v1',
77
212
  promptDir: ''
78
213
  } ) ).toThrow( ValidationError );
214
+ } );
79
215
 
80
- expect( () => validateStreamTextArgs( {
216
+ it( 'throws ValidationError for invalid callbacks', () => {
217
+ expect( () => parseStreamTextArgs( {
81
218
  prompt: 'summary@v1',
82
- skills: [ { name: 'style', description: 'Style', instructions: '' } ]
219
+ onFinish: 'not a function'
83
220
  } ) ).toThrow( ValidationError );
84
221
 
85
- expect( () => validateStreamTextArgs( {
222
+ expect( () => parseStreamTextArgs( {
86
223
  prompt: 'summary@v1',
87
- maxSteps: 1.5
224
+ onError: {}
88
225
  } ) ).toThrow( ValidationError );
226
+
227
+ expect( () => parseStreamTextArgs( {
228
+ prompt: 'summary@v1',
229
+ onChunk: 'not a function'
230
+ } ) ).toThrow( ValidationError );
231
+ } );
232
+
233
+ it( 'throws ValidationError for unrecognized call arguments', () => {
234
+ expect( () => parseStreamTextArgs( {
235
+ prompt: 'summary@v1',
236
+ temperature: 0.2
237
+ } ) ).toThrow( /unrecognized key/i );
238
+
239
+ expect( () => parseStreamTextArgs( {
240
+ prompt: 'summary@v1',
241
+ skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
242
+ } ) ).toThrow( /skills must be set in the prompt file/ );
243
+
244
+ expect( () => parseStreamTextArgs( {
245
+ prompt: 'summary@v1',
246
+ maxSteps: 1.5
247
+ } ) ).toThrow( /maxSteps must be set in the prompt file/ );
248
+ } );
249
+ } );
250
+
251
+ describe( 'parseGenerateTextWithStreamingArgs', () => {
252
+ it( 'keeps onChunk and omits streamText-only callbacks', () => {
253
+ const parsed = parseGenerateTextWithStreamingArgs( {
254
+ prompt: 'summary@v1',
255
+ onChunk: () => {}
256
+ } );
257
+
258
+ expect( parsed.promptFile ).toBe( 'summary@v1' );
259
+ expect( parsed.onChunk ).toBeTypeOf( 'function' );
260
+
261
+ expect( () => parseGenerateTextWithStreamingArgs( {
262
+ prompt: 'summary@v1',
263
+ onFinish: () => {}
264
+ } ) ).toThrow( /unrecognized key/i );
265
+ } );
266
+
267
+ it( 'uses the generateTextWithStreaming error prefix', () => {
268
+ expect( () => parseGenerateTextWithStreamingArgs( {
269
+ prompt: 'summary@v1',
270
+ maxSteps: 0
271
+ } ) ).toThrow( /Invalid generateTextWithStreaming\(\) arguments[\s\S]*maxSteps must be set in the prompt file/ );
89
272
  } );
90
273
  } );
91
274
 
92
- describe( 'validateGenerateImageArgs', () => {
275
+ describe( 'parseGenerateImageArgs', () => {
276
+ it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
277
+ const images = [ Buffer.from( 'image-bytes' ) ];
278
+ const mask = Buffer.from( 'mask-bytes' );
279
+ const abortSignal = AbortSignal.abort();
280
+
281
+ expect( parseGenerateImageArgs( {
282
+ prompt: 'image@v1',
283
+ variables: { scene: 'race cars' },
284
+ promptDir: '/prompts',
285
+ images,
286
+ mask,
287
+ abortSignal
288
+ } ) ).toEqual( {
289
+ promptFile: 'image@v1',
290
+ variables: { scene: 'race cars' },
291
+ promptDir: '/prompts',
292
+ images,
293
+ mask,
294
+ abortSignal
295
+ } );
296
+ } );
297
+
93
298
  it( 'accepts text-to-image args without images or mask', () => {
94
- expect( () => validateGenerateImageArgs( {
299
+ expect( parseGenerateImageArgs( {
95
300
  prompt: 'image@v1',
96
301
  variables: { topic: 'race cars' },
97
302
  promptDir: '/prompts'
98
- } ) ).not.toThrow();
303
+ } ) ).toEqual( {
304
+ promptFile: 'image@v1',
305
+ variables: { topic: 'race cars' },
306
+ promptDir: '/prompts'
307
+ } );
99
308
  } );
100
309
 
101
310
  it( 'accepts all supported image input shapes', () => {
@@ -105,7 +314,7 @@ describe( 'validateGenerateImageArgs', () => {
105
314
  const paddedBase64 = 'aW1hZ2UtYnl0ZXM=';
106
315
  const unpaddedBase64 = 'aW1hZ2U';
107
316
 
108
- expect( () => validateGenerateImageArgs( {
317
+ expect( () => parseGenerateImageArgs( {
109
318
  prompt: 'image@v1',
110
319
  images: [
111
320
  buffer,
@@ -123,21 +332,21 @@ describe( 'validateGenerateImageArgs', () => {
123
332
  } );
124
333
 
125
334
  it( 'throws ValidationError for invalid image args', () => {
126
- expect( () => validateGenerateImageArgs( {
335
+ expect( () => parseGenerateImageArgs( {
127
336
  prompt: ''
128
337
  } ) ).toThrow( /Invalid generateImage\(\) arguments/ );
129
338
 
130
- expect( () => validateGenerateImageArgs( {
339
+ expect( () => parseGenerateImageArgs( {
131
340
  prompt: 'image@v1',
132
341
  images: []
133
342
  } ) ).toThrow( ValidationError );
134
343
 
135
- expect( () => validateGenerateImageArgs( {
344
+ expect( () => parseGenerateImageArgs( {
136
345
  prompt: 'image@v1',
137
346
  images: [ { data: null } ]
138
347
  } ) ).toThrow( ValidationError );
139
348
 
140
- expect( () => validateGenerateImageArgs( {
349
+ expect( () => parseGenerateImageArgs( {
141
350
  prompt: 'image@v1',
142
351
  images: [ { data: 'aW1hZ2U=', mediaType: '' } ]
143
352
  } ) ).toThrow( ValidationError );
@@ -150,7 +359,7 @@ describe( 'validateGenerateImageArgs', () => {
150
359
  'not base64',
151
360
  'abcde'
152
361
  ] ) {
153
- expect( () => validateGenerateImageArgs( {
362
+ expect( () => parseGenerateImageArgs( {
154
363
  prompt: 'image@v1',
155
364
  images: [ image ]
156
365
  } ) ).toThrow( /Image strings must be raw base64 data/ );
@@ -158,9 +367,198 @@ describe( 'validateGenerateImageArgs', () => {
158
367
  } );
159
368
 
160
369
  it( 'requires images when mask is provided', () => {
161
- expect( () => validateGenerateImageArgs( {
370
+ expect( () => parseGenerateImageArgs( {
162
371
  prompt: 'image@v1',
163
372
  mask: Buffer.from( 'mask-bytes' )
164
373
  } ) ).toThrow( /mask requires images/ );
165
374
  } );
375
+
376
+ it( 'throws ValidationError for unrecognized or prompt-owned call arguments', () => {
377
+ expect( () => parseGenerateImageArgs( {
378
+ prompt: 'image@v1',
379
+ size: '1024x1024',
380
+ n: 2
381
+ } ) ).toThrow( /unrecognized key/i );
382
+
383
+ expect( () => parseGenerateImageArgs( {
384
+ prompt: 'image@v1',
385
+ aspectRatio: '1:1'
386
+ } ) ).toThrow( /unrecognized key/i );
387
+
388
+ expect( () => parseGenerateImageArgs( {
389
+ prompt: 'image@v1',
390
+ providerOptions: { openai: { quality: 'high' } }
391
+ } ) ).toThrow( /unrecognized key/i );
392
+
393
+ expect( () => parseGenerateImageArgs( {
394
+ prompt: 'image@v1',
395
+ maxSteps: 4
396
+ } ) ).toThrow( /unrecognized key/i );
397
+ } );
398
+ } );
399
+
400
+ describe( 'parseAgentArgs', () => {
401
+ it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
402
+ const tools = { search: searchTool };
403
+ const output = makeOutput();
404
+ const messageStore = { getMessages() {}, addMessages() {} };
405
+
406
+ const parsed = parseAgentArgs( {
407
+ prompt: 'summary@v1',
408
+ variables: { topic: 'testing' },
409
+ promptDir: '/prompts',
410
+ tools,
411
+ output,
412
+ stopWhen,
413
+ messageStore
414
+ } );
415
+
416
+ expect( parsed ).toEqual( {
417
+ promptFile: 'summary@v1',
418
+ variables: { topic: 'testing' },
419
+ promptDir: '/prompts',
420
+ tools,
421
+ output,
422
+ stopWhen,
423
+ messageStore
424
+ } );
425
+ expect( parsed.messageStore ).toBe( messageStore );
426
+ } );
427
+
428
+ it( 'preserves class-based message store identity', () => {
429
+ class TestMessageStore {
430
+ #messages = [];
431
+
432
+ getMessages() {
433
+ return this.#messages;
434
+ }
435
+
436
+ addMessages( messages ) {
437
+ this.#messages.push( ...messages );
438
+ }
439
+ }
440
+
441
+ const messageStore = new TestMessageStore();
442
+ const parsed = parseAgentArgs( {
443
+ prompt: 'summary@v1',
444
+ messageStore
445
+ } );
446
+ const message = { role: 'user', content: 'Hello' };
447
+
448
+ parsed.messageStore.addMessages( [ message ] );
449
+
450
+ expect( parsed.messageStore ).toBe( messageStore );
451
+ expect( parsed.messageStore.getMessages() ).toEqual( [ message ] );
452
+ } );
453
+
454
+ it( 'throws ValidationError with Agent prefix for invalid args', () => {
455
+ expect( () => parseAgentArgs( {} ) ).toThrow( ValidationError );
456
+ expect( () => parseAgentArgs( { prompt: '' } ) ).toThrow( /Invalid Agent\(\) arguments/ );
457
+
458
+ expect( () => parseAgentArgs( {
459
+ prompt: 'summary@v1',
460
+ promptDir: ''
461
+ } ) ).toThrow( ValidationError );
462
+ } );
463
+
464
+ it( 'does not keep prompt-owned call arguments on the parsed result', () => {
465
+ expect( () => parseAgentArgs( {
466
+ prompt: 'summary@v1',
467
+ skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
468
+ } ) ).toThrow( /skills must be set in the prompt file/ );
469
+
470
+ expect( () => parseAgentArgs( {
471
+ prompt: 'summary@v1',
472
+ maxSteps: 4,
473
+ skills: [ './writer.md' ]
474
+ } ) ).toThrow( /must be set in the prompt file/ );
475
+ } );
476
+
477
+ it( 'throws ValidationError for unrecognized constructor arguments', () => {
478
+ expect( () => parseAgentArgs( {
479
+ prompt: 'summary@v1',
480
+ temperature: 0.2
481
+ } ) ).toThrow( /unrecognized key/i );
482
+ } );
483
+
484
+ it( 'throws ValidationError for an invalid message store', () => {
485
+ expect( () => parseAgentArgs( {
486
+ prompt: 'summary@v1',
487
+ messageStore: { getMessages() {} }
488
+ } ) ).toThrow( ValidationError );
489
+ } );
490
+ } );
491
+
492
+ describe( 'parseAgentGenerateArgs', () => {
493
+ it( 'defaults omitted args and messages to an empty list', () => {
494
+ expect( parseAgentGenerateArgs() ).toEqual( { messages: [] } );
495
+ expect( parseAgentGenerateArgs( undefined ) ).toEqual( { messages: [] } );
496
+ expect( parseAgentGenerateArgs( {} ) ).toEqual( { messages: [] } );
497
+ } );
498
+
499
+ it( 'keeps allowlisted generate options', () => {
500
+ const messages = [ { role: 'user', content: 'Hello' } ];
501
+ const abortSignal = AbortSignal.abort();
502
+
503
+ expect( parseAgentGenerateArgs( {
504
+ messages,
505
+ abortSignal,
506
+ toolChoice: 'required'
507
+ } ) ).toEqual( {
508
+ messages,
509
+ abortSignal,
510
+ toolChoice: 'required'
511
+ } );
512
+ } );
513
+
514
+ it( 'throws ValidationError for unrecognized or prompt-owned call arguments', () => {
515
+ expect( () => parseAgentGenerateArgs( { temperature: 0.2 } ) ).toThrow( /unrecognized key/i );
516
+ expect( () => parseAgentGenerateArgs( { onChunk: () => {} } ) ).toThrow( /unrecognized key/i );
517
+ expect( () => parseAgentGenerateArgs( { maxSteps: 4 } ) ).toThrow( /maxSteps must be set in the prompt file/ );
518
+ } );
519
+
520
+ it( 'throws ValidationError for invalid messages or toolChoice', () => {
521
+ expect( () => parseAgentGenerateArgs( {
522
+ messages: [ { content: 'Hello' } ]
523
+ } ) ).toThrow( ValidationError );
524
+
525
+ expect( () => parseAgentGenerateArgs( {
526
+ messages: [ { role: 'moderator', content: 'Hello' } ]
527
+ } ) ).toThrow( ValidationError );
528
+
529
+ expect( () => parseAgentGenerateArgs( {
530
+ toolChoice: { type: 'tool' }
531
+ } ) ).toThrow( ValidationError );
532
+ } );
533
+ } );
534
+
535
+ describe( 'parseAgentGenerateWithStreamingArgs', () => {
536
+ it( 'defaults omitted args and accepts onChunk', () => {
537
+ expect( parseAgentGenerateWithStreamingArgs() ).toEqual( { messages: [] } );
538
+
539
+ const parsed = parseAgentGenerateWithStreamingArgs( { onChunk: () => {} } );
540
+ expect( parsed.messages ).toEqual( [] );
541
+ expect( parsed.onChunk ).toBeTypeOf( 'function' );
542
+ } );
543
+
544
+ it( 'rejects stream-only callbacks', () => {
545
+ expect( () => parseAgentGenerateWithStreamingArgs( { onFinish: () => {} } ) ).toThrow( /unrecognized key/i );
546
+ } );
547
+ } );
548
+
549
+ describe( 'parseAgentStreamArgs', () => {
550
+ it( 'defaults omitted args and keeps stream callbacks', () => {
551
+ expect( parseAgentStreamArgs() ).toEqual( { messages: [] } );
552
+
553
+ const parsed = parseAgentStreamArgs( {
554
+ onFinish: () => {},
555
+ onError: () => {},
556
+ onChunk: () => {}
557
+ } );
558
+
559
+ expect( parsed.messages ).toEqual( [] );
560
+ expect( parsed.onFinish ).toBeTypeOf( 'function' );
561
+ expect( parsed.onError ).toBeTypeOf( 'function' );
562
+ expect( parsed.onChunk ).toBeTypeOf( 'function' );
563
+ } );
166
564
  } );
package/src/ai_model.js DELETED
@@ -1,60 +0,0 @@
1
- import { ValidationError } from '@outputai/core';
2
- import { getProvider } from './ai_provider.js';
3
-
4
- /**
5
- * Load a text model from a loaded prompt config.
6
- *
7
- * @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
8
- * @returns {unknown} AI SDK language model
9
- */
10
- export const loadTextModel = prompt => getProvider( prompt.config.provider )( prompt.config.model );
11
-
12
- /**
13
- * Load an image model from a loaded prompt config.
14
- *
15
- * @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
16
- * @returns {unknown} AI SDK image model
17
- */
18
- export const loadImageModel = prompt => {
19
- const { provider: providerName, model } = prompt.config;
20
- const provider = getProvider( prompt.config.provider );
21
-
22
- const imageModelFactory = provider.image ?? provider.imageModel;
23
- if ( typeof imageModelFactory !== 'function' ) {
24
- throw new ValidationError( `Provider "${providerName}" does not support image models.` );
25
- }
26
- return imageModelFactory( model );
27
- };
28
-
29
- /**
30
- * Load provider-specific tools configured in a prompt.
31
- *
32
- * @param {object} prompt - Loaded prompt object with `config.provider` and optional `config.tools`
33
- * @returns {Record<string, unknown> | null} AI SDK tools, or null when none are configured
34
- */
35
- export const loadTools = prompt => {
36
- const { tools: promptTools, provider: providerName } = prompt.config;
37
-
38
- if ( Object.keys( promptTools ?? {} ).length === 0 ) {
39
- return null;
40
- }
41
-
42
- const provider = getProvider( providerName );
43
-
44
- if ( !provider.tools ) {
45
- throw new ValidationError( `Provider "${providerName}" does not support provider-specific tools.` );
46
- }
47
-
48
- const supportedTools = Object.keys( provider.tools );
49
- const invalidTools = Object.keys( promptTools ).filter( name => !supportedTools.includes( name ) );
50
-
51
- if ( invalidTools.length > 0 ) {
52
- throw new ValidationError( `Invalid tool(s) ${invalidTools.join( ', ' )} for provider "${providerName}". \
53
- Available: ${supportedTools.join( ', ' )}.` );
54
- }
55
-
56
- // load all tools and return in an object
57
- return Object.fromEntries(
58
- Object.entries( promptTools ).map( ( [ name, args ] ) => [ name, provider.tools[name]( args ) ] )
59
- );
60
- };