78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package backend
|
|
|
|
// SanitizeMessages filters a conversation history, removing invalid sequences.
|
|
// It ensures:
|
|
// 1. Every assistant message with ToolCalls has matching tool result messages
|
|
// 2. No orphan tool result messages (tool results without preceding tool calls)
|
|
// 3. System/user/assistant messages without tool calls are preserved
|
|
//
|
|
// The returned slice shares memory with the input — messages are not copied.
|
|
// This is primarily used when inheriting context for sub-agents.
|
|
func SanitizeMessages(msgs []Message) []Message {
|
|
if len(msgs) == 0 {
|
|
return msgs
|
|
}
|
|
|
|
// We'll build a validated slice by scanning forward.
|
|
// Track pending tool call IDs that need results.
|
|
var result []Message
|
|
var pendingToolCalls map[string]bool // tool call IDs awaiting results
|
|
var toolCallStartIdx int // index in result where current tool sequence started
|
|
|
|
for _, msg := range msgs {
|
|
switch msg.Role {
|
|
case "system", "user":
|
|
// System and user messages are always valid.
|
|
// But if we have pending tool calls, that's a problem — the sequence is broken.
|
|
if len(pendingToolCalls) > 0 {
|
|
// Truncate back to before the incomplete tool sequence
|
|
result = result[:toolCallStartIdx]
|
|
pendingToolCalls = nil
|
|
}
|
|
result = append(result, msg)
|
|
|
|
case "assistant":
|
|
if len(msg.ToolCalls) > 0 {
|
|
// Assistant with tool calls — start tracking
|
|
if len(pendingToolCalls) > 0 {
|
|
// Previous tool calls weren't satisfied — truncate
|
|
result = result[:toolCallStartIdx]
|
|
}
|
|
pendingToolCalls = make(map[string]bool)
|
|
toolCallStartIdx = len(result)
|
|
for _, tc := range msg.ToolCalls {
|
|
pendingToolCalls[tc.ID] = true
|
|
}
|
|
result = append(result, msg)
|
|
} else {
|
|
// Assistant without tool calls
|
|
if len(pendingToolCalls) > 0 {
|
|
// Previous tool calls weren't satisfied — truncate
|
|
result = result[:toolCallStartIdx]
|
|
pendingToolCalls = nil
|
|
}
|
|
result = append(result, msg)
|
|
}
|
|
|
|
case "tool":
|
|
if len(pendingToolCalls) == 0 {
|
|
// Orphan tool result — skip it
|
|
continue
|
|
}
|
|
if !pendingToolCalls[msg.ToolCallID] {
|
|
// Tool result for unknown call — skip it
|
|
continue
|
|
}
|
|
delete(pendingToolCalls, msg.ToolCallID)
|
|
result = append(result, msg)
|
|
}
|
|
}
|
|
|
|
// Final check: if we still have pending tool calls, truncate
|
|
if len(pendingToolCalls) > 0 {
|
|
result = result[:toolCallStartIdx]
|
|
}
|
|
|
|
return result
|
|
}
|