-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Description
Description
The forceCompression function in pkg/agent/loop.go (lines 771-820) aggressively drops the oldest 50% of messages when context limit is hit. However, it does not preserve the pairing between tool calls and tool responses, leading to orphaned tool role messages that cause API 400 errors.
Error Message
Status: 400
Body: {"error":{"code":"invalid_parameter_error","message":"<400> InternalError.Algo.InvalidParameter: messages with role \"tool\" must be a response to a preceeding message with \"tool_calls\".","param":null,"type":"invalid_request_error"},"request_id":"99d697db-271a-98c4-a6b4-41937d43e224"}, retry=0}
Root Cause
In forceCompression:
conversation := history[1 : len(history)-1]
mid := len(conversation) / 2
keptConversation := conversation[mid:] // Simply cuts from the middle!The code cuts messages from the middle without checking if:
- A
toolrole message has a correspondingassistantmessage withtool_callsbefore it - The cut point breaks a tool call/response pair
Example Scenario
History before compression:
[0] system: "You are..."
[1] user: "What's the weather?"
[2] assistant: [tool_calls: get_weather]
[3] tool: "Sunny, 25°C"
[4] assistant: "It's sunny..."
[5] user: "Thanks!"
After forceCompression (mid=2, keeps [3], [4]):
[0] system: "... [compression note]"
[1] tool: "Sunny, 25°C" // ❌ Orphaned! No preceding assistant with tool_calls
[2] assistant: "It's sunny..."
[3] user: "Thanks!"
This results in a tool message without a preceding assistant message containing tool_calls, which violates API requirements.
Affected Providers
- OpenAI-compatible APIs (including Zhipu/GLM)
- Anthropic
- Any provider that enforces strict tool call/response pairing
Suggested Fix
Option 1: Skip tool messages when finding cut point
// Find a safe cut point that doesn't break tool call/response pairs
func findSafeCutPoint(conversation []providers.Message) int {
mid := len(conversation) / 2
// Walk backwards from mid to find a user message (safe cut point)
for i := mid; i >= 0; i-- {
if conversation[i].Role == "user" {
return i + 1 // Cut after user message
}
}
// Walk forwards from mid to find a user message
for i := mid; i < len(conversation); i++ {
if conversation[i].Role == "user" {
return i + 1
}
}
return mid // Fallback (may still cause issues)
}Option 2: Drop tool call/response pairs together
When dropping messages, ensure that if an assistant message with tool_calls is dropped, all corresponding tool responses are also dropped.
Option 3: Remove orphaned tool messages after compression
Add a sanitization step after compression to remove tool messages without preceding assistant[tool_calls].
Environment
- PicoClaw version: latest
- Provider: Zhipu GLM (also affects other OpenAI-compatible providers)
Related Issues
- Race condition in session history causes "tool_call_ids did not have response messages" (HTTP 400) #704 Race condition in session history causes similar error
- Codex provider fails with 400 when assistant emits multiple tool calls in one turn (
No tool output found for function call ...) #760 Codex provider fails with 400 for multiple tool calls