on main help sign in
chore(agent): move filter logic into a function
3e8c148Christian Rocha
internal/agent/agent.go+34−24
@@ -792,30 +792,7 @@ If not, please feel free to ignore. Again do not mention this message to the use
792 continue
792 continue
793 }
793 }
794 if m.Role == message.Tool {
794 if m.Role == message.Tool {
795− // Filter out tool results that have no matching tool call. An orphaned
795+ if msg, ok := filterOrphanedToolResults(m, knownToolCallIDs); ok {
796− // result causes every subsequent API call to fail validation.
797− aiMsgs := m.ToAIMessage()
798− if len(aiMsgs) == 0 {
799− continue
800− }
801− var validParts []fantasy.MessagePart
802− for _, part := range aiMsgs[0].Content {
803− tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
804− if !ok {
805− validParts = append(validParts, part)
806− continue
807− }
808− if _, known := knownToolCallIDs[tr.ToolCallID]; known {
809− validParts = append(validParts, part)
810− } else {
811− slog.Warn("Dropping orphaned tool result with no matching tool call",
812− "tool_call_id", tr.ToolCallID,
813− )
814− }
815− }
816− if len(validParts) > 0 {
817− msg := aiMsgs[0]
818− msg.Content = validParts
819 history = append(history, msg)
796 history = append(history, msg)
820 }
797 }
821 continue
798 continue
@@ -838,6 +815,39 @@ If not, please feel free to ignore. Again do not mention this message to the use
838 return history, files
815 return history, files
839}
816}
840
817
818+// filterOrphanedToolResults converts a tool message to a fantasy.Message,
819+// dropping any tool result parts whose tool_call_id has no matching tool call
820+// in the known set. An orphaned result causes API validation to fail on every
821+// subsequent turn, permanently locking the session. Returns the filtered
822+// message and true if at least one valid part remains.
823+func filterOrphanedToolResults(m message.Message, knownToolCallIDs map[string]struct{}) (fantasy.Message, bool) {
824+ aiMsgs := m.ToAIMessage()
825+ if len(aiMsgs) == 0 {
826+ return fantasy.Message{}, false
827+ }
828+ var validParts []fantasy.MessagePart
829+ for _, part := range aiMsgs[0].Content {
830+ tr, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
831+ if !ok {
832+ validParts = append(validParts, part)
833+ continue
834+ }
835+ if _, known := knownToolCallIDs[tr.ToolCallID]; known {
836+ validParts = append(validParts, part)
837+ } else {
838+ slog.Warn("Dropping orphaned tool result with no matching tool call",
839+ "tool_call_id", tr.ToolCallID,
840+ )
841+ }
842+ }
843+ if len(validParts) == 0 {
844+ return fantasy.Message{}, false
845+ }
846+ msg := aiMsgs[0]
847+ msg.Content = validParts
848+ return msg, true
849+}
850+
841func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
851func (a *sessionAgent) getSessionMessages(ctx context.Context, session session.Session) ([]message.Message, error) {
842 msgs, err := a.messages.List(ctx, session.ID)
852 msgs, err := a.messages.List(ctx, session.ID)
843 if err != nil {
853 if err != nil {