From 2f885c61333a520f16cec1c4adc57846f4778e23 Mon Sep 17 00:00:00 2001 From: Alix-007 <267018309+Alix-007@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:04:59 +0800 Subject: [PATCH] fix(agent): drop malformed empty-id tool call rounds --- pkg/agent/context.go | 21 +++++++++++++++++---- pkg/agent/context_test.go | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 7c311e79b..8d69397f1 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -696,8 +696,10 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message // within the same assistant turn. expectedOrder := make([]string, 0, len(msg.ToolCalls)) expectedSet := make(map[string]struct{}, len(msg.ToolCalls)) + hasEmptyToolCallID := false for _, tc := range msg.ToolCalls { if tc.ID == "" { + hasEmptyToolCallID = true continue } if _, exists := expectedSet[tc.ID]; exists { @@ -707,10 +709,21 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message expectedOrder = append(expectedOrder, tc.ID) } - // If provider output is malformed and tool calls have no IDs, keep the - // assistant turn as-is and let provider-level validation handle it. - if len(expectedOrder) == 0 { - final = append(final, msg) + // A tool-call round with any empty tool_call ID is malformed. Drop the + // entire round, including any contiguous tool results, so we do not + // preserve provider-invalid history. + if hasEmptyToolCallID { + logger.DebugCF("agent", "Dropping assistant tool-call round with empty tool_call ID", map[string]any{ + "tool_call_count": len(msg.ToolCalls), + }) + + j := i + 1 + for ; j < len(sanitized); j++ { + if sanitized[j].Role != "tool" { + break + } + } + i = j - 1 continue } diff --git a/pkg/agent/context_test.go b/pkg/agent/context_test.go index 94c2c27b7..55957647f 100644 --- a/pkg/agent/context_test.go +++ b/pkg/agent/context_test.go @@ -335,3 +335,19 @@ func TestSanitizeHistoryForProvider_ReusedToolCallIDAcrossRounds(t *testing.T) { t.Fatalf("expected both rounds to keep tool_call_id call_1, got %q and %q", result[2].ToolCallID, result[6].ToolCallID) } } + +func TestSanitizeHistoryForProvider_DropsMalformedMixedToolCallIDs(t *testing.T) { + history := []providers.Message{ + msg("user", "do two things"), + assistantWithTools("A", ""), + toolResult("A"), + msg("user", "next question"), + msg("assistant", "answer"), + } + + result := sanitizeHistoryForProvider(history) + if len(result) != 3 { + t.Fatalf("expected 3 messages, got %d: %+v", len(result), roles(result)) + } + assertRoles(t, result, "user", "user", "assistant") +}