fix(providers/anthropic): drop empty assistant messages

When using the Anthropic API, it expects `tool_result` blocks in a
`user` message to immediately follow the `tool_use` block from the
preceding `assistant` message. However, the agent's logic can
sometimes produce empty `assistant` messages (with no text and no
tool calls) between consecutive tool calls.

This commit updates `buildRequestBody` in the Anthropic Messages
provider to skip empty `assistant` messages, ensuring that the
merged `user` tool results correctly follow their originating
`assistant` tool uses without interruption.

Co-authored-by: TanLuong <28281768+TanLuong@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-03-28 15:16:59 +00:00
parent ff7095e88e
commit aa2eac9741
2 changed files with 37 additions and 0 deletions

View file

@ -245,6 +245,14 @@ func buildRequestBody(
content = append(content, toolUse) content = append(content, toolUse)
} }
// Skip empty assistant messages that have no content and no valid tool calls.
// This prevents breaking the Anthropic API requirement where a user message
// containing tool results must immediately follow the assistant message
// containing the corresponding tool calls.
if len(content) == 0 {
continue
}
apiMessages = append(apiMessages, map[string]any{ apiMessages = append(apiMessages, map[string]any{
"role": "assistant", "role": "assistant",
"content": content, "content": content,

View file

@ -506,6 +506,25 @@ func TestBuildRequestBodyEdgeCases(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "skip empty assistant messages",
messages: []Message{
{Role: "user", Content: "hello"},
{Role: "assistant", Content: "", ToolCalls: []ToolCall{}}, // Should be skipped
{Role: "assistant", Content: "valid message", ToolCalls: []ToolCall{}},
{Role: "assistant", Content: "", ToolCalls: []ToolCall{
{ID: "tool-valid", Name: "test_tool", Arguments: map[string]any{"arg": "value"}},
}},
{Role: "assistant", Content: "", ToolCalls: []ToolCall{
{ID: "tool-empty", Name: "", Arguments: map[string]any{"ignored": true}},
}}, // Should be skipped because tool call is empty and content is empty
},
model: "test-model",
options: map[string]any{
"max_tokens": 8192,
},
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
@ -558,6 +577,16 @@ func TestBuildRequestBodyEdgeCases(t *testing.T) {
t.Fatalf("tool_use id = %v, want %q", gotID, "tool-valid") t.Fatalf("tool_use id = %v, want %q", gotID, "tool-valid")
} }
} }
if tt.name == "skip empty assistant messages" {
messages, ok := got["messages"].([]any)
if !ok {
t.Fatalf("messages is not []any")
}
if len(messages) != 3 {
t.Fatalf("expected 3 API messages (user, assistant with text, assistant with tool_use), got %d: %#v", len(messages), messages)
}
}
}) })
} }
} }