diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 1866a7865879fc83da7a8fdbaa5c87e70fcea1f2..f25cbdc7849c9f9f3d55e34206faaca82834960c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -792,30 +792,7 @@ If not, please feel free to ignore. Again do not mention this message to the use continue } if m.Role == message.Tool { - // Filter out tool results that have no matching tool call. An orphaned - // result causes every subsequent API call to fail validation. - aiMsgs := m.ToAIMessage() - if len(aiMsgs) == 0 { - continue - } - var validParts []fantasy.MessagePart - for _, part := range aiMsgs[0].Content { - tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part) - if !ok { - validParts = append(validParts, part) - continue - } - if _, known := knownToolCallIDs[tr.ToolCallID]; known { - validParts = append(validParts, part) - } else { - slog.Warn("Dropping orphaned tool result with no matching tool call", - "tool_call_id", tr.ToolCallID, - ) - } - } - if len(validParts) > 0 { - msg := aiMsgs[0] - msg.Content = validParts + if msg, ok := filterOrphanedToolResults(m, knownToolCallIDs); ok { history = append(history, msg) } continue @@ -838,6 +815,39 @@ If not, please feel free to ignore. Again do not mention this message to the use return history, files } +// filterOrphanedToolResults converts a tool message to a fantasy.Message, +// dropping any tool result parts whose tool_call_id has no matching tool call +// in the known set. An orphaned result causes API validation to fail on every +// subsequent turn, permanently locking the session. Returns the filtered +// message and true if at least one valid part remains. +func filterOrphanedToolResults(m message.Message, knownToolCallIDs map[string]struct{}) (fantasy.Message, bool) { + aiMsgs := m.ToAIMessage() + if len(aiMsgs) == 0 { + return fantasy.Message{}, false + } + var validParts []fantasy.MessagePart + for _, part := range aiMsgs[0].Content { + tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part) + if !ok { + validParts = append(validParts, part) + continue + } + if _, known := knownToolCallIDs[tr.ToolCallID]; known { + validParts = append(validParts, part) + } else { + slog.Warn("Dropping orphaned tool result with no matching tool call", + "tool_call_id", tr.ToolCallID, + ) + } + } + if len(validParts) == 0 { + return fantasy.Message{}, false + } + msg := aiMsgs[0] + msg.Content = validParts + return msg, true +} + func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) { msgs, err := a.messages.List(ctx, session.ID) if err != nil {