fix(antigravity): normalize tool calls to avoid empty function names

This commit is contained in:
mrbeandev 2026-02-17 11:25:44 +05:30
parent d1655d5996
commit caf3913347
4 changed files with 229 additions and 18 deletions

View file

@ -605,15 +605,20 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
break break
} }
// Log tool calls normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
toolNames := make([]string, 0, len(response.ToolCalls))
for _, tc := range response.ToolCalls { for _, tc := range response.ToolCalls {
normalizedToolCalls = append(normalizedToolCalls, normalizeProviderToolCall(tc))
}
// Log tool calls
toolNames := make([]string, 0, len(normalizedToolCalls))
for _, tc := range normalizedToolCalls {
toolNames = append(toolNames, tc.Name) toolNames = append(toolNames, tc.Name)
} }
logger.InfoCF("agent", "LLM requested tool calls", logger.InfoCF("agent", "LLM requested tool calls",
map[string]interface{}{ map[string]interface{}{
"tools": toolNames, "tools": toolNames,
"count": len(response.ToolCalls), "count": len(normalizedToolCalls),
"iteration": iteration, "iteration": iteration,
}) })
@ -622,7 +627,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
Role: "assistant", Role: "assistant",
Content: response.Content, Content: response.Content,
} }
for _, tc := range response.ToolCalls { for _, tc := range normalizedToolCalls {
argumentsJSON, _ := json.Marshal(tc.Arguments) argumentsJSON, _ := json.Marshal(tc.Arguments)
thoughtSignature := "" thoughtSignature := ""
if tc.Function != nil { if tc.Function != nil {
@ -632,6 +637,8 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
ID: tc.ID, ID: tc.ID,
Type: "function", Type: "function",
Name: tc.Name,
Arguments: tc.Arguments,
Function: &providers.FunctionCall{ Function: &providers.FunctionCall{
Name: tc.Name, Name: tc.Name,
Arguments: string(argumentsJSON), Arguments: string(argumentsJSON),
@ -645,7 +652,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
al.sessions.AddFullMessage(opts.SessionKey, assistantMsg) al.sessions.AddFullMessage(opts.SessionKey, assistantMsg)
// Execute tool calls // Execute tool calls
for _, tc := range response.ToolCalls { for _, tc := range normalizedToolCalls {
// Log tool call with arguments preview // Log tool call with arguments preview
argsJSON, _ := json.Marshal(tc.Arguments) argsJSON, _ := json.Marshal(tc.Arguments)
argsPreview := utils.Truncate(string(argsJSON), 200) argsPreview := utils.Truncate(string(argsJSON), 200)
@ -708,6 +715,45 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M
return finalContent, iteration, nil return finalContent, iteration, nil
} }
func normalizeProviderToolCall(tc providers.ToolCall) providers.ToolCall {
normalized := tc
if normalized.Name == "" && normalized.Function != nil {
normalized.Name = normalized.Function.Name
}
if normalized.Arguments == nil {
normalized.Arguments = map[string]interface{}{}
}
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
normalized.Arguments = parsed
}
}
argsJSON, _ := json.Marshal(normalized.Arguments)
if normalized.Function == nil {
normalized.Function = &providers.FunctionCall{
Name: normalized.Name,
Arguments: string(argsJSON),
}
} else {
if normalized.Function.Name == "" {
normalized.Function.Name = normalized.Name
}
if normalized.Name == "" {
normalized.Name = normalized.Function.Name
}
if normalized.Function.Arguments == "" {
normalized.Function.Arguments = string(argsJSON)
}
}
return normalized
}
// updateToolContexts updates the context for tools that need channel/chatID info. // updateToolContexts updates the context for tools that need channel/chatID info.
func (al *AgentLoop) updateToolContexts(channel, chatID string) { func (al *AgentLoop) updateToolContexts(channel, chatID string) {
// Use ContextualTool interface instead of type assertions // Use ContextualTool interface instead of type assertions

View file

@ -195,6 +195,7 @@ type antigravityGenConfig struct {
func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest { func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest {
req := antigravityRequest{} req := antigravityRequest{}
toolCallNames := make(map[string]string)
// Build contents from messages // Build contents from messages
for _, msg := range messages { for _, msg := range messages {
@ -205,12 +206,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
} }
case "user": case "user":
if msg.ToolCallID != "" { if msg.ToolCallID != "" {
toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames)
// Tool result // Tool result
req.Contents = append(req.Contents, antigravityContent{ req.Contents = append(req.Contents, antigravityContent{
Role: "user", Role: "user",
Parts: []antigravityPart{{ Parts: []antigravityPart{{
FunctionResponse: &antigravityFunctionResponse{ FunctionResponse: &antigravityFunctionResponse{
Name: msg.ToolCallID, Name: toolName,
Response: map[string]interface{}{ Response: map[string]interface{}{
"result": msg.Content, "result": msg.Content,
}, },
@ -231,10 +233,20 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
content.Parts = append(content.Parts, antigravityPart{Text: msg.Content}) content.Parts = append(content.Parts, antigravityPart{Text: msg.Content})
} }
for _, tc := range msg.ToolCalls { for _, tc := range msg.ToolCalls {
toolName, toolArgs := normalizeStoredToolCall(tc)
if toolName == "" {
logger.WarnCF("provider.antigravity", "Skipping tool call with empty name in history", map[string]interface{}{
"tool_call_id": tc.ID,
})
continue
}
if tc.ID != "" {
toolCallNames[tc.ID] = toolName
}
content.Parts = append(content.Parts, antigravityPart{ content.Parts = append(content.Parts, antigravityPart{
FunctionCall: &antigravityFunctionCall{ FunctionCall: &antigravityFunctionCall{
Name: tc.Name, Name: toolName,
Args: tc.Arguments, Args: toolArgs,
}, },
}) })
} }
@ -242,11 +254,12 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
req.Contents = append(req.Contents, content) req.Contents = append(req.Contents, content)
} }
case "tool": case "tool":
toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames)
req.Contents = append(req.Contents, antigravityContent{ req.Contents = append(req.Contents, antigravityContent{
Role: "user", Role: "user",
Parts: []antigravityPart{{ Parts: []antigravityPart{{
FunctionResponse: &antigravityFunctionResponse{ FunctionResponse: &antigravityFunctionResponse{
Name: msg.ToolCallID, Name: toolName,
Response: map[string]interface{}{ Response: map[string]interface{}{
"result": msg.Content, "result": msg.Content,
}, },
@ -294,6 +307,56 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin
return req return req
} }
func normalizeStoredToolCall(tc ToolCall) (string, map[string]interface{}) {
name := tc.Name
args := tc.Arguments
if name == "" && tc.Function != nil {
name = tc.Function.Name
}
if args == nil {
args = map[string]interface{}{}
}
if len(args) == 0 && tc.Function != nil && tc.Function.Arguments != "" {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(tc.Function.Arguments), &parsed); err == nil && parsed != nil {
args = parsed
}
}
return name, args
}
func resolveToolResponseName(toolCallID string, toolCallNames map[string]string) string {
if toolCallID == "" {
return ""
}
if name, ok := toolCallNames[toolCallID]; ok && name != "" {
return name
}
return inferToolNameFromCallID(toolCallID)
}
func inferToolNameFromCallID(toolCallID string) string {
if !strings.HasPrefix(toolCallID, "call_") {
return toolCallID
}
rest := strings.TrimPrefix(toolCallID, "call_")
if idx := strings.LastIndex(rest, "_"); idx > 0 {
candidate := rest[:idx]
if candidate != "" {
return candidate
}
}
return toolCallID
}
// --- Response parsing --- // --- Response parsing ---
type antigravityJSONResponse struct { type antigravityJSONResponse struct {

View file

@ -0,0 +1,56 @@
package providers
import "testing"
func TestBuildRequestUsesFunctionFieldsWhenToolCallNameMissing(t *testing.T) {
p := &AntigravityProvider{}
messages := []Message{
{
Role: "assistant",
ToolCalls: []ToolCall{{
ID: "call_read_file_123",
Function: &FunctionCall{
Name: "read_file",
Arguments: `{"path":"README.md"}`,
},
}},
},
{
Role: "tool",
ToolCallID: "call_read_file_123",
Content: "ok",
},
}
req := p.buildRequest(messages, nil, "", nil)
if len(req.Contents) != 2 {
t.Fatalf("expected 2 contents, got %d", len(req.Contents))
}
modelPart := req.Contents[0].Parts[0]
if modelPart.FunctionCall == nil {
t.Fatal("expected functionCall in assistant message")
}
if modelPart.FunctionCall.Name != "read_file" {
t.Fatalf("expected functionCall name read_file, got %q", modelPart.FunctionCall.Name)
}
if got := modelPart.FunctionCall.Args["path"]; got != "README.md" {
t.Fatalf("expected functionCall args[path] to be README.md, got %v", got)
}
toolPart := req.Contents[1].Parts[0]
if toolPart.FunctionResponse == nil {
t.Fatal("expected functionResponse in tool message")
}
if toolPart.FunctionResponse.Name != "read_file" {
t.Fatalf("expected functionResponse name read_file, got %q", toolPart.FunctionResponse.Name)
}
}
func TestResolveToolResponseNameInfersNameFromGeneratedCallID(t *testing.T) {
got := resolveToolResponseName("call_search_docs_999", map[string]string{})
if got != "search_docs" {
t.Fatalf("expected inferred tool name search_docs, got %q", got)
}
}

View file

@ -83,15 +83,20 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
break break
} }
// 5. Log tool calls normalizedToolCalls := make([]providers.ToolCall, 0, len(response.ToolCalls))
toolNames := make([]string, 0, len(response.ToolCalls))
for _, tc := range response.ToolCalls { for _, tc := range response.ToolCalls {
normalizedToolCalls = append(normalizedToolCalls, normalizeProviderToolCall(tc))
}
// 5. Log tool calls
toolNames := make([]string, 0, len(normalizedToolCalls))
for _, tc := range normalizedToolCalls {
toolNames = append(toolNames, tc.Name) toolNames = append(toolNames, tc.Name)
} }
logger.InfoCF("toolloop", "LLM requested tool calls", logger.InfoCF("toolloop", "LLM requested tool calls",
map[string]any{ map[string]any{
"tools": toolNames, "tools": toolNames,
"count": len(response.ToolCalls), "count": len(normalizedToolCalls),
"iteration": iteration, "iteration": iteration,
}) })
@ -100,11 +105,13 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
Role: "assistant", Role: "assistant",
Content: response.Content, Content: response.Content,
} }
for _, tc := range response.ToolCalls { for _, tc := range normalizedToolCalls {
argumentsJSON, _ := json.Marshal(tc.Arguments) argumentsJSON, _ := json.Marshal(tc.Arguments)
assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{
ID: tc.ID, ID: tc.ID,
Type: "function", Type: "function",
Name: tc.Name,
Arguments: tc.Arguments,
Function: &providers.FunctionCall{ Function: &providers.FunctionCall{
Name: tc.Name, Name: tc.Name,
Arguments: string(argumentsJSON), Arguments: string(argumentsJSON),
@ -114,7 +121,7 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
messages = append(messages, assistantMsg) messages = append(messages, assistantMsg)
// 7. Execute tool calls // 7. Execute tool calls
for _, tc := range response.ToolCalls { for _, tc := range normalizedToolCalls {
argsJSON, _ := json.Marshal(tc.Arguments) argsJSON, _ := json.Marshal(tc.Arguments)
argsPreview := utils.Truncate(string(argsJSON), 200) argsPreview := utils.Truncate(string(argsJSON), 200)
logger.InfoCF("toolloop", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview), logger.InfoCF("toolloop", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview),
@ -152,3 +159,42 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider
Iterations: iteration, Iterations: iteration,
}, nil }, nil
} }
func normalizeProviderToolCall(tc providers.ToolCall) providers.ToolCall {
normalized := tc
if normalized.Name == "" && normalized.Function != nil {
normalized.Name = normalized.Function.Name
}
if normalized.Arguments == nil {
normalized.Arguments = map[string]interface{}{}
}
if len(normalized.Arguments) == 0 && normalized.Function != nil && normalized.Function.Arguments != "" {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(normalized.Function.Arguments), &parsed); err == nil && parsed != nil {
normalized.Arguments = parsed
}
}
argsJSON, _ := json.Marshal(normalized.Arguments)
if normalized.Function == nil {
normalized.Function = &providers.FunctionCall{
Name: normalized.Name,
Arguments: string(argsJSON),
}
} else {
if normalized.Function.Name == "" {
normalized.Function.Name = normalized.Name
}
if normalized.Name == "" {
normalized.Name = normalized.Function.Name
}
if normalized.Function.Arguments == "" {
normalized.Function.Arguments = string(argsJSON)
}
}
return normalized
}