From 6e634fe3f99bd6f25c4d6164d9e2d3683e38ab6e Mon Sep 17 00:00:00 2001 From: Darley Date: Mon, 23 Feb 2026 14:33:59 +0800 Subject: [PATCH] fix: filter out orphaned tool results from history and current context --- .../kiro/openai/kiro_openai_request.go | 56 +++++++++++++++++++ .../kiro/openai/kiro_openai_request_test.go | 54 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/internal/translator/kiro/openai/kiro_openai_request.go b/internal/translator/kiro/openai/kiro_openai_request.go index 474231b3..79411c42 100644 --- a/internal/translator/kiro/openai/kiro_openai_request.go +++ b/internal/translator/kiro/openai/kiro_openai_request.go @@ -578,6 +578,7 @@ func processOpenAIMessages(messages gjson.Result, modelID, origin string) ([]Kir // Truncate history if too long to prevent Kiro API errors history = truncateHistoryIfNeeded(history) + history, currentToolResults = filterOrphanedToolResults(history, currentToolResults) return history, currentUserMsg, currentToolResults } @@ -593,6 +594,61 @@ func truncateHistoryIfNeeded(history []KiroHistoryMessage) []KiroHistoryMessage return history[len(history)-kiroMaxHistoryMessages:] } +func filterOrphanedToolResults(history []KiroHistoryMessage, currentToolResults []KiroToolResult) ([]KiroHistoryMessage, []KiroToolResult) { + // Remove tool results with no matching tool_use in retained history. + // This happens after truncation when the assistant turn that produced tool_use + // is dropped but a later user/tool_result survives. + validToolUseIDs := make(map[string]bool) + for _, h := range history { + if h.AssistantResponseMessage == nil { + continue + } + for _, tu := range h.AssistantResponseMessage.ToolUses { + validToolUseIDs[tu.ToolUseID] = true + } + } + + for i, h := range history { + if h.UserInputMessage == nil || h.UserInputMessage.UserInputMessageContext == nil { + continue + } + ctx := h.UserInputMessage.UserInputMessageContext + if len(ctx.ToolResults) == 0 { + continue + } + + filtered := make([]KiroToolResult, 0, len(ctx.ToolResults)) + for _, tr := range ctx.ToolResults { + if validToolUseIDs[tr.ToolUseID] { + filtered = append(filtered, tr) + continue + } + log.Debugf("kiro-openai: dropping orphaned tool_result in history[%d]: toolUseId=%s (no matching tool_use)", i, tr.ToolUseID) + } + ctx.ToolResults = filtered + if len(ctx.ToolResults) == 0 && len(ctx.Tools) == 0 { + h.UserInputMessage.UserInputMessageContext = nil + } + } + + if len(currentToolResults) > 0 { + filtered := make([]KiroToolResult, 0, len(currentToolResults)) + for _, tr := range currentToolResults { + if validToolUseIDs[tr.ToolUseID] { + filtered = append(filtered, tr) + continue + } + log.Debugf("kiro-openai: dropping orphaned tool_result in currentMessage: toolUseId=%s (no matching tool_use)", tr.ToolUseID) + } + if len(filtered) != len(currentToolResults) { + log.Infof("kiro-openai: dropped %d orphaned tool_result(s) from currentMessage", len(currentToolResults)-len(filtered)) + } + currentToolResults = filtered + } + + return history, currentToolResults +} + // buildUserMessageFromOpenAI builds a user message from OpenAI format and extracts tool results func buildUserMessageFromOpenAI(msg gjson.Result, modelID, origin string) (KiroUserInputMessage, []KiroToolResult) { content := msg.Get("content") diff --git a/internal/translator/kiro/openai/kiro_openai_request_test.go b/internal/translator/kiro/openai/kiro_openai_request_test.go index 85e95d4a..22953bbc 100644 --- a/internal/translator/kiro/openai/kiro_openai_request_test.go +++ b/internal/translator/kiro/openai/kiro_openai_request_test.go @@ -384,3 +384,57 @@ func TestAssistantEndsConversation(t *testing.T) { t.Error("Expected a 'Continue' message to be created when assistant is last") } } + +func TestFilterOrphanedToolResults_RemovesHistoryAndCurrentOrphans(t *testing.T) { + history := []KiroHistoryMessage{ + { + AssistantResponseMessage: &KiroAssistantResponseMessage{ + Content: "assistant", + ToolUses: []KiroToolUse{ + {ToolUseID: "keep-1", Name: "Read", Input: map[string]interface{}{}}, + }, + }, + }, + { + UserInputMessage: &KiroUserInputMessage{ + Content: "user-with-mixed-results", + UserInputMessageContext: &KiroUserInputMessageContext{ + ToolResults: []KiroToolResult{ + {ToolUseID: "keep-1", Status: "success", Content: []KiroTextContent{{Text: "ok"}}}, + {ToolUseID: "orphan-1", Status: "success", Content: []KiroTextContent{{Text: "bad"}}}, + }, + }, + }, + }, + { + UserInputMessage: &KiroUserInputMessage{ + Content: "user-only-orphans", + UserInputMessageContext: &KiroUserInputMessageContext{ + ToolResults: []KiroToolResult{ + {ToolUseID: "orphan-2", Status: "success", Content: []KiroTextContent{{Text: "bad"}}}, + }, + }, + }, + }, + } + + currentToolResults := []KiroToolResult{ + {ToolUseID: "keep-1", Status: "success", Content: []KiroTextContent{{Text: "ok"}}}, + {ToolUseID: "orphan-3", Status: "success", Content: []KiroTextContent{{Text: "bad"}}}, + } + + filteredHistory, filteredCurrent := filterOrphanedToolResults(history, currentToolResults) + + ctx1 := filteredHistory[1].UserInputMessage.UserInputMessageContext + if ctx1 == nil || len(ctx1.ToolResults) != 1 || ctx1.ToolResults[0].ToolUseID != "keep-1" { + t.Fatalf("expected mixed history message to keep only keep-1, got: %+v", ctx1) + } + + if filteredHistory[2].UserInputMessage.UserInputMessageContext != nil { + t.Fatalf("expected orphan-only history context to be removed") + } + + if len(filteredCurrent) != 1 || filteredCurrent[0].ToolUseID != "keep-1" { + t.Fatalf("expected current tool results to keep only keep-1, got: %+v", filteredCurrent) + } +}