11use super :: AgentResult ;
2- use crate :: llm:: { Message , TokenUsage } ;
2+ use crate :: llm:: { ContentBlock , Message , TokenUsage } ;
33use crate :: verification:: VerificationReport ;
44use serde_json:: Value ;
55use std:: time:: Instant ;
@@ -211,6 +211,34 @@ impl ExecutionLoopState {
211211 }
212212 }
213213
214+ /// Build a result from a turn that was cancelled mid-generation. Keeps the
215+ /// conversation accumulated so far (the user's message above all) so the next
216+ /// turn remembers it. Appends a short assistant marker when the log would
217+ /// otherwise end on a user message, so the next user turn still alternates.
218+ pub ( super ) fn finish_interrupted ( mut self ) -> AgentResult {
219+ let ends_on_user = self
220+ . messages
221+ . last ( )
222+ . map ( |m| m. role != "assistant" )
223+ . unwrap_or ( false ) ;
224+ if ends_on_user {
225+ self . messages . push ( Message {
226+ role : "assistant" . to_string ( ) ,
227+ content : vec ! [ ContentBlock :: Text {
228+ text: "(Response interrupted by the user.)" . to_string( ) ,
229+ } ] ,
230+ reasoning_content : None ,
231+ } ) ;
232+ }
233+ AgentResult {
234+ text : String :: new ( ) ,
235+ messages : self . messages ,
236+ usage : self . total_usage ,
237+ tool_calls_count : self . tool_calls_count ,
238+ verification_reports : self . verification_reports ,
239+ }
240+ }
241+
214242 fn tool_signature ( tool_name : & str , args : & Value ) -> String {
215243 format ! (
216244 "{}:{}" ,
@@ -225,6 +253,27 @@ mod tests {
225253 use super :: * ;
226254 use serde_json:: json;
227255
256+ #[ test]
257+ fn finish_interrupted_keeps_user_message_and_alternates ( ) {
258+ // A cancelled turn must keep the user's message (so the next turn
259+ // remembers it) and end on an assistant message (so it still alternates).
260+ let mut state = ExecutionLoopState :: new ( & [ ] ) ;
261+ state. messages . push ( Message :: user ( "what is the plan?" ) ) ;
262+ let result = state. finish_interrupted ( ) ;
263+ assert ! (
264+ result
265+ . messages
266+ . iter( )
267+ . any( |m| m. role == "user" && m. text( ) . contains( "what is the plan?" ) ) ,
268+ "user message must survive the interrupt"
269+ ) ;
270+ assert_eq ! (
271+ result. messages. last( ) . unwrap( ) . role,
272+ "assistant" ,
273+ "history must end on an assistant message to alternate"
274+ ) ;
275+ }
276+
228277 #[ test]
229278 fn duplicate_tool_call_uses_recent_success_and_error_signatures ( ) {
230279 let mut state = ExecutionLoopState :: new ( & [ ] ) ;
0 commit comments