@outputai/llm 0.7.1-next.db8ddd7.0 → 0.7.1-next.ed233ce.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 +2 -2
- package/src/agent.js +9 -10
- package/src/agent.spec.js +28 -3
- package/src/ai_sdk.js +2 -0
- package/src/ai_sdk.spec.js +4 -0
- package/src/ai_sdk_options.js +11 -1
- package/src/ai_sdk_options.spec.js +42 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/llm",
|
|
3
|
-
"version": "0.7.1-next.
|
|
3
|
+
"version": "0.7.1-next.ed233ce.0",
|
|
4
4
|
"description": "Framework abstraction to interact with LLM models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"gray-matter": "4.0.3",
|
|
14
14
|
"liquidjs": "10.25.7",
|
|
15
15
|
"undici": "8.1.0",
|
|
16
|
-
"@outputai/core": "0.7.1-next.
|
|
16
|
+
"@outputai/core": "0.7.1-next.ed233ce.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"ai": "6.0.168",
|
package/src/agent.js
CHANGED
|
@@ -43,17 +43,13 @@ export class Agent extends AIToolLoopAgent {
|
|
|
43
43
|
|
|
44
44
|
const { loadedPrompt, tools } = prepareTextPrompt( { prompt, variables, promptDir: resolvedPromptDir, skills, tools: toolsArg } );
|
|
45
45
|
|
|
46
|
-
const { messages
|
|
47
|
-
|
|
48
|
-
// Extract system messages as `instructions` for the ToolLoopAgent constructor
|
|
49
|
-
// and keep user messages for generate() calls — avoids provider errors
|
|
50
|
-
// with multiple system messages during multi-step tool loops.
|
|
51
|
-
// Pass message objects (not a string) so per-message providerOptions are preserved.
|
|
52
|
-
const systemMessages = allMessages.filter( isRole( ROLE.SYSTEM ) );
|
|
46
|
+
const { system, messages, ...constructorOptions } = loadAiSdkTextOptions( loadedPrompt );
|
|
53
47
|
|
|
48
|
+
// loadAiSdkTextOptions routes system blocks to the `system` slot (preserving
|
|
49
|
+
// per-message providerOptions); pass them as the agent's `instructions`.
|
|
54
50
|
super( {
|
|
55
51
|
...constructorOptions,
|
|
56
|
-
...(
|
|
52
|
+
...( system.length > 0 ? { instructions: system } : {} ),
|
|
57
53
|
...( tools ? { tools } : {} ),
|
|
58
54
|
stopWhen: stopWhen ?? stepCountIs( maxSteps ),
|
|
59
55
|
...rest
|
|
@@ -61,7 +57,9 @@ export class Agent extends AIToolLoopAgent {
|
|
|
61
57
|
|
|
62
58
|
this.#prompt = prompt;
|
|
63
59
|
this.#modelId = loadedPrompt.config.model;
|
|
64
|
-
|
|
60
|
+
// `messages` is system-free but may still hold authored <assistant>/<tool>
|
|
61
|
+
// blocks; seed only <user> turns into each generate()/stream() call.
|
|
62
|
+
this.#initialMessages = messages.filter( isRole( ROLE.USER ) );
|
|
65
63
|
this.#store = conversationStore ?? null;
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -80,7 +78,7 @@ export class Agent extends AIToolLoopAgent {
|
|
|
80
78
|
const traceId = startTrace( { name: 'Agent.generate', prompt: this.#prompt } );
|
|
81
79
|
try {
|
|
82
80
|
const messages = await this.#fetchMessages( userMessages );
|
|
83
|
-
const response = await super.generate( { messages, ...callOptions } );
|
|
81
|
+
const response = await super.generate( { messages, allowSystemInMessages: true, ...callOptions } );
|
|
84
82
|
const wrapped = await wrapTextResponse( { traceId, response, modelId: this.#modelId } );
|
|
85
83
|
await this.#storeMessages( userMessages, wrapped );
|
|
86
84
|
return wrapped;
|
|
@@ -96,6 +94,7 @@ export class Agent extends AIToolLoopAgent {
|
|
|
96
94
|
const messages = await this.#fetchMessages( userMessages );
|
|
97
95
|
return super.stream( {
|
|
98
96
|
messages,
|
|
97
|
+
allowSystemInMessages: true,
|
|
99
98
|
...callOptions,
|
|
100
99
|
...wrapStreamOnFinishResponse( { traceId, modelId: this.#modelId, onFinish } ),
|
|
101
100
|
onError( event ) {
|
package/src/agent.spec.js
CHANGED
|
@@ -113,7 +113,8 @@ const model = { id: 'MODEL' };
|
|
|
113
113
|
|
|
114
114
|
const textOptions = {
|
|
115
115
|
model,
|
|
116
|
-
|
|
116
|
+
system: [ { role: 'system', content: 'You are concise.' } ],
|
|
117
|
+
messages: [ { role: 'user', content: 'Initial user message' } ],
|
|
117
118
|
providerOptions: { test: true },
|
|
118
119
|
temperature: 0.3
|
|
119
120
|
};
|
|
@@ -239,7 +240,8 @@ describe( 'Agent', () => {
|
|
|
239
240
|
};
|
|
240
241
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
241
242
|
model,
|
|
242
|
-
|
|
243
|
+
system: [ systemMessage ],
|
|
244
|
+
messages: [ { role: 'user', content: 'Hello' } ]
|
|
243
245
|
} );
|
|
244
246
|
|
|
245
247
|
new Agent( { prompt: 'test@v1' } );
|
|
@@ -303,7 +305,28 @@ describe( 'Agent', () => {
|
|
|
303
305
|
await agent.generate();
|
|
304
306
|
|
|
305
307
|
expect( aiMocks.superGenerate ).toHaveBeenCalledWith( {
|
|
306
|
-
messages: [ { role: 'user', content: 'Initial user message' } ]
|
|
308
|
+
messages: [ { role: 'user', content: 'Initial user message' } ],
|
|
309
|
+
allowSystemInMessages: true
|
|
310
|
+
} );
|
|
311
|
+
} );
|
|
312
|
+
|
|
313
|
+
it( 'excludes authored assistant/tool blocks from the initial generate messages', async () => {
|
|
314
|
+
const { Agent } = await importSut();
|
|
315
|
+
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
316
|
+
model,
|
|
317
|
+
system: [ { role: 'system', content: 'You are concise.' } ],
|
|
318
|
+
messages: [
|
|
319
|
+
{ role: 'user', content: 'Initial user message' },
|
|
320
|
+
{ role: 'assistant', content: 'Authored assistant block' }
|
|
321
|
+
]
|
|
322
|
+
} );
|
|
323
|
+
const agent = new Agent( { prompt: 'test@v1' } );
|
|
324
|
+
|
|
325
|
+
await agent.generate();
|
|
326
|
+
|
|
327
|
+
expect( aiMocks.superGenerate ).toHaveBeenCalledWith( {
|
|
328
|
+
messages: [ { role: 'user', content: 'Initial user message' } ],
|
|
329
|
+
allowSystemInMessages: true
|
|
307
330
|
} );
|
|
308
331
|
} );
|
|
309
332
|
|
|
@@ -326,6 +349,7 @@ describe( 'Agent', () => {
|
|
|
326
349
|
{ role: 'assistant', content: 'Stored reply' },
|
|
327
350
|
callerMessage
|
|
328
351
|
],
|
|
352
|
+
allowSystemInMessages: true,
|
|
329
353
|
maxRetries: 1
|
|
330
354
|
} );
|
|
331
355
|
} );
|
|
@@ -405,6 +429,7 @@ describe( 'Agent', () => {
|
|
|
405
429
|
{ role: 'assistant', content: 'Stored reply' },
|
|
406
430
|
callerMessage
|
|
407
431
|
],
|
|
432
|
+
allowSystemInMessages: true,
|
|
408
433
|
maxRetries: 1,
|
|
409
434
|
onFinish: expect.any( Function ),
|
|
410
435
|
onError: expect.any( Function )
|
package/src/ai_sdk.js
CHANGED
|
@@ -22,6 +22,7 @@ export async function generateText( { prompt, variables, promptDir, skills = [],
|
|
|
22
22
|
try {
|
|
23
23
|
const response = await AI.generateText( {
|
|
24
24
|
...loadAiSdkTextOptions( loadedPrompt ),
|
|
25
|
+
allowSystemInMessages: true,
|
|
25
26
|
maxRetries: 0,
|
|
26
27
|
...aiSdkArgs,
|
|
27
28
|
...( tools && { tools } ),
|
|
@@ -50,6 +51,7 @@ export function streamText( { prompt, variables, promptDir, skills = [], maxStep
|
|
|
50
51
|
try {
|
|
51
52
|
return AI.streamText( {
|
|
52
53
|
...loadAiSdkTextOptions( loadedPrompt ),
|
|
54
|
+
allowSystemInMessages: true,
|
|
53
55
|
maxRetries: 0,
|
|
54
56
|
...aiSdkArgs,
|
|
55
57
|
...( tools && { tools } ),
|
package/src/ai_sdk.spec.js
CHANGED
|
@@ -190,6 +190,7 @@ describe( 'ai_sdk', () => {
|
|
|
190
190
|
expect( aiFns.stepCountIs ).toHaveBeenCalledWith( 4 );
|
|
191
191
|
expect( aiFns.generateText ).toHaveBeenCalledWith( {
|
|
192
192
|
...textOptions,
|
|
193
|
+
allowSystemInMessages: true,
|
|
193
194
|
maxRetries: 0,
|
|
194
195
|
tools,
|
|
195
196
|
temperature: 0.2,
|
|
@@ -225,6 +226,7 @@ describe( 'ai_sdk', () => {
|
|
|
225
226
|
expect( aiFns.stepCountIs ).not.toHaveBeenCalled();
|
|
226
227
|
expect( aiFns.generateText ).toHaveBeenCalledWith( {
|
|
227
228
|
...textOptions,
|
|
229
|
+
allowSystemInMessages: true,
|
|
228
230
|
maxRetries: 0
|
|
229
231
|
} );
|
|
230
232
|
} );
|
|
@@ -326,6 +328,7 @@ describe( 'ai_sdk', () => {
|
|
|
326
328
|
expect( aiFns.stepCountIs ).toHaveBeenCalledWith( 4 );
|
|
327
329
|
expect( aiFns.streamText ).toHaveBeenCalledWith( {
|
|
328
330
|
...textOptions,
|
|
331
|
+
allowSystemInMessages: true,
|
|
329
332
|
maxRetries: 0,
|
|
330
333
|
tools,
|
|
331
334
|
temperature: 0.2,
|
|
@@ -373,6 +376,7 @@ describe( 'ai_sdk', () => {
|
|
|
373
376
|
expect( aiFns.stepCountIs ).not.toHaveBeenCalled();
|
|
374
377
|
expect( aiFns.streamText ).toHaveBeenCalledWith( {
|
|
375
378
|
...textOptions,
|
|
379
|
+
allowSystemInMessages: true,
|
|
376
380
|
maxRetries: 0,
|
|
377
381
|
onFinish: expect.any( Function ),
|
|
378
382
|
onError: expect.any( Function )
|
package/src/ai_sdk_options.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { loadImageModel, loadTextModel, loadTools } from './ai_model.js';
|
|
2
2
|
import { resolveMessageProviderOptions } from './prompt/block_options.js';
|
|
3
|
+
import { ROLE, isRole } from './utils/message.js';
|
|
3
4
|
import { FatalError } from '@outputai/core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Convert a loaded prompt into AI SDK text generation options.
|
|
7
8
|
*
|
|
9
|
+
* System blocks are routed to the `system` option (as `SystemModelMessage[]`, so
|
|
10
|
+
* per-message providerOptions like `cacheControl` are preserved) rather than left
|
|
11
|
+
* in `messages` — the AI SDK flags system roles inside `messages` as a prompt
|
|
12
|
+
* injection risk, and `system` is the provider-recommended slot.
|
|
13
|
+
*
|
|
8
14
|
* @param {object} prompt - Loaded prompt object
|
|
9
15
|
* @returns {object} Options for AI SDK text calls
|
|
10
16
|
*/
|
|
@@ -12,9 +18,13 @@ export const loadAiSdkTextOptions = prompt => {
|
|
|
12
18
|
if ( prompt.messages.length === 0 ) {
|
|
13
19
|
throw new FatalError( `Prompt "${prompt.name}" has no chat-style messages. Add role-tagged blocks like <system> or <user>.` );
|
|
14
20
|
}
|
|
21
|
+
const isSystem = isRole( ROLE.SYSTEM );
|
|
22
|
+
const resolvedMessages = resolveMessageProviderOptions( prompt );
|
|
23
|
+
|
|
15
24
|
const options = {
|
|
16
25
|
model: loadTextModel( prompt ),
|
|
17
|
-
|
|
26
|
+
system: resolvedMessages.filter( isSystem ),
|
|
27
|
+
messages: resolvedMessages.filter( message => !isSystem( message ) ),
|
|
18
28
|
providerOptions: prompt.config.providerOptions
|
|
19
29
|
};
|
|
20
30
|
|
|
@@ -60,7 +60,8 @@ describe( 'ai_sdk_options', () => {
|
|
|
60
60
|
expect( loadToolsImpl ).toHaveBeenCalledWith( prompt );
|
|
61
61
|
expect( result ).toEqual( {
|
|
62
62
|
model: 'MODEL',
|
|
63
|
-
|
|
63
|
+
system: [ { role: 'system', content: 'You are concise.' } ],
|
|
64
|
+
messages: [ { role: 'user', content: 'Hello' } ],
|
|
64
65
|
providerOptions: prompt.config.providerOptions,
|
|
65
66
|
temperature: 0.3,
|
|
66
67
|
maxOutputTokens: 1000
|
|
@@ -180,13 +181,50 @@ describe( 'ai_sdk_options', () => {
|
|
|
180
181
|
const { loadAiSdkTextOptions } = await importSut();
|
|
181
182
|
const result = loadAiSdkTextOptions( prompt );
|
|
182
183
|
|
|
183
|
-
expect( result.
|
|
184
|
+
expect( result.system ).toEqual( [
|
|
184
185
|
{
|
|
185
186
|
role: 'system',
|
|
186
187
|
content: 'Static',
|
|
187
188
|
providerOptions: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } } }
|
|
188
|
-
}
|
|
189
|
-
|
|
189
|
+
}
|
|
190
|
+
] );
|
|
191
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
192
|
+
} );
|
|
193
|
+
|
|
194
|
+
it( 'returns an empty system array when the prompt has no system block', async () => {
|
|
195
|
+
const prompt = {
|
|
196
|
+
name: 'no-system@v1',
|
|
197
|
+
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
198
|
+
messages: [ { role: 'user', content: 'Hello' } ],
|
|
199
|
+
instructions: null
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
203
|
+
const result = loadAiSdkTextOptions( prompt );
|
|
204
|
+
|
|
205
|
+
expect( result.system ).toEqual( [] );
|
|
206
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'groups multiple system blocks into the system option and keeps them out of messages', async () => {
|
|
210
|
+
const prompt = {
|
|
211
|
+
name: 'multi-system@v1',
|
|
212
|
+
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
213
|
+
messages: [
|
|
214
|
+
{ role: 'system', content: 'First' },
|
|
215
|
+
{ role: 'system', content: 'Second' },
|
|
216
|
+
{ role: 'user', content: 'Hello' }
|
|
217
|
+
],
|
|
218
|
+
instructions: null
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
222
|
+
const result = loadAiSdkTextOptions( prompt );
|
|
223
|
+
|
|
224
|
+
expect( result.system ).toEqual( [
|
|
225
|
+
{ role: 'system', content: 'First' },
|
|
226
|
+
{ role: 'system', content: 'Second' }
|
|
190
227
|
] );
|
|
228
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
191
229
|
} );
|
|
192
230
|
} );
|