From 81d2dc5d0f94268cda8306656d435562ae86a944 Mon Sep 17 00:00:00 2001 From: kyssta-exe Date: Fri, 19 Jun 2026 07:25:57 +0000 Subject: [PATCH] fix(agent): close tool-call sequence on interrupt to prevent role alternation violation (#48879) --- agent/turn_finalizer.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/agent/turn_finalizer.py b/agent/turn_finalizer.py index 3a013503110..60d6a88b103 100644 --- a/agent/turn_finalizer.py +++ b/agent/turn_finalizer.py @@ -166,6 +166,29 @@ def finalize_turn( # same empty-response loop again. try: agent._drop_trailing_empty_response_scaffolding(messages) + + # When the turn was interrupted and the last message is a tool + # result, append a synthetic assistant message to close the + # tool-call sequence. Without this, the session persists a + # ``tool → user`` alternation that strict providers (Gemini, + # Claude) reject, causing them to hallucinate a continuation of + # the user's message on the next turn (#48879). + # + # ``_drop_trailing_empty_response_scaffolding`` only rewinds the + # tool tail when an empty-response scaffolding flag is present; a + # clean ``/stop`` interrupt after a successful tool sets no such + # flag, so the tool result survives as the tail and we close it + # here instead. On an interrupt ``final_response`` is typically + # empty, so fall back to an explicit placeholder rather than + # persisting an empty-content assistant turn. + if interrupted and messages and messages[-1].get("role") == "tool": + messages.append( + { + "role": "assistant", + "content": (final_response or "").strip() or "Operation interrupted.", + } + ) + agent._persist_session(messages, conversation_history) except Exception as _persist_err: _cleanup_errors.append(f"persist_session: {_persist_err}")