From c6ff2afb7a0591e8caf4bf634667c29b945f6c2c Mon Sep 17 00:00:00 2001 From: Omar Alani Date: Thu, 21 May 2026 17:54:46 -0500 Subject: [PATCH 1/3] fix(assistant): use native anthropic tool names --- internal/assistant/anthropic_internal_test.go | 38 +++++++++++++++++++ internal/assistant/tool_schema.go | 4 +- internal/tool/registry_test.go | 13 +++++++ internal/tool/write.go | 3 ++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/internal/assistant/anthropic_internal_test.go b/internal/assistant/anthropic_internal_test.go index 05d52481..f9289e62 100644 --- a/internal/assistant/anthropic_internal_test.go +++ b/internal/assistant/anthropic_internal_test.go @@ -1,6 +1,7 @@ package assistant import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -42,6 +43,10 @@ func TestAnthropicOAuthPayloadAddsClaudeCodeIdentity(t *testing.T) { assert.Len(t, systemBlocks, 2) assert.Contains(t, systemBlocks[0]["text"], "Claude Code") assert.Equal(t, anthropicTestSystemPrompt, systemBlocks[1][jsonTextKey]) + encodedTools := encodeTestJSON(t, payload["tools"]) + assert.Contains(t, encodedTools, `"name":"Read"`) + assert.Contains(t, encodedTools, `"name":"Write"`) + assert.Contains(t, encodedTools, `"eager_input_streaming":true`) } func TestAnthropicPayloadAddsBudgetThinking(t *testing.T) { @@ -73,6 +78,30 @@ func TestAnthropicPayloadDisablesThinkingWhenOff(t *testing.T) { assert.NotContains(t, payload, "output_config") } +func TestAnthropicPayloadUsesLocalToolNamesForAPIKey(t *testing.T) { + t.Parallel() + + payload := anthropicPayload(testCompletionRequestAuth("sk-ant-api03-secret"), nil) + + encodedTools := encodeTestJSON(t, payload["tools"]) + assert.Contains(t, encodedTools, `"name":"`+jsonReadToolName+`"`) + assert.Contains(t, encodedTools, `"name":"`+jsonWriteToolName+`"`) + assert.NotContains(t, encodedTools, `"name":"`+anthropicReadToolName+`"`) +} + +func TestAnthropicToolCallMapsClaudeCodeNamesToLocalNames(t *testing.T) { + t.Parallel() + + call := anthropicToolCall(testAnthropicToolUseID, "Write", map[string]any{ + jsonPathKey: "hello.txt", + jsonContentKey: "hello", + }) + + assert.Equal(t, jsonWriteToolName, call.Name) + assert.Equal(t, "hello.txt", call.Arguments[jsonPathKey]) + assert.Equal(t, "hello", call.Arguments[jsonContentKey]) +} + func TestAnthropicPayloadAddsAdaptiveThinking(t *testing.T) { t.Parallel() @@ -115,6 +144,15 @@ func TestAnthropicHeadersUseBearerForOAuth(t *testing.T) { assert.NotContains(t, headers["anthropic-beta"], "interleaved-thinking-2025-05-14") } +func encodeTestJSON(t *testing.T, value any) string { + t.Helper() + + encoded, err := json.Marshal(value) + assert.NoError(t, err) + + return string(encoded) +} + func testCompletionRequestAuth(args ...string) *CompletionRequest { provider := "anthropic" apiKey := "" diff --git a/internal/assistant/tool_schema.go b/internal/assistant/tool_schema.go index c7527ab2..8c16c62d 100644 --- a/internal/assistant/tool_schema.go +++ b/internal/assistant/tool_schema.go @@ -137,9 +137,9 @@ func writeToolSchema() map[string]any { jsonPathKey: stringSchema( "Path to create or overwrite, relative to the current workspace or absolute.", ), - "content": stringSchema("Complete file content to write."), + jsonContentKey: stringSchema("Complete file content to write."), }, - jsonRequiredKey: []string{jsonPathKey, "content"}, + jsonRequiredKey: []string{jsonPathKey, jsonContentKey}, } } diff --git a/internal/tool/registry_test.go b/internal/tool/registry_test.go index 18aa50fb..6293af06 100644 --- a/internal/tool/registry_test.go +++ b/internal/tool/registry_test.go @@ -66,6 +66,19 @@ func TestRegistry_ExecuteJSONRunsBashInWorkingDirectory(t *testing.T) { assert.Contains(t, result.Text(), "ok") } +func TestRegistry_ExecuteJSONRejectsEmptyWriteContent(t *testing.T) { + t.Parallel() + + registry := tool.NewRegistry(t.TempDir()) + payload, err := json.Marshal(map[string]any{"path": "empty.txt", "content": ""}) + require.NoError(t, err) + + _, err = registry.ExecuteJSON(context.Background(), "write", payload) + + require.Error(t, err) + assert.Contains(t, err.Error(), "write content is required") +} + func executeTool( ctx context.Context, t *testing.T, diff --git a/internal/tool/write.go b/internal/tool/write.go index e8b6b912..647e1a93 100644 --- a/internal/tool/write.go +++ b/internal/tool/write.go @@ -55,6 +55,9 @@ func (writeTool *WriteTool) Write(ctx context.Context, input WriteInput) (Result if strings.TrimSpace(input.Path) == "" { return emptyToolResult(), oops.In("tool").Code("write_path_required").Errorf("write path is required") } + if strings.TrimSpace(input.Content) == "" { + return emptyToolResult(), oops.In("tool").Code("write_content_required").Errorf("write content is required") + } absolutePath, err := ResolveToCWD(input.Path, writeTool.cwd) if err != nil { return emptyToolResult(), oops.In("tool").Code("write_resolve_path").Wrapf(err, "resolve write path") From 291dd4d4e6138ea7cf2e11fab5da764645681136 Mon Sep 17 00:00:00 2001 From: Omar Alani Date: Thu, 21 May 2026 17:55:30 -0500 Subject: [PATCH 2/3] fix(assistant): support anthropic native tool calls From 4bfae88a25b96fe281a365db084af37e8310b4a0 Mon Sep 17 00:00:00 2001 From: Omar Alani Date: Fri, 22 May 2026 18:37:03 -0500 Subject: [PATCH 3/3] test(assistant): address anthropic review nits --- internal/assistant/anthropic_internal_test.go | 32 +++++++++++++++++-- internal/tool/registry_test.go | 24 +++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/internal/assistant/anthropic_internal_test.go b/internal/assistant/anthropic_internal_test.go index f9289e62..cd80c045 100644 --- a/internal/assistant/anthropic_internal_test.go +++ b/internal/assistant/anthropic_internal_test.go @@ -78,9 +78,35 @@ func TestAnthropicPayloadDisablesThinkingWhenOff(t *testing.T) { assert.NotContains(t, payload, "output_config") } -func TestAnthropicPayloadUsesLocalToolNamesForAPIKey(t *testing.T) { +func TestAnthropicToolNameMapping(t *testing.T) { t.Parallel() + tests := []struct { + run func(t *testing.T) + name string + }{ + { + name: "api key payload keeps local tool names", + run: assertAnthropicPayloadKeepsLocalToolNames, + }, + { + name: "native claude code tool calls map to local names", + run: assertAnthropicToolCallMapsClaudeCodeName, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + testCase.run(t) + }) + } +} + +func assertAnthropicPayloadKeepsLocalToolNames(t *testing.T) { + t.Helper() + payload := anthropicPayload(testCompletionRequestAuth("sk-ant-api03-secret"), nil) encodedTools := encodeTestJSON(t, payload["tools"]) @@ -89,8 +115,8 @@ func TestAnthropicPayloadUsesLocalToolNamesForAPIKey(t *testing.T) { assert.NotContains(t, encodedTools, `"name":"`+anthropicReadToolName+`"`) } -func TestAnthropicToolCallMapsClaudeCodeNamesToLocalNames(t *testing.T) { - t.Parallel() +func assertAnthropicToolCallMapsClaudeCodeName(t *testing.T) { + t.Helper() call := anthropicToolCall(testAnthropicToolUseID, "Write", map[string]any{ jsonPathKey: "hello.txt", diff --git a/internal/tool/registry_test.go b/internal/tool/registry_test.go index 6293af06..ca1348b9 100644 --- a/internal/tool/registry_test.go +++ b/internal/tool/registry_test.go @@ -69,14 +69,28 @@ func TestRegistry_ExecuteJSONRunsBashInWorkingDirectory(t *testing.T) { func TestRegistry_ExecuteJSONRejectsEmptyWriteContent(t *testing.T) { t.Parallel() + tests := []struct { + name string + content string + }{ + {name: "empty", content: ""}, + {name: "whitespace only", content: " \n\t"}, + } + registry := tool.NewRegistry(t.TempDir()) - payload, err := json.Marshal(map[string]any{"path": "empty.txt", "content": ""}) - require.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + payload, err := json.Marshal(map[string]any{"path": "empty.txt", "content": tt.content}) + require.NoError(t, err) - _, err = registry.ExecuteJSON(context.Background(), "write", payload) + _, err = registry.ExecuteJSON(context.Background(), "write", payload) - require.Error(t, err) - assert.Contains(t, err.Error(), "write content is required") + require.Error(t, err) + assert.Contains(t, err.Error(), "write content is required") + }) + } } func executeTool(