@@ -2582,8 +2582,11 @@ func buildAssistantMessageWithReasoningMetadata(text string, toolCalls []ToolCal
25822582// may arrive in non-deterministic order. Consumers should use ToolCallID to correlate
25832583// start/end events rather than relying on ordering.
25842584func (e * Engine ) executeToolCalls (ctx context.Context , calls []ToolCall , parallel bool , send eventSender , debug bool , debugRaw bool ) ([]Message , error ) {
2585+ // Cancellation must still yield a result message for every announced call:
2586+ // the caller persists the assistant message with its tool calls, and a turn
2587+ // with dangling tool calls breaks conversation resume on strict providers.
25852588 if err := ctx .Err (); err != nil {
2586- return nil , err
2589+ return cancelledToolCallMessages ( calls , err ), nil
25872590 }
25882591
25892592 // Fast path: single call, no concurrency overhead
@@ -2593,9 +2596,9 @@ func (e *Engine) executeToolCalls(ctx context.Context, calls []ToolCall, paralle
25932596
25942597 if ! parallel {
25952598 results := make ([]Message , 0 , len (calls ))
2596- for _ , call := range calls {
2599+ for i , call := range calls {
25972600 if err := ctx .Err (); err != nil {
2598- return nil , err
2601+ return append ( results , cancelledToolCallMessages ( calls [ i :], err ) ... ), nil
25992602 }
26002603 msgs , err := e .executeSingleToolCallSafe (ctx , call , send , debug , debugRaw )
26012604 if err != nil {
@@ -2659,13 +2662,43 @@ func (e *Engine) executeToolCalls(ctx context.Context, calls []ToolCall, paralle
26592662 case r := <- resultChan :
26602663 results [r .index ] = r .message
26612664 case <- ctx .Done ():
2662- return nil , ctx .Err ()
2665+ // Keep any results that finished before cancellation, then
2666+ // synthesize cancelled results for the rest so every announced
2667+ // call stays paired with a result in the persisted turn.
2668+ for drained := false ; ! drained ; {
2669+ select {
2670+ case r := <- resultChan :
2671+ results [r .index ] = r .message
2672+ default :
2673+ drained = true
2674+ }
2675+ }
2676+ for i := range results {
2677+ if results [i ].Role == "" {
2678+ results [i ] = cancelledToolCallMessage (calls [i ], ctx .Err ())
2679+ }
2680+ }
2681+ return results , nil
26632682 }
26642683 }
26652684
26662685 return results , nil
26672686}
26682687
2688+ // cancelledToolCallMessage synthesizes the error result for a tool call that
2689+ // was skipped or abandoned because the context was cancelled.
2690+ func cancelledToolCallMessage (call ToolCall , err error ) Message {
2691+ return ToolErrorMessage (call .ID , call .Name , fmt .Sprintf ("Error: %v" , err ), call .ThoughtSig )
2692+ }
2693+
2694+ func cancelledToolCallMessages (calls []ToolCall , err error ) []Message {
2695+ msgs := make ([]Message , 0 , len (calls ))
2696+ for _ , call := range calls {
2697+ msgs = append (msgs , cancelledToolCallMessage (call , err ))
2698+ }
2699+ return msgs
2700+ }
2701+
26692702// executeSingleToolCallSafe wraps executeSingleToolCall with panic recovery.
26702703func (e * Engine ) executeSingleToolCallSafe (ctx context.Context , call ToolCall , send eventSender , debug bool , debugRaw bool ) (msgs []Message , err error ) {
26712704 defer func () {
0 commit comments