From deebe3e7e8c3faed53eb0eebdbd9181cc8c156ea Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 05:59:47 +0900 Subject: [PATCH] fix: multi-tool-call sessions drop 2nd+ tool results sanitizeHistoryForProvider only allowed tool messages when the preceding message was an assistant with tool_calls. For assistant turns with 2+ tool calls, the 2nd tool result's predecessor was the 1st tool result (role "tool"), causing it to be dropped. This made the API see an assistant with N tool_calls but --- pkg/agent/context.go | 7 ++++--- pkg/agent/loop_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index d11e05590..0c20b70e0 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -259,11 +259,12 @@ func sanitizeHistoryForProvider(history []providers.Message) []providers.Message continue } last := sanitized[len(sanitized)-1] - if last.Role != "assistant" || len(last.ToolCalls) == 0 { + // Allow tool results after their assistant or after sibling tool results + if last.Role == "tool" || (last.Role == "assistant" && len(last.ToolCalls) > 0) { + sanitized = append(sanitized, msg) + } else { logger.DebugCF("agent", "Dropping orphaned tool message", map[string]any{}) - continue } - sanitized = append(sanitized, msg) case "assistant": if len(msg.ToolCalls) > 0 { diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 6008599f4..8b7d2a83e 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1679,3 +1679,39 @@ func TestBuildRichStatus_LatestEntryNoInlineResult(t *testing.T) { t.Errorf("expected exactly 1 separator, got %d in:\n%s", sepCount, got) } } + +func TestSanitizeHistoryForProvider_MultiToolCall(t *testing.T) { + // Regression: assistant with 2+ tool_calls had 2nd+ tool results dropped + // because the check only allowed tool after assistant, not after sibling tool. + history := []providers.Message{ + {Role: "user", Content: "hello"}, + {Role: "assistant", Content: "", ToolCalls: []providers.ToolCall{ + {ID: "a", Function: &providers.FunctionCall{Name: "exec"}}, + {ID: "b", Function: &providers.FunctionCall{Name: "read_file"}}, + }}, + {Role: "tool", Content: "ok", ToolCallID: "a"}, + {Role: "tool", Content: "ok", ToolCallID: "b"}, + {Role: "assistant", Content: "done"}, + } + + got := sanitizeHistoryForProvider(history) + + // All 5 messages must survive + if len(got) != 5 { + roles := make([]string, len(got)) + for i, m := range got { + roles[i] = m.Role + } + t.Fatalf("expected 5 messages, got %d: %v", len(got), roles) + } + // Verify both tool results present + toolCount := 0 + for _, m := range got { + if m.Role == "tool" { + toolCount++ + } + } + if toolCount != 2 { + t.Errorf("expected 2 tool results, got %d", toolCount) + } +}