fix: repair orphaned tool_calls in context for DeepSeek compatibility#23
fix: repair orphaned tool_calls in context for DeepSeek compatibility#23efecnc wants to merge 2 commits intoaltaidevorg:mainfrom
Conversation
…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.
There was a problem hiding this comment.
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.
| for id in missing.into_iter().rev() { | ||
| context.insert( | ||
| i + 1, | ||
| crate::utils::ChatMessage::tool("[Cancelled — tool execution interrupted]", &id), | ||
| ); | ||
| } | ||
|
|
||
| i = j; |
There was a problem hiding this comment.
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.
|
✅ 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. |
Summary
invalid_request_errorfrom DeepSeek when a previous reasoning loop was cancelled mid-tool-execution, leaving orphaned assistant messages (withtool_calls) in SQLite memory without corresponding tool response messagesrepair_tool_call_context()which injects placeholder tool responses for any missingtool_call_idbefore sending context to the providerRoot Cause
When the reasoning loop is cancelled (via
/cancel, timeout, or signal):tool_callshas already been persisted to memory (agent/mod.rs:1516)get_context_since_reflection()loads this broken sequencetool_call_idhas a matchingtoolresponse — rejects with 400Test plan
cargo checkpasses (confirmed locally — no new warnings)