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