@@ -12,9 +12,11 @@ import {
1212} from '@/providers/openai-compat/streaming-tool-loop'
1313import type { AgentStreamEvent } from '@/providers/stream-events'
1414import type { StreamingToolLoopComplete } from '@/providers/streaming-tool-loop-shared'
15+ import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter'
1516import type { ProviderToolConfig , TimeSegment } from '@/providers/types'
1617import type { ToolResponse } from '@/tools/types'
1718import type {
19+ AgentToolUseExpectations ,
1820 AgentToolUseResult ,
1921 AgentToolUseScenario ,
2022 EvalCheck ,
@@ -49,6 +51,20 @@ interface ToolCallList {
4951 count : number
5052}
5153
54+ /** Scripted runs assert exact behavior; live runs assert outcomes across trials. */
55+ export type EvalRunMode = 'scripted' | 'live'
56+
57+ /** Options for {@link runScenario}. */
58+ export interface RunScenarioOptions {
59+ /** Model turns. Defaults to the scenario's scripted turns. */
60+ completion ?: OpenAICompatCreateCompletion
61+ mode ?: EvalRunMode
62+ /** Model id sent to a live provider and recorded in the run. */
63+ model ?: string
64+ /** Provider label used in loop diagnostics. */
65+ providerName ?: string
66+ }
67+
5268/** Raw call counts as a value the loop never reads; scenarios only assert on it. */
5369const COMPLETION_USAGE = ( ) : CompletionUsage => ( {
5470 prompt_tokens : 10 ,
@@ -184,22 +200,34 @@ function matchesContent(content: string, expected: string | RegExp): boolean {
184200 return typeof expected === 'string' ? content . includes ( expected ) : expected . test ( content )
185201}
186202
203+ function isOrderedSubsequence ( actual : string [ ] , expected : string [ ] ) : boolean {
204+ let index = 0
205+ for ( const name of actual ) {
206+ if ( name === expected [ index ] ) index += 1
207+ }
208+ return index === expected . length
209+ }
210+
187211function score (
188- scenario : AgentToolUseScenario ,
212+ expected : AgentToolUseExpectations ,
189213 toolCalls : CapturedToolCall [ ] ,
190214 finalContent : string ,
191215 iterations : number ,
192- error : unknown
216+ error : unknown ,
217+ mode : EvalRunMode
193218) : EvalCheck [ ] {
194- const expected = scenario . expect
195219 const actualSequence = toolCalls . map ( ( call ) => call . name )
196220 const checks : EvalCheck [ ] = [ ]
197221
198222 if ( expected . toolCallSequence ) {
223+ const sequenceMatches =
224+ mode === 'live'
225+ ? isOrderedSubsequence ( actualSequence , expected . toolCallSequence )
226+ : sameSequence ( actualSequence , expected . toolCallSequence )
199227 checks . push (
200228 check (
201229 'tool-call-sequence' ,
202- sameSequence ( actualSequence , expected . toolCallSequence ) ,
230+ sequenceMatches ,
203231 `expected [${ expected . toolCallSequence . join ( ', ' ) } ], got [${ actualSequence . join ( ', ' ) } ]`
204232 )
205233 )
@@ -249,17 +277,22 @@ function score(
249277
250278 const successful = toolCalls . filter ( ( call ) => call . success ) . length
251279 if ( expected . successfulToolCalls !== undefined ) {
280+ const successMatches =
281+ mode === 'live'
282+ ? successful >= expected . successfulToolCalls
283+ : successful === expected . successfulToolCalls
252284 checks . push (
253285 check (
254286 'successful-tool-calls' ,
255- successful === expected . successfulToolCalls ,
256- `expected ${ expected . successfulToolCalls } , got ${ successful } `
287+ successMatches ,
288+ `expected ${ mode === 'live' ? 'at least ' : '' } ${ expected . successfulToolCalls } , got ${ successful } `
257289 )
258290 )
259291 }
260292
261293 const errored = toolCalls . length - successful
262- if ( expected . erroredToolCalls !== undefined ) {
294+ /** A live model chooses its own retry count, so an exact error count is scripted-only. */
295+ if ( expected . erroredToolCalls !== undefined && mode !== 'live' ) {
263296 checks . push (
264297 check (
265298 'errored-tool-calls' ,
@@ -283,9 +316,18 @@ function score(
283316}
284317
285318/** Runs and scores one scenario. */
286- export async function runScenario ( scenario : AgentToolUseScenario ) : Promise < AgentToolUseResult > {
319+ export async function runScenario (
320+ scenario : AgentToolUseScenario ,
321+ options : RunScenarioOptions = { }
322+ ) : Promise < AgentToolUseResult > {
323+ const mode = options . mode ?? 'scripted'
324+ const model = options . model ?? EVAL_MODEL
325+ const providerName = options . providerName ?? EVAL_PROVIDER
326+ const expected =
327+ mode === 'live' ? { ...scenario . expect , ...scenario . liveExpect } : scenario . expect
287328 const toolNames = new Set ( scenario . tools . map ( ( tool ) => tool . name ) )
288329 const resultQueues = buildResultQueues ( scenario , toolNames )
330+ const providerTools = toProviderTools ( scenario . tools )
289331 const invocations : EvalToolInvocation [ ] = [ ]
290332
291333 providersMock . MAX_TOOL_ITERATIONS = MAX_TOOL_ITERATIONS
@@ -312,18 +354,22 @@ export async function runScenario(scenario: AgentToolUseScenario): Promise<Agent
312354 const startedAt = Date . now ( )
313355 try {
314356 const stream = createOpenAICompatStreamingToolLoopStream ( {
315- providerName : EVAL_PROVIDER ,
357+ providerName,
316358 request : {
317- model : EVAL_MODEL ,
359+ model,
318360 apiKey : 'eval-key' ,
319361 messages : [ ] ,
320- tools : toProviderTools ( scenario . tools ) ,
362+ tools : providerTools ,
363+ } ,
364+ basePayload : {
365+ model,
366+ tools : providerTools . map ( ( tool ) => adaptOpenAIChatToolSchema ( tool ) ) ,
321367 } ,
322- basePayload : { model : EVAL_MODEL } ,
323368 messages : [ { role : 'user' , content : scenario . userMessage } ] ,
324- createStream : createScriptedCompletion ( scenario ) ,
369+ createStream : options . completion ?? createScriptedCompletion ( scenario ) ,
325370 logger,
326371 timeSegments,
372+ preserveAssistantReasoning : true ,
327373 onComplete : ( result ) => {
328374 completed = result
329375 } ,
@@ -337,7 +383,7 @@ export async function runScenario(scenario: AgentToolUseScenario): Promise<Agent
337383 const toolCalls = ( ( completed ?. toolCalls as ToolCallList | undefined ) ?. list ?? [ ] ) . slice ( )
338384 const finalContent = completed ?. content ?? ''
339385 const iterations = completed ?. iterations ?? 0
340- const checks = score ( scenario , toolCalls , finalContent , iterations , streamError )
386+ const checks = score ( expected , toolCalls , finalContent , iterations , streamError , mode )
341387 const successful = toolCalls . filter ( ( call ) => call . success ) . length
342388
343389 return {
0 commit comments