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:
parent
abec0f2ea1
commit
bd9c8702d2
3 changed files with 43 additions and 2 deletions
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue