Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions provider/anthropicprovider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,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{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity issue: InformationalOnly not set for server_tool_use blocks

The Python _chat_client.py explicitly marks server-side tool calls as informational-only (_chat_client.py line 1271):

Content.from_function_call(
    call_id=content_block.id,
    name=resolved_tool_name,
    arguments=content_block.input,
    informational_only=content_block.type == "server_tool_use",  # always True for this case
    ...
)

The Go FunctionCallContent emitted here is missing InformationalOnly: true. Without it, the Go tool auto-call harness (autocall.go) will treat this as a locally-executable tool and try to dispatch web_search or other server-side tools as client-side calls — which will fail, since Anthropic already executed them.

Suggested fix:

contents = append(contents, &message.FunctionCallContent{
    CallID:            v.ID,
    Name:              string(v.Name),
    Arguments:         args,
    InformationalOnly: true, // server already executed this tool
    ContentHeader: message.ContentHeader{
        RawRepresentation: v,
    },
})

Upstream reference: python/packages/anthropic/agent_framework_anthropic/_chat_client.py line 1271.

CallID: v.ID,
Name: string(v.Name),
Arguments: args,
ContentHeader: message.ContentHeader{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity gap: InformationalOnly not set on ServerToolUseBlock

The upstream Python implementation sets informational_only=True for every server_tool_use block (_chat_client.py line 1271):

informational_only=content_block.type == "server_tool_use",

This flag tells the tool-autocall harness not to attempt re-execution — the server already ran the tool. Go's FunctionCallContent has the same field (InformationalOnly bool, message/content.go:346), and toolautocall/autocall.go already gates invocation on !fcc.InformationalOnly (lines 240, 403, 654, 1000).

As written, the Go ServerToolUseBlock case emits a FunctionCallContent with InformationalOnly left as false. If toolautocall is active, it will attempt to invoke a locally-registered tool whose name matches the server-tool name (e.g. web_search), which either fails or silently calls the wrong handler.

Suggested fix:

contents = append(contents, &message.FunctionCallContent{
    CallID:            v.ID,
    Name:              string(v.Name),
    Arguments:         args,
    InformationalOnly: true,   // server already executed this; do not re-invoke locally
    ContentHeader: message.ContentHeader{
        RawRepresentation: v,
    },
})

RawRepresentation: v,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity issue: InformationalOnly not set for server_tool_use blocks

The Python implementation (_chat_client.py) sets informational_only=True for server_tool_use content blocks, marking the resulting FunctionCallContent as informational so the tool-autocall harness skips local dispatch (Anthropic already executed the tool server-side).

This PR omits InformationalOnly: true from the FunctionCallContent it creates for ServerToolUseBlock. As a result, toolautocall/autocall.go (which guards on !fcc.InformationalOnly at lines 237, 362, 596, 908, 927, etc.) will treat the server-side invocation as a locally-dispatchable call, search for a registered tool by name, and produce a runtime error when none is found.

Suggested fix:

contents = append(contents, &message.FunctionCallContent{
    CallID:            v.ID,
    Name:              string(v.Name),
    Arguments:         args,
    InformationalOnly: true, // Anthropic executed this server-side; skip local dispatch
    ContentHeader: message.ContentHeader{
        RawRepresentation: v,
    },
})

Upstream reference: python/packages/anthropic/agent_framework_anthropic/_chat_client.py, case "server_tool_use" branch.

})
case anthropic.WebSearchToolResultBlock:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity gap — InformationalOnly not set for server_tool_use

The upstream Python SDK sets informational_only=True explicitly when the content block type is "server_tool_use":

# python/packages/anthropic/agent_framework_anthropic/_chat_client.py, ~line 1256
Content.from_function_call(
    call_id=content_block.id,
    name=resolved_tool_name,
    arguments=content_block.input,
    informational_only=content_block.type == "server_tool_use",  # True for server tools
    raw_representation=content_block,
)

The Go FunctionCallContent type already has an InformationalOnly bool field, but it is not set in this new case. Downstream consumers that check InformationalOnly to decide whether to re-invoke a tool will treat an Anthropic server-tool call the same as a regular client-side tool call, diverging from Python behaviour.

Suggested fix:

contents = append(contents, &message.FunctionCallContent{
    CallID:            v.ID,
    Name:              string(v.Name),
    Arguments:         args,
    InformationalOnly: true,   // server tool was already executed by Anthropic
    ContentHeader: message.ContentHeader{
        RawRepresentation: v,
    },
})

// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity note: web_search_tool_result shape diverges from Python

The Python implementation maps web_search_tool_result blocks to Content.from_function_result(call_id=..., result=content_block.content, ...) — it passes the raw content array through without unpacking individual web search result items into citation annotations.

This PR instead:

  1. Iterates v.Content.AsWebSearchResultBlockArray()
  2. Maps each result to a *message.CitationAnnotation stored in result.Annotations
  3. Sets result.Result = results (the raw array)

This is a richer representation, but it diverges from the upstream Go convention where Annotations live on text content (via ContentHeader) rather than on a FunctionResultContent. The FunctionResultContent struct does not have an Annotations field — the PR is attaching them via the embedded ContentHeader.Annotations, which is typically used for text spans and citations on text blocks.

If cross-SDK consistency matters here, consider aligning with Python by passing result.Result = content_block.content (the raw slice) and omitting the citation-annotation expansion, or opening a follow-up issue to also adopt the richer citation-on-function-result shape in Python.

Upstream reference: python/packages/anthropic/agent_framework_anthropic/_chat_client.py, case "web_search_tool_result" | "web_fetch_tool_result" branch.

Expand Down
85 changes: 85 additions & 0 deletions provider/anthropicprovider/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,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")
}
}

// TestStreamingTextCitationsBecomeAnnotations mirrors
// TestTextCitationsBecomeAnnotations for the streaming path: citations are only
// present on the accumulated text block, so the content_block_stop handler must
Expand Down
Loading