ollie/cmd/olliesrv/internal/backend/validate.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
}