From caf3913347df406887a76f7e9e881112a7ecb788 Mon Sep 17 00:00:00 2001 From: mrbeandev Date: Tue, 17 Feb 2026 11:25:44 +0530 Subject: [PATCH] fix(antigravity): normalize tool calls to avoid empty function names --- pkg/agent/loop.go | 60 +++++++++++++++--- pkg/providers/antigravity_provider.go | 71 ++++++++++++++++++++-- pkg/providers/antigravity_provider_test.go | 56 +++++++++++++++++ pkg/tools/toolloop.go | 60 +++++++++++++++--- 4 files changed, 229 insertions(+), 18 deletions(-) create mode 100644 pkg/providers/antigravity_provider_test.go diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index edbd1d6a3..b90c473f1 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -605,15 +605,20 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M break } - // Log tool calls - toolNames := make([]string, 0, len(response.ToolCalls)) + normalizedToolCalls := make([]providers.ToolCall, 0, len(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) } logger.InfoCF("agent", "LLM requested tool calls", map[string]interface{}{ "tools": toolNames, - "count": len(response.ToolCalls), + "count": len(normalizedToolCalls), "iteration": iteration, }) @@ -622,7 +627,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M Role: "assistant", Content: response.Content, } - for _, tc := range response.ToolCalls { + for _, tc := range normalizedToolCalls { argumentsJSON, _ := json.Marshal(tc.Arguments) thoughtSignature := "" if tc.Function != nil { @@ -630,8 +635,10 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M } assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ - ID: tc.ID, - Type: "function", + ID: tc.ID, + Type: "function", + Name: tc.Name, + Arguments: tc.Arguments, Function: &providers.FunctionCall{ Name: tc.Name, Arguments: string(argumentsJSON), @@ -645,7 +652,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M al.sessions.AddFullMessage(opts.SessionKey, assistantMsg) // Execute tool calls - for _, tc := range response.ToolCalls { + for _, tc := range normalizedToolCalls { // Log tool call with arguments preview argsJSON, _ := json.Marshal(tc.Arguments) argsPreview := utils.Truncate(string(argsJSON), 200) @@ -708,6 +715,45 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, messages []providers.M 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. func (al *AgentLoop) updateToolContexts(channel, chatID string) { // Use ContextualTool interface instead of type assertions diff --git a/pkg/providers/antigravity_provider.go b/pkg/providers/antigravity_provider.go index 15786a2eb..03bc7e190 100644 --- a/pkg/providers/antigravity_provider.go +++ b/pkg/providers/antigravity_provider.go @@ -195,6 +195,7 @@ type antigravityGenConfig struct { func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) antigravityRequest { req := antigravityRequest{} + toolCallNames := make(map[string]string) // Build contents from messages for _, msg := range messages { @@ -205,12 +206,13 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin } case "user": if msg.ToolCallID != "" { + toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames) // Tool result req.Contents = append(req.Contents, antigravityContent{ Role: "user", Parts: []antigravityPart{{ FunctionResponse: &antigravityFunctionResponse{ - Name: msg.ToolCallID, + Name: toolName, Response: map[string]interface{}{ "result": msg.Content, }, @@ -231,10 +233,20 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin content.Parts = append(content.Parts, antigravityPart{Text: msg.Content}) } 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{ FunctionCall: &antigravityFunctionCall{ - Name: tc.Name, - Args: tc.Arguments, + Name: toolName, + Args: toolArgs, }, }) } @@ -242,11 +254,12 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin req.Contents = append(req.Contents, content) } case "tool": + toolName := resolveToolResponseName(msg.ToolCallID, toolCallNames) req.Contents = append(req.Contents, antigravityContent{ Role: "user", Parts: []antigravityPart{{ FunctionResponse: &antigravityFunctionResponse{ - Name: msg.ToolCallID, + Name: toolName, Response: map[string]interface{}{ "result": msg.Content, }, @@ -294,6 +307,56 @@ func (p *AntigravityProvider) buildRequest(messages []Message, tools []ToolDefin 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 --- type antigravityJSONResponse struct { diff --git a/pkg/providers/antigravity_provider_test.go b/pkg/providers/antigravity_provider_test.go new file mode 100644 index 000000000..238765321 --- /dev/null +++ b/pkg/providers/antigravity_provider_test.go @@ -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) + } +} diff --git a/pkg/tools/toolloop.go b/pkg/tools/toolloop.go index 1302079b4..a95710816 100644 --- a/pkg/tools/toolloop.go +++ b/pkg/tools/toolloop.go @@ -83,15 +83,20 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider break } - // 5. Log tool calls - toolNames := make([]string, 0, len(response.ToolCalls)) + normalizedToolCalls := make([]providers.ToolCall, 0, len(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) } logger.InfoCF("toolloop", "LLM requested tool calls", map[string]any{ "tools": toolNames, - "count": len(response.ToolCalls), + "count": len(normalizedToolCalls), "iteration": iteration, }) @@ -100,11 +105,13 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider Role: "assistant", Content: response.Content, } - for _, tc := range response.ToolCalls { + for _, tc := range normalizedToolCalls { argumentsJSON, _ := json.Marshal(tc.Arguments) assistantMsg.ToolCalls = append(assistantMsg.ToolCalls, providers.ToolCall{ - ID: tc.ID, - Type: "function", + ID: tc.ID, + Type: "function", + Name: tc.Name, + Arguments: tc.Arguments, Function: &providers.FunctionCall{ Name: tc.Name, Arguments: string(argumentsJSON), @@ -114,7 +121,7 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider messages = append(messages, assistantMsg) // 7. Execute tool calls - for _, tc := range response.ToolCalls { + for _, tc := range normalizedToolCalls { argsJSON, _ := json.Marshal(tc.Arguments) argsPreview := utils.Truncate(string(argsJSON), 200) 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, }, 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 +}