agent: preserve session on interrupt only when state is clean

Keep session state after an interrupt if the last message is a clean
assistant text response (no dangling tool calls). Roll back to pre-turn
snapshot if the history is inconsistent — mid-tool-call interrupts would
send malformed history to the backend on the next turn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-26 13:36:43 +02:00
parent 554697e259
commit dabae7f72b
1 changed files with 22 additions and 7 deletions

View File

@ -855,16 +855,19 @@ func (s *agent) executeTurn(ctx context.Context, input string, handler EventHand
if err != nil {
interrupted := errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted)
if interrupted && s.session != nil && sessionClean(s.session.messages) {
// Interrupt with clean state: keep whatever was built during the turn.
return ""
}
// Real error or dirty session (dangling tool calls): restore pre-turn state.
if snapSession == nil {
s.session = nil
} else {
s.session.messages = snapMessages
}
if !interrupted {
// Real error: restore pre-turn state so the session is clean for retry.
if snapSession == nil {
s.session = nil
} else {
s.session.messages = snapMessages
}
handler(Event{Role: "error", Content: err.Error()})
}
// Interrupt: keep whatever session state was built during the turn.
return ""
}
@ -900,6 +903,18 @@ func (s *agent) executeTurn(ctx context.Context, input string, handler EventHand
return ""
}
// sessionClean reports whether the message history is in a consistent state
// that is safe to resume from. An interrupted turn may leave dangling tool
// calls (assistant message with ToolCalls but no following tool results) or
// end on a tool result with no subsequent assistant message — both are unsafe.
func sessionClean(msgs []backend.Message) bool {
if len(msgs) == 0 {
return true
}
last := msgs[len(msgs)-1]
return last.Role == "assistant" && len(last.ToolCalls) == 0
}
func toolInfosToBackend(infos []tools.ToolInfo) []backend.Tool {
out := make([]backend.Tool, len(infos))
for i, t := range infos {