Skip to content

fix: repair orphaned tool_calls in context for DeepSeek compatibility#23

Open
efecnc wants to merge 2 commits intoaltaidevorg:mainfrom
efecnc:fix/repair-orphaned-tool-calls-for-deepseek
Open

fix: repair orphaned tool_calls in context for DeepSeek compatibility#23
efecnc wants to merge 2 commits intoaltaidevorg:mainfrom
efecnc:fix/repair-orphaned-tool-calls-for-deepseek

Conversation

@efecnc
Copy link
Copy Markdown
Contributor

@efecnc efecnc commented May 6, 2026

Summary

  • Fixes 400 invalid_request_error from DeepSeek when a previous reasoning loop was cancelled mid-tool-execution, leaving orphaned assistant messages (with tool_calls) in SQLite memory without corresponding tool response messages
  • Adds repair_tool_call_context() which injects placeholder tool responses for any missing tool_call_id before sending context to the provider
  • Defensive fix that works for all OpenAI-compatible providers, not just DeepSeek

Root Cause

When the reasoning loop is cancelled (via /cancel, timeout, or signal):

  1. The assistant message with tool_calls has already been persisted to memory (agent/mod.rs:1516)
  2. The early-return on cancellation (lines 1540, 1615, 1646, 1704) skips saving the corresponding tool response messages
  3. On the next turn, get_context_since_reflection() loads this broken sequence
  4. DeepSeek strictly validates that every tool_call_id has a matching tool response — rejects with 400

Test plan

  • Verify cargo check passes (confirmed locally — no new warnings)
  • Trigger cancellation mid-tool-execution, then send a new message — should no longer produce 400 errors on DeepSeek
  • Verify other providers (OpenAI, Gemini, Anthropic) still work correctly with the repair in place

…rors

When the reasoning loop is cancelled mid-tool-execution, the assistant
message (with tool_calls) has already been persisted to memory but the
corresponding tool response messages haven't been saved. On the next
turn, the context loaded from the database is structurally invalid —
DeepSeek's API (which strictly validates that every tool_call_id has a
matching tool response) rejects it with a 400 invalid_request_error.

Add repair_tool_call_context() which scans loaded context for assistant
messages with tool_calls that lack corresponding tool responses, and
injects a placeholder "[Cancelled]" message for each missing one. This
runs after context is loaded from memory and before being sent to the
provider.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a repair_tool_call_context function to ensure that assistant messages with tool calls are always followed by corresponding tool responses, preventing errors with strict LLM providers like DeepSeek. Feedback suggests refining the insertion logic to maintain the correct sequence of tool responses and improve loop efficiency by appending missing responses at the end of the tool block.

Comment thread src/agent/mod.rs Outdated
Comment on lines +135 to +142
for id in missing.into_iter().rev() {
context.insert(
i + 1,
crate::utils::ChatMessage::tool("[Cancelled — tool execution interrupted]", &id),
);
}

i = j;
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.

medium

Inserting missing tool responses at i + 1 can result in an incorrect sequence if some tool responses already exist in the context. Most LLM providers (including DeepSeek) expect tool responses to appear in the same order as the tool_calls array in the assistant message. Additionally, setting i = j after the insertions causes the loop to re-scan the tool messages (both existing and newly inserted), which is slightly inefficient. Consider appending the missing responses to the end of the tool block (at index j) and updating i to skip the entire block.

        for id in missing {
            context.insert(
                j,
                crate::utils::ChatMessage::tool("[Cancelled — tool execution interrupted]", &id),
            );
            j += 1;
        }

        i = j;

Addresses review feedback: inserting at i+1 placed placeholder responses
before existing ones, breaking ordering. Now appends at index j (end of
tool block) and increments j to correctly skip the entire block.
@efecnc
Copy link
Copy Markdown
Contributor Author

efecnc commented May 6, 2026

Ready to merge. The ordering fix (d7aec5a) correctly appends placeholder tool responses at the end of the tool block instead of at i+1. No further changes needed.

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