Skip to content

fix: persist tool call results when the LLM request fails mid-turn - #10293

Open
Linyesantan wants to merge 1 commit into
AstrBotDevs:masterfrom
Linyesantan:fix/persist-tool-results-on-llm-failure
Open

Linyesantan wants to merge 1 commit into
AstrBotDevs:masterfrom
Linyesantan:fix/persist-tool-results-on-llm-failure

Conversation

@Linyesantan

@Linyesantan Linyesantan commented Sep 30, 2026 •

Copy link
Copy Markdown

fix: persist tool call results when the LLM request fails mid-turn

Problem

When the LLM request fails after tools have already run in the current turn, the
whole turn is dropped from the conversation history. The user watches the bot run
several tool calls, then the request fails, and on the next message the bot reports
only the tool calls from the previous turn and cannot continue from where it
stopped.

Root cause

InternalAgentSubStage._save_to_history already persists a turn whose LLM
response was missing altogether (llm_response is None), but a response that
came back with role="err" fell through the condition:

if has_checkpoint or (llm_response is None and req.tool_calls_result):

llm_response is not None in the error case, so neither branch matched and the
method returned without writing anything, discarding the user message, every tool
call, and every tool result of the turn.

Fix

Include the error case in that condition. When the history would otherwise end on
a tool result, append a short assistant turn so the next request does not carry an
unpaired tool call.

The turn is only persisted when req.tool_calls_result is non-empty, so a turn
that failed before running any tool behaves exactly as before.

Test

Two cases added to tests/test_conversation_checkpoint.py:

  • failed response with tool results persists them and appends the assistant turn
  • failed response without tool results still skips the save

Full suite: 3613 passed. The two failures in tests/test_fastapi_v1_dashboard.py
are pre-existing and reproduce without this change.

Summary by Sourcery

Persist completed tool interactions when an LLM request fails mid-turn so the agent can continue from the correct conversation state.

Bug Fixes:

  • Preserve tool calls and results in conversation history when an LLM request fails after tools have executed.
  • Add an assistant fallback message after failed tool execution so subsequent requests maintain valid conversation structure.

Tests:

  • Add coverage for persisting tool results on failed LLM responses and skipping persistence when no tools ran.

When the LLM request fails after tools have already run in the current
turn, the whole turn was dropped from the conversation history. The user
saw several tool calls, but the model reported only what it did in the
previous turn and could not continue from where it stopped.

`_save_to_history` already persisted a turn whose LLM response was missing
altogether (`llm_response is None`), but a response that came back with
role="err" fell through: the condition required `llm_response is None`, so
the method returned without writing anything.

Include the error case in that condition, and append a short assistant turn
when the history would otherwise end on a tool result, because an unpaired
tool call is rejected by the next request.

Co-Authored-By: opencode <noreply@opencode.ai>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py" line_range="543-547" />
<code_context>
+            ):
+                # A tool result must be followed by an assistant turn, otherwise
+                # the next request is rejected as an unpaired tool call.
+                if (
+                    llm_failed
+                    and message_to_save
+                    and message_to_save[-1].get("role") == "tool"
+                ):
+                    message_to_save.append(
+                        Message(
+                            role="assistant",
</code_context>
<issue_to_address>
**issue (broader_impact):** When the runner finishes with `llm_response is None` after executing tools, `_save_to_history` persists the assistant tool call and tool result but does not append an assistant message, leaving the saved history ending with a `tool` message. The next LLM request receives an unpaired tool call and is rejected by providers that require tool results to be followed by an assistant turn.

**Triggers:** When the runner has executed at least one tool but produces no final LLM response.

**Suggested fix:** Apply the fallback-assistant append logic to the `llm_response is None` path as well as the `llm_failed` path.

```suggestion
                if (
                    (llm_response is None or llm_failed)
                    and message_to_save
                    and message_to_save[-1].get("role") == "tool"
                ):
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if the new persistence or synthetic assistant message is wrong, the conversation history can retain a malformed or misleading turn after the code is reverted. The affected history is bounded and can be cleared or repaired, but reverting alone does not undo the stored record.

Blocking findings: astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py:547


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +543 to +547
if (
llm_failed
and message_to_save
and message_to_save[-1].get("role") == "tool"
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): When the runner finishes with llm_response is None after executing tools, _save_to_history persists the assistant tool call and tool result but does not append an assistant message, leaving the saved history ending with a tool message. The next LLM request receives an unpaired tool call and is rejected by providers that require tool results to be followed by an assistant turn.

Triggers: When the runner has executed at least one tool but produces no final LLM response.

Suggested fix: Apply the fallback-assistant append logic to the llm_response is None path as well as the llm_failed path.

Suggested change
if (
llm_failed
and message_to_save
and message_to_save[-1].get("role") == "tool"
):
if (
(llm_response is None or llm_failed)
and message_to_save
and message_to_save[-1].get("role") == "tool"
):

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant