Fix #475: Implement sanitizeToolPairs for tool call/result integrity

This commit addresses the issue where history compression was creating orphaned tool_call/tool_result pairs that cause Anthropic API errors. Changes include:

- Added sanitizeToolPairs() helper function to validate tool call/result pairs

- Applied sanitization in forceCompression() to ensure message integrity after cutting

- Applied sanitization in summarizeSession() to remove orphaned results during truncation
This commit is contained in:
liugangjian 2026-03-05 10:05:49 +08:00
parent abec0f2ea1
commit bd9c8702d2
3 changed files with 43 additions and 2 deletions

Binary file not shown.

View file

@ -571,7 +571,7 @@ func (al *AgentLoop) processSystemMessage(
UserMessage: fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content),
DefaultResponse: "Background task completed.",
EnableSummary: false,
SendResponse: true,
SendResponse: false, // Prevent duplicate responses caused by system message processing
})
}
@ -1157,7 +1157,7 @@ func (al *AgentLoop) forceCompression(agent *AgentInstance, sessionKey string) {
newHistory = append(newHistory, history[len(history)-1]) // Last message
// Update session
agent.Sessions.SetHistory(sessionKey, newHistory)
agent.Sessions.SetHistory(sessionKey, sanitizeToolPairs(newHistory))
agent.Sessions.Save(sessionKey)
logger.WarnCF("agent", "Forced compression executed", map[string]any{
@ -1332,6 +1332,9 @@ func (al *AgentLoop) summarizeSession(agent *AgentInstance, sessionKey string) {
if finalSummary != "" {
agent.Sessions.SetSummary(sessionKey, finalSummary)
agent.Sessions.TruncateHistory(sessionKey, 4)
history := agent.Sessions.GetHistory(sessionKey)
cleanHistory := sanitizeToolPairs(history)
agent.Sessions.SetHistory(sessionKey, cleanHistory)
agent.Sessions.Save(sessionKey)
}
}
@ -1502,3 +1505,40 @@ func extractParentPeer(msg bus.InboundMessage) *routing.RoutePeer {
}
return &routing.RoutePeer{Kind: parentKind, ID: parentID}
}
// sanitizeToolPairs removes malformed tool_call/tool_result pairs from message histories. It ensures
// that all tool_use IDs referenced in tool results have corresponding tool_use blocks in
// assistant messages, and removes orphaned tool_result messages that don't pair with a
// prior tool_call in the same session history.
func sanitizeToolPairs(messages []providers.Message) []providers.Message {
// Build map of expected tool call IDs
expectedToolCallIDs := make(map[string]bool)
var sanitized []providers.Message
for _, msg := range messages {
if msg.Role == "assistant" && len(msg.ToolCalls) > 0 {
// Record all tool call IDs in assistant messages
for _, call := range msg.ToolCalls {
expectedToolCallIDs[call.ID] = true
}
sanitized = append(sanitized, msg)
} else if msg.Role == "tool" {
// Verify this tool result has a corresponding tool call
if expectedToolCallIDs[msg.ToolCallID] {
// Valid pairing: tool result has corresponding tool call
sanitized = append(sanitized, msg)
// Remove the ID after use to avoid reuse of stale ID
delete(expectedToolCallIDs, msg.ToolCallID)
} else {
// Invalid: orphaned tool result without matching tool call
// Skip this message rather than including it
continue
}
} else {
// Regular messages (user/assistant without tools/other) - always include
sanitized = append(sanitized, msg)
}
}
return sanitized
}

View file

@ -20,6 +20,7 @@ type SubagentTask struct {
Status string
Result string
Created int64
announcedOnce sync.Once // Ensures the completion announcement is sent only once
}
type SubagentManager struct {