From e63be0dbe2f83527cde42414f49da2f13e45708b Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Thu, 23 Jul 2026 12:22:52 +0530 Subject: [PATCH] Surface Anthropic server_tool_use and web_search_tool_result blocks buildBlock only switched on TextBlock/ThinkingBlock/RedactedThinkingBlock/ ToolUseBlock, so server-side web search response blocks were silently dropped. Add cases for ServerToolUseBlock (as a FunctionCallContent) and WebSearchToolResultBlock (as a FunctionResultContent whose citations point at each source), mirroring the Python SDK which parses server_tool_use and web_search_tool_result. --- provider/anthropicprovider/agent.go | 49 ++++++++++++++ provider/anthropicprovider/agent_test.go | 85 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index 82822f09..2482c53a 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -254,6 +254,55 @@ func (a *client) buildBlock(index int, v any, contents []message.Content, functi Name: v.Name, Arguments: string(v.Input), } + case anthropic.ServerToolUseBlock: + // Server-side tool invocations (e.g. web_search) are executed by + // Anthropic itself. Surface them as function calls so callers can + // observe the request, mirroring the client-side ToolUseBlock handling + // and the Python SDK's server_tool_use parsing. Unlike client tool + // calls these are appended in place (not via the functions map, which + // is nil on the streaming path) since Anthropic already ran them. + var args string + if v.Input != nil { + if b, err := json.Marshal(v.Input); err == nil { + args = string(b) + } + } + contents = append(contents, &message.FunctionCallContent{ + CallID: v.ID, + Name: string(v.Name), + Arguments: args, + ContentHeader: message.ContentHeader{ + RawRepresentation: v, + }, + }) + case anthropic.WebSearchToolResultBlock: + // The paired result for a server-side web search. Surface it as a + // function result whose citations point at each source, matching the + // Python SDK's web_search_tool_result handling. + result := &message.FunctionResultContent{ + CallID: v.ToolUseID, + ContentHeader: message.ContentHeader{ + RawRepresentation: v, + }, + } + if v.Content.Type == "web_search_tool_result_error" { + searchErr := v.Content.AsResponseWebSearchToolResultError() + result.Error = fmt.Errorf("web search failed: %s", searchErr.ErrorCode) + result.Result = string(searchErr.ErrorCode) + } else { + results := v.Content.AsWebSearchResultBlockArray() + var annotations []message.Annotation + for _, r := range results { + annotations = append(annotations, &message.CitationAnnotation{ + Title: r.Title, + URL: r.URL, + RawRepresentation: r, + }) + } + result.Annotations = annotations + result.Result = results + } + contents = append(contents, result) } return contents } diff --git a/provider/anthropicprovider/agent_test.go b/provider/anthropicprovider/agent_test.go index 7ffecea2..befadea2 100644 --- a/provider/anthropicprovider/agent_test.go +++ b/provider/anthropicprovider/agent_test.go @@ -285,6 +285,91 @@ func TestTextCitationsBecomeAnnotations(t *testing.T) { } } +func TestServerToolUseAndWebSearchResultBlocks(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{ + "id":"msg_server_tool", + "type":"message", + "role":"assistant", + "model":"claude-3-5-sonnet-20241022", + "stop_reason":"end_turn", + "stop_sequence":null, + "content":[ + { + "type":"server_tool_use", + "id":"srvtoolu_01", + "name":"web_search", + "input":{"query":"agent framework"} + }, + { + "type":"web_search_tool_result", + "tool_use_id":"srvtoolu_01", + "content":[{ + "type":"web_search_result", + "title":"Example Result", + "url":"https://example.com/result", + "encrypted_content":"enc", + "page_age":"1 day" + }] + } + ], + "usage":{"input_tokens":10,"output_tokens":5} + }`) + })) + defer server.Close() + + a := newTestClient(t, server) + resp, err := a.RunText(t.Context(), "search the web").Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var call *message.FunctionCallContent + var result *message.FunctionResultContent + for content := range resp.Contents() { + switch c := content.(type) { + case *message.FunctionCallContent: + call = c + case *message.FunctionResultContent: + result = c + } + } + + if call == nil { + t.Fatal("expected a FunctionCallContent for the server_tool_use block") + } + if call.CallID != "srvtoolu_01" { + t.Errorf("call CallID = %q, want %q", call.CallID, "srvtoolu_01") + } + if call.Name != "web_search" { + t.Errorf("call Name = %q, want %q", call.Name, "web_search") + } + if !strings.Contains(call.Arguments, "agent framework") { + t.Errorf("call Arguments = %q, want it to contain the query", call.Arguments) + } + + if result == nil { + t.Fatal("expected a FunctionResultContent for the web_search_tool_result block") + } + if result.CallID != "srvtoolu_01" { + t.Errorf("result CallID = %q, want %q", result.CallID, "srvtoolu_01") + } + if len(result.Annotations) != 1 { + t.Fatalf("result annotations length = %d, want 1", len(result.Annotations)) + } + citation, ok := result.Annotations[0].(*message.CitationAnnotation) + if !ok { + t.Fatalf("annotation type = %T, want *message.CitationAnnotation", result.Annotations[0]) + } + if citation.URL != "https://example.com/result" { + t.Errorf("citation URL = %q, want %q", citation.URL, "https://example.com/result") + } + if citation.Title != "Example Result" { + t.Errorf("citation Title = %q, want %q", citation.Title, "Example Result") + } +} + // TestStructuredOutput_NonStreaming verifies that passing agent.WithStructuredOutput // with a typed struct causes the provider to: // 1. Send output_config.format with type "json_schema" and a schema derived