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 <N
results, triggering "tool call result does not follow tool call".

Fix: also allow tool messages after sibling tool messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 05:59:47 +09:00
parent 1e9154f8a5
commit deebe3e7e8
2 changed files with 40 additions and 3 deletions

View file

@ -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 {
logger.DebugCF("agent", "Dropping orphaned tool message", map[string]any{})
continue
}
// 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{})
}
case "assistant":
if len(msg.ToolCalls) > 0 {

View file

@ -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)
}
}