@@ -92,7 +92,7 @@ describe("chat.agent steering on a turn that fails", () => {
9292 const chatId = "steer-error-path" ;
9393 const toolGate = deferred ( ) ;
9494 let toolEntered = false ;
95- const events : { newUIMessages : UIMessage [ ] ; finishReason ?: string } [ ] = [ ] ;
95+ const events : { newUIMessages : UIMessage [ ] ; messages : unknown [ ] ; finishReason ?: string } [ ] = [ ] ;
9696 const promptsSawSteer : boolean [ ] = [ ] ;
9797
9898 const gateTool = tool ( {
@@ -109,25 +109,36 @@ describe("chat.agent steering on a turn that fails", () => {
109109 const model = new MockLanguageModelV3 ( {
110110 doStream : async ( { prompt } ) => {
111111 promptsSawSteer . push ( JSON . stringify ( prompt ) . includes ( "steer-me" ) ) ;
112- // Step 1 calls the tool, step 2 fails mid-stream.
113- return step ++ === 0
114- ? {
115- stream : new ReadableStream ( {
116- start ( c ) {
117- for ( const ch of toolCallChunks ( "tc-1" ) ) c . enqueue ( ch ) ;
118- c . close ( ) ;
119- } ,
120- } ) ,
121- }
122- : { stream : erroringStream ( ) } ;
112+ // Step 1 calls the tool, step 2 fails mid-stream, the next turn answers.
113+ const n = step ++ ;
114+ const fromChunks = ( chunks : LanguageModelV3StreamPart [ ] ) => ( {
115+ stream : new ReadableStream < LanguageModelV3StreamPart > ( {
116+ start ( c ) {
117+ for ( const ch of chunks ) c . enqueue ( ch ) ;
118+ c . close ( ) ;
119+ } ,
120+ } ) ,
121+ } ) ;
122+ if ( n === 0 ) return fromChunks ( toolCallChunks ( "tc-1" ) ) ;
123+ if ( n === 1 ) return { stream : erroringStream ( ) } ;
124+ return fromChunks ( [
125+ { type : "text-start" , id : "t1" } ,
126+ { type : "text-delta" , id : "t1" , delta : "ok" } ,
127+ { type : "text-end" , id : "t1" } ,
128+ { type : "finish" , finishReason : { unified : "stop" , raw : "stop" } , usage : USAGE } ,
129+ ] ) ;
123130 } ,
124131 } ) ;
125132
126133 const agent = chat . agent ( {
127134 id : "steer-error-path" ,
128135 pendingMessages : { shouldInject : ( ) => true } ,
129- onTurnComplete : async ( { newUIMessages, finishReason } ) => {
130- events . push ( { newUIMessages : [ ...( newUIMessages ?? [ ] ) ] , finishReason } ) ;
136+ onTurnComplete : async ( { newUIMessages, messages, finishReason } ) => {
137+ events . push ( {
138+ newUIMessages : [ ...( newUIMessages ?? [ ] ) ] ,
139+ messages : [ ...messages ] ,
140+ finishReason,
141+ } ) ;
131142 } ,
132143 run : async ( { messages, signal } ) =>
133144 streamText ( {
@@ -160,6 +171,16 @@ describe("chat.agent steering on a turn that fails", () => {
160171 . map ( ( p ) => p . text ?? "" )
161172 ) ;
162173 expect ( texts ) . toContain ( "steer-me" ) ;
174+
175+ // The model lane the failed turn reports has to carry it too, and so
176+ // does the prompt the next turn actually sends. Reconciling only on the
177+ // success path leaves it pending, so the next turn misses it and it
178+ // lands a slot late at the end of that turn.
179+ expect ( JSON . stringify ( events [ 0 ] ! . messages ) ) . toContain ( "steer-me" ) ;
180+ const promptsBefore = promptsSawSteer . length ;
181+ await harness . sendMessage ( userMessage ( "m3" , "u-3" ) ) ;
182+ await waitFor ( ( ) => promptsSawSteer . length > promptsBefore , "turn 2 prompt built" ) ;
183+ expect ( promptsSawSteer [ promptsBefore ] ) . toBe ( true ) ;
163184 } finally {
164185 toolGate . resolve ( ) ;
165186 await harness . close ( ) ;
0 commit comments