Skip to content

Anthropic server web_search turns lose their result on persistence and are dropped from replayed history #575

Description

@danny-avila

Summary

An Anthropic server-side web_search turn is persisted without its result (tool_call.output is never set), so on the next request the assistant message is replayed as plain text with no server_tool_use / web_search_tool_result blocks. The model sees a prior answer citing "search results" that, from its point of view, never happened. Newer Claude models that are candid about what they did and didn't do then tell the user the earlier answer was fabricated and that they "have no search tool", even when web_search is still declared in tools.

Observed on @librechat/agents 3.8.6 with @langchain/anthropic 1.5.2.

Reproduction

  1. Anthropic provider, built-in web search enabled (web_search server tool in tools).
  2. Turn A: "Search the web: what is the latest stable release of Node.js? Cite the source." → the model runs web_search, results stream, the host receives the artifact through TOOL_END, and the answer cites sources.
  3. Turn B (same conversation, web search still on or off — both reproduce): "Looking back at this conversation, is there a record of a web search having been run?"

Observed model reply (turn B, web_search still declared):

No, the history shows no web search call or results, only my Node.js answer describing "results" that weren't there, and I was also wrong to say I have no search tool, since web_search is available and I just didn't run it.

Captured request bodies for turn B (block types per message; str = string content):

u:text
a:str          <- turn A, should be server_tool_use + web_search_tool_result + text
u:text

What is persisted after turn A

The tool_call content part for the server tool has no output and empty args:

{
  "id": "srvtoolu_01…",
  "name": "web_search",
  "args": "",
  "type": "tool_call",
  "runStepStatus": "…",
  "runStepDurationMs": …
}

The search results themselves only reach the host through the TOOL_END artifact (handleAnthropicSearchResults in src/tools/handlers.ts), which the host turns into attachments/sources. Nothing dispatches ON_RUN_STEP_COMPLETED for the server-tool step, so the content part never gets its output (or final args).

Why the turn is dropped on replay

formatAgentMessages (src/messages/format.ts) only replays a srvtoolu_ tool call when a matching server tool result is present in the same message (serverToolResultIds), otherwise it is skipped unless preserveUnpairedServerToolUses is set (only true for the last message). With no persisted result the server_tool_use is treated as unpaired and skipped, and the message collapses to its text.

Relevant code paths:

  • src/tools/handlers.ts → handleServerToolResult / handleAnthropicSearchResults: dispatches TOOL_END only.
  • src/events.ts → ToolEndHandler: "tool completion (ON_RUN_STEP_COMPLETED dispatch …) is handled in graph context by ToolNode directly" — server tools never go through ToolNode.
  • src/messages/format.ts ~L1880: !serverToolResultIds.has(id) && options.preserveUnpairedServerToolUses !== true → continue.

This looks like fallout from cac0c9e ("streamline tool completion handling in ToolNode and Graph"), which moved step completion into ToolNode; the server-tool path was not given an equivalent completion.

Expected

Either:

  • complete the server-tool run step with the search result so the tool_call part carries output (and the final args), and replay it as server_tool_use + web_search_tool_result when the provider is Anthropic; or
  • when the paired result is unavailable, replay a textual trace of the call (query + result summary, or at least "used web_search with query …") rather than silently dropping the block, so the model keeps an accurate record of its own actions.

Dropping the record is worse than either option: the model's follow-up contradicts an answer the user already received.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions