Skip to content

fix(ai-anthropic): preserve web_search/web_fetch results across turns (#839)#855

Open
tombeckenham wants to merge 2 commits into
mainfrom
839-anthropic-web_search-tool-results-are-not-preserved-across-turns-in-streamprocessor
Open

fix(ai-anthropic): preserve web_search/web_fetch results across turns (#839)#855
tombeckenham wants to merge 2 commits into
mainfrom
839-anthropic-web_search-tool-results-are-not-preserved-across-turns-in-streamprocessor

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Anthropic web_search / web_fetch results were not preserved across turns when using chat() + StreamProcessor (#839). The adapter accumulated the server_tool_use and *_tool_result blocks during streaming but emitted nothing for them, so the evidence never became part of the assistant message and never round-tripped — a follow-up turn could no longer cite the prior sources. The official Anthropic SDK preserves them because you push the assistant turn's full content (including those blocks) back into the next request.

These blocks were deliberately dropped because routing a server-executed tool through the normal TOOL_CALL_* path makes the agent loop try to execute web_search client-side (it's a ProviderTool with no execute) and hang.

Fix — provider-executed tool calls

Server tools now stream as a provider-executed tool call carrying the raw result, which the agent loop skips and the adapter replays verbatim.

Core (@tanstack/ai)

  • ProviderExecutedToolMetadata convention + isProviderExecutedToolCall / getProviderExecutedMetadata helpers (exported).
  • getPendingToolCallsFromMessages skips provider-executed calls so the loop never runs them.
  • StreamProcessor.areAllToolsComplete treats them as complete; isToolCallPartErrored hardened against parts-less seeded initialMessages (the latent crash this change exposed — exactly the issue's new StreamProcessor({ initialMessages: messages }) pattern).

Anthropic adapter

  • Streaming: buffers each server_tool_use and, when its matching result block arrives, emits one TOOL_CALL_START/END pair tagged { providerExecuted: true, anthropic: { serverToolType, resultBlockType, result } }.
  • formatMessages: replays those as the original server_tool_use + *_tool_result blocks in the assistant content.

Test plan

  • Unit: updated the inverted server-tool assertions, added a full round-trip test (stream → StreamProcessor → second turn preserves the blocks), and a core converter test. 125 anthropic + 1107 core tests pass.
  • Live (gold standard): ran the issue's exact two-turn repro against the real Anthropic API — turn 2 now lists the source URLs instead of "I cannot see the previous web search evidence."
  • Full pnpm test:pr: green across 33 projects (sherif, knip, docs, kiira, eslint, lib, types, build).
  • Docs: added a multi-turn-persistence section to docs/tools/provider-tools.md (per the source↔docs coupling rule).

No e2e added — aimock can't synthesize server_tool_use blocks, so unit + live coverage stands in (noted in the changeset).

Closes #839

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Preserved provider-run web search/fetch evidence across chat turns.
    • Prevented provider-executed tool calls from being treated as pending or re-executed client-side.
    • Improved streaming so server tool calls/results round-trip reliably.
  • New Features

    • Exposed provider-executed tool metadata helpers for detecting and reading provider-run tool call metadata.
  • Documentation

    • Added “Multi-turn persistence” guidance with examples for continuing conversations using prior provider-native tool results.

Anthropic server-tool blocks (server_tool_use + *_tool_result) were dropped
during streaming, so a follow-up turn could no longer see prior web-search
evidence (#839). They now stream as a provider-executed tool call carrying the
raw result, which the agent loop skips and the adapter replays verbatim into
the next request.

- core: add ProviderExecutedToolMetadata convention + isProviderExecutedToolCall
  / getProviderExecutedMetadata helpers; skip provider-executed calls in the
  agent loop; treat them as complete in StreamProcessor; guard isToolCallPartErrored
  against parts-less seeded messages
- anthropic: emit server tools as provider-executed tool calls during streaming;
  replay server_tool_use + result blocks in formatMessages
- docs: provider-tools multi-turn persistence section
- tests: round-trip coverage + inverted server-tool assertions

Closes #839

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04397c9f-c787-4184-9e81-d22d4376c74f

📥 Commits

Reviewing files that changed from the base of the PR and between c964f0f and 6deab10.

📒 Files selected for processing (1)
  • testing/e2e/tests/anthropic-server-tool.spec.ts

📝 Walkthrough

Walkthrough

Adds provider-executed tool metadata and Anthropic adapter support so web_search and web_fetch results persist across streamed turns. It also updates chat/message processing, tests, docs, and package exports around the new round-trip behavior.

Changes

Provider-executed Anthropic tools

Layer / File(s) Summary
Metadata contract and exports
packages/ai/src/types.ts, packages/ai/src/utilities/provider-executed.ts, packages/ai/src/index.ts, .changeset/anthropic-server-tool-roundtrip.md
Adds ProviderExecutedToolMetadata, helper predicates, root re-exports, and the release note entry for the provider-executed tool convention.
Anthropic server-tool replay
packages/ai-anthropic/src/adapters/text.ts
Replays provider-executed web_search / web_fetch calls as Anthropic server_tool_use plus web_*_tool_result blocks and emits matching streamed tool events from buffered server-tool data.
Tool completion checks
packages/ai/src/activities/chat/stream/processor.ts, packages/ai/src/activities/chat/index.ts
Skips provider-executed tool calls in pending-tool detection, treats them as complete in StreamProcessor, and guards errored-part detection when initial messages lack parts.
Tests and docs
packages/ai-anthropic/tests/anthropic-adapter.test.ts, packages/ai/tests/messages.test.ts, docs/tools/provider-tools.md, docs/config.json
Adds coverage for provider-executed tool metadata, turn-to-turn web_search replay, and updates the provider-tools docs section and metadata timestamp.

Sequence Diagram(s)

sequenceDiagram
  participant chat
  participant anthropicText
  participant processAnthropicStream
  participant StreamProcessor
  chat->>anthropicText: formatMessages() with providerExecuted metadata
  anthropicText->>processAnthropicStream: emit server_tool_use and web_*_tool_result blocks
  processAnthropicStream->>StreamProcessor: TOOL_CALL_START / TOOL_CALL_END
  StreamProcessor->>chat: getMessages() for the next turn
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TanStack/ai#596: Both PRs adjust the chat/stream tool-call lifecycle so server-side tool results are reflected in completed tool-call state and output.
  • TanStack/ai#606: Both PRs change Anthropic streaming handling around server_tool_use and web_fetch_tool_result / web_search_tool_result.
  • TanStack/ai#727: Both PRs touch packages/ai/src/activities/chat/stream/processor.ts and the tool-call error/completion path.

Suggested reviewers

  • AlemTuzlak

Poem

A bunny hopped through streaming night,
With tool-call crumbs held snug and tight.
Web search sparkles now survive,
Across each turn, they stay alive.
Thump-thump — the evidence takes flight. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the Anthropic web_search/web_fetch persistence fix.
Description check ✅ Passed The description covers the bug, fix, and test plan; only the optional checklist and release-impact template fields are missing.
Linked Issues check ✅ Passed The changes implement provider-executed server-tool replay and keep Anthropic web evidence across turns as requested in #839.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the docs, tests, and adapter updates all support the same server-tool persistence fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 839-anthropic-web_search-tool-results-are-not-preserved-across-turns-in-streamprocessor

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

testing/e2e/tests/anthropic-server-tool.spec.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/tests/anthropic-server-tool.spec.ts


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

9 package(s) bumped directly, 15 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-react-ui 0.8.11 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.10 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai-angular 0.1.11 → 0.2.0 Changeset
@tanstack/ai-client 0.18.6 → 0.19.0 Changeset
@tanstack/ai-react 0.15.15 → 0.16.0 Changeset
@tanstack/ai-solid 0.13.15 → 0.14.0 Changeset
@tanstack/ai-svelte 0.13.15 → 0.14.0 Changeset
@tanstack/ai-vue 0.13.15 → 0.14.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai 0.37.0 → 0.37.1 Changeset
@tanstack/ai-anthropic 0.15.10 → 0.15.11 Changeset
@tanstack/ai-grok 0.14.4 → 0.14.5 Changeset
@tanstack/ai-code-mode 0.3.1 → 0.3.2 Dependent
@tanstack/ai-code-mode-skills 0.3.4 → 0.3.5 Dependent
@tanstack/ai-devtools-core 0.4.18 → 0.4.19 Dependent
@tanstack/ai-fal 0.9.6 → 0.9.7 Dependent
@tanstack/ai-isolate-cloudflare 0.2.31 → 0.2.32 Dependent
@tanstack/ai-isolate-node 0.1.40 → 0.1.41 Dependent
@tanstack/ai-isolate-quickjs 0.1.40 → 0.1.41 Dependent
@tanstack/ai-mcp 0.1.10 → 0.1.11 Dependent
@tanstack/ai-preact 0.9.15 → 0.9.16 Dependent
@tanstack/ai-vue-ui 0.2.27 → 0.2.28 Dependent
@tanstack/preact-ai-devtools 0.1.61 → 0.1.62 Dependent
@tanstack/react-ai-devtools 0.2.61 → 0.2.62 Dependent
@tanstack/solid-ai-devtools 0.2.61 → 0.2.62 Dependent

@nx-cloud

nx-cloud Bot commented Jun 26, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 6deab10

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-26 10:12:50 UTC

@nx-cloud

nx-cloud Bot commented Jun 26, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c964f0f

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 25s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-26 08:30:18 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@855

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@855

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@855

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@855

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@855

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@855

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@855

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@855

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@855

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@855

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@855

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@855

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@855

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@855

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@855

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@855

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@855

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@855

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@855

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@855

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@855

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@855

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@855

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@855

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@855

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@855

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@855

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@855

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@855

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@855

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@855

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@855

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@855

commit: 6deab10

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/tools/provider-tools.md`:
- Around line 65-83: The provider-tools example is replaying turn 2 with only
the streamed assistant output, so the original user prompt is lost. Update the
StreamProcessor usage in the documented chat/StreamProcessor flow to seed it
with the full first-turn UIMessage array via initialMessages, and reuse that
same firstTurnMessages value for both the initial chat() call and the follow-up
messages spread so the conversation is replayed end-to-end.

In `@packages/ai-anthropic/src/adapters/text.ts`:
- Around line 89-114: readAnthropicServerToolMetadata currently validates
serverToolType and resultBlockType independently, which allows mismatched
Anthropic server-tool pairs to slip through. Tighten the checks in
readAnthropicServerToolMetadata and the downstream formatMessages path so only
known valid combinations of serverToolType and resultBlockType are accepted, and
return null for any persisted metadata where the pair does not match an allowed
server-tool/result pairing.

In `@packages/ai-anthropic/tests/anthropic-adapter.test.ts`:
- Around line 1166-1170: The `chunks.find(...)` usage in
`anthropic-adapter.test.ts` is relying on redundant casts for the
`TOOL_CALL_START` event, even though `ToolCallStartEvent` already includes
`toolCallId` and `metadata`. Update the `serverStart` lookup to use a type
predicate in the `find()` callback, following the pattern already used elsewhere
in this test file, so the result narrows naturally without any `as (StreamChunk
& { metadata?: Record<string, unknown> }) | undefined` assertion.

In `@packages/ai/src/activities/chat/stream/processor.ts`:
- Around line 405-413: areAllToolsComplete() still assumes lastAssistant has
parts and can crash on seeded ModelMessage-shaped assistant messages. Update
this helper to guard against a missing parts array before iterating or
dereferencing it, similar to the existing handling in isToolCallPartErrored(),
and make sure the provider-executed tool-call check in processor.ts remains
reachable after the normalization/guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6313ca57-5134-43ac-9b83-7eb2fd3c47af

📥 Commits

Reviewing files that changed from the base of the PR and between 33acdd4 and c964f0f.

📒 Files selected for processing (11)
  • .changeset/anthropic-server-tool-roundtrip.md
  • docs/config.json
  • docs/tools/provider-tools.md
  • packages/ai-anthropic/src/adapters/text.ts
  • packages/ai-anthropic/tests/anthropic-adapter.test.ts
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/src/index.ts
  • packages/ai/src/types.ts
  • packages/ai/src/utilities/provider-executed.ts
  • packages/ai/tests/messages.test.ts

Comment on lines +65 to +83
const processor = new StreamProcessor()
for await (const chunk of chat({
adapter,
tools,
messages: [{ role: 'user', content: 'Find two sources on the drone market.' }],
})) {
processor.processChunk(chunk)
}
processor.finalizeStream()

// The follow-up turn can still cite the previous search results.
const followUp = chat({
adapter,
tools,
messages: [
...processor.getMessages(),
{ role: 'user', content: 'List the exact sources you used.' },
],
})

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Seed StreamProcessor with the first-turn messages before replaying turn 2.

As written, processor.getMessages() will only contain the streamed assistant turn, so the follow-up call drops the original user prompt. Reuse a UIMessage-shaped firstTurnMessages array for both chat() and new StreamProcessor({ initialMessages: ... }) so the example actually replays the full conversation.

Suggested doc fix
-const processor = new StreamProcessor()
+const firstTurnMessages = [
+  {
+    id: 'user-1',
+    role: 'user',
+    parts: [{ type: 'text', content: 'Find two sources on the drone market.' }],
+  },
+]
+
+const processor = new StreamProcessor({ initialMessages: firstTurnMessages })
 for await (const chunk of chat({
   adapter,
   tools,
-  messages: [{ role: 'user', content: 'Find two sources on the drone market.' }],
+  messages: firstTurnMessages,
 })) {
   processor.processChunk(chunk)
 }
 processor.finalizeStream()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const processor = new StreamProcessor()
for await (const chunk of chat({
adapter,
tools,
messages: [{ role: 'user', content: 'Find two sources on the drone market.' }],
})) {
processor.processChunk(chunk)
}
processor.finalizeStream()
// The follow-up turn can still cite the previous search results.
const followUp = chat({
adapter,
tools,
messages: [
...processor.getMessages(),
{ role: 'user', content: 'List the exact sources you used.' },
],
})
const firstTurnMessages = [
{
id: 'user-1',
role: 'user',
parts: [{ type: 'text', content: 'Find two sources on the drone market.' }],
},
]
const processor = new StreamProcessor({ initialMessages: firstTurnMessages })
for await (const chunk of chat({
adapter,
tools,
messages: firstTurnMessages,
})) {
processor.processChunk(chunk)
}
processor.finalizeStream()
// The follow-up turn can still cite the previous search results.
const followUp = chat({
adapter,
tools,
messages: [
...processor.getMessages(),
{ role: 'user', content: 'List the exact sources you used.' },
],
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/tools/provider-tools.md` around lines 65 - 83, The provider-tools
example is replaying turn 2 with only the streamed assistant output, so the
original user prompt is lost. Update the StreamProcessor usage in the documented
chat/StreamProcessor flow to seed it with the full first-turn UIMessage array
via initialMessages, and reuse that same firstTurnMessages value for both the
initial chat() call and the follow-up messages spread so the conversation is
replayed end-to-end.

Comment on lines +89 to +114
function readAnthropicServerToolMetadata(
metadata: unknown,
): AnthropicServerToolMetadata | null {
if (typeof metadata !== 'object' || metadata === null) return null
const outer = metadata as { providerExecuted?: unknown; anthropic?: unknown }
if (outer.providerExecuted !== true) return null
const inner = outer.anthropic
if (typeof inner !== 'object' || inner === null) return null
const { serverToolType, resultBlockType, result } = inner as {
serverToolType?: unknown
resultBlockType?: unknown
result?: unknown
}
if (
typeof serverToolType !== 'string' ||
(resultBlockType !== 'web_search_tool_result' &&
resultBlockType !== 'web_fetch_tool_result')
) {
return null
}
return {
// Validated as a string above; widen back to the SDK's tool-name union.
serverToolType: serverToolType as ServerToolUseBlockParam['name'],
resultBlockType,
result,
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject mismatched Anthropic server-tool metadata pairs.

readAnthropicServerToolMetadata() currently accepts any string serverToolType as long as resultBlockType is one of the two web_* values. That means a persisted message with a mismatched pair will pass validation and formatMessages() will replay an impossible server_tool_use/*_tool_result combination back to Anthropic on the next turn.

Suggested hardening
-  if (
-    typeof serverToolType !== 'string' ||
-    (resultBlockType !== 'web_search_tool_result' &&
-      resultBlockType !== 'web_fetch_tool_result')
-  ) {
+  const isWebSearchPair =
+    serverToolType === 'web_search' &&
+    resultBlockType === 'web_search_tool_result'
+  const isWebFetchPair =
+    serverToolType === 'web_fetch' &&
+    resultBlockType === 'web_fetch_tool_result'
+  if (!isWebSearchPair && !isWebFetchPair) {
     return null
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function readAnthropicServerToolMetadata(
metadata: unknown,
): AnthropicServerToolMetadata | null {
if (typeof metadata !== 'object' || metadata === null) return null
const outer = metadata as { providerExecuted?: unknown; anthropic?: unknown }
if (outer.providerExecuted !== true) return null
const inner = outer.anthropic
if (typeof inner !== 'object' || inner === null) return null
const { serverToolType, resultBlockType, result } = inner as {
serverToolType?: unknown
resultBlockType?: unknown
result?: unknown
}
if (
typeof serverToolType !== 'string' ||
(resultBlockType !== 'web_search_tool_result' &&
resultBlockType !== 'web_fetch_tool_result')
) {
return null
}
return {
// Validated as a string above; widen back to the SDK's tool-name union.
serverToolType: serverToolType as ServerToolUseBlockParam['name'],
resultBlockType,
result,
}
function readAnthropicServerToolMetadata(
metadata: unknown,
): AnthropicServerToolMetadata | null {
if (typeof metadata !== 'object' || metadata === null) return null
const outer = metadata as { providerExecuted?: unknown; anthropic?: unknown }
if (outer.providerExecuted !== true) return null
const inner = outer.anthropic
if (typeof inner !== 'object' || inner === null) return null
const { serverToolType, resultBlockType, result } = inner as {
serverToolType?: unknown
resultBlockType?: unknown
result?: unknown
}
const isWebSearchPair =
serverToolType === 'web_search' &&
resultBlockType === 'web_search_tool_result'
const isWebFetchPair =
serverToolType === 'web_fetch' &&
resultBlockType === 'web_fetch_tool_result'
if (!isWebSearchPair && !isWebFetchPair) {
return null
}
return {
// Validated as a string above; widen back to the SDK's tool-name union.
serverToolType: serverToolType as ServerToolUseBlockParam['name'],
resultBlockType,
result,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-anthropic/src/adapters/text.ts` around lines 89 - 114,
readAnthropicServerToolMetadata currently validates serverToolType and
resultBlockType independently, which allows mismatched Anthropic server-tool
pairs to slip through. Tighten the checks in readAnthropicServerToolMetadata and
the downstream formatMessages path so only known valid combinations of
serverToolType and resultBlockType are accepted, and return null for any
persisted metadata where the pair does not match an allowed server-tool/result
pairing.

Comment on lines +1166 to +1170
const serverStart = chunks.find(
(c) =>
c.type === 'TOOL_CALL_START' &&
(c as { toolCallId: string }).toolCallId === 'srv_fetch',
) as (StreamChunk & { metadata?: Record<string, unknown> }) | undefined

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.

📐 Maintainability & Code Quality | 🟡 Minor

Remove redundant type assertions and replace find() calls with type predicates.

The casts to (StreamChunk & { metadata?: Record<string, unknown> }) | undefined are unnecessary because ToolCallStartEvent already defines metadata and toolCallId. Using type predicates as shown elsewhere in the file (lines 1643-1644) properly narrows the type without assertions, resolving the @typescript-eslint/no-unnecessary-type-assertion warnings.

Apply fix
- const serverStart = chunks.find(
-   (c) =>
-     c.type === 'TOOL_CALL_START' &&
-     (c as { toolCallId: string }).toolCallId === 'srv_fetch',
- ) as (StreamChunk & { metadata?: Record<string, unknown> }) | undefined
+ const serverStart = chunks.find((c): c is Extract<StreamChunk, { type: 'TOOL_CALL_START' }> =>
-     c.type === 'TOOL_CALL_START' && c.toolCallId === 'srv_fetch'
- )
- const start = chunks.find((c) => c.type === 'TOOL_CALL_START') as
-   | (StreamChunk & { metadata?: Record<string, unknown> })
-   | undefined
+ const start = chunks.find((c): c is Extract<StreamChunk, { type: 'TOOL_CALL_START' }> =>
-     c.type === 'TOOL_CALL_START'
- )
🧰 Tools
🪛 ESLint

[error] 1166-1170: This assertion is unnecessary since it does not change the type of the expression.

(@typescript-eslint/no-unnecessary-type-assertion)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-anthropic/tests/anthropic-adapter.test.ts` around lines 1166 -
1170, The `chunks.find(...)` usage in `anthropic-adapter.test.ts` is relying on
redundant casts for the `TOOL_CALL_START` event, even though
`ToolCallStartEvent` already includes `toolCallId` and `metadata`. Update the
`serverStart` lookup to use a type predicate in the `find()` callback, following
the pattern already used elsewhere in this test file, so the result narrows
naturally without any `as (StreamChunk & { metadata?: Record<string, unknown> })
| undefined` assertion.

Comment on lines +405 to +413
// 4. It is provider-executed (e.g. Anthropic web_search) — already run by
// the provider, so there is no client result to wait for.
return toolParts.every(
(part) =>
part.state === 'complete' ||
part.state === 'approval-responded' ||
(part.output !== undefined && !part.approval) ||
toolResultIds.has(part.id),
toolResultIds.has(part.id) ||
isProviderExecutedToolCall(part),

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

areAllToolsComplete() can still throw on seeded ModelMessage-shaped messages.

This PR already treats parts-less initialMessages as a supported runtime shape in isToolCallPartErrored(), but areAllToolsComplete() still dereferences lastAssistant.parts unconditionally earlier in the same method. A seeded assistant message without parts will still crash this helper before the new provider-executed clause is reached. Normalizing initialMessages once would be ideal; short-term, this method needs the same guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/chat/stream/processor.ts` around lines 405 - 413,
areAllToolsComplete() still assumes lastAssistant has parts and can crash on
seeded ModelMessage-shaped assistant messages. Update this helper to guard
against a missing parts array before iterating or dereferencing it, similar to
the existing handling in isToolCallPartErrored(), and make sure the
provider-executed tool-call check in processor.ts remains reachable after the
normalization/guard.

Comment on lines +104 to +105
(resultBlockType !== 'web_search_tool_result' &&
resultBlockType !== 'web_fetch_tool_result')

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.

I think IIRC llms can return other tool results as well like code exectuion etc, this will be a problem on openai as well i'm 100% sure as I remember they returned these chunks as well so lets fix it on openai as well and lets make sure all hte provider tool chunks are covered

…b_fetch

The #604 regression spec still asserted web_fetch was never surfaced as a
tool call. After #839, provider-executed server-tool results stream as a
provider-executed TOOL_CALL_START/END pair so they persist across turns.
Invert the assertions: expect two tool calls, the web_fetch flagged
metadata.providerExecuted with its raw result, both with clean parsed input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic web_search tool results are not preserved across turns in StreamProcessor

2 participants