42d03d4d98550267b5f080eb018ea4a8c1c2cea6
The agent had thirteen tools and no way to search. `fetch` reads a URL
it is handed; nothing produced one, so looking up a CVE, a changelog or
an error string meant the model guessing an address. Provider-native
search was not wired either: every kind sent only local function
declarations, and the parsers knew `text`, `tool_use` and
`function_call` and nothing else.
Two paths now, and exactly one is live per turn. A model with the
`webSearch` ability on an endpoint that actually implements the server
tool gets the provider's own search; everything else gets a
`web_search` tool backed by a backend configured once in Settings.
Offering both would put two tools in front of the model for one job and
let its guess decide whether the search appears as a card beforehand or
a record afterwards.
Chat Completions is excluded. `web_search_options` exists there on two
`*-search-preview` SKUs, while `.openAI` is also every compatible
gateway this app talks to, and an unknown tool type is a 400 on every
message rather than a missing feature.
The wire protocol alone turned out not to be enough. A server tool
exists where a vendor's API and that vendor's model meet, and a gateway
breaks the pairing: an OpenAI-shaped `/responses` endpoint pointed at
Claude accepts `{"type": "web_search"}` and forwards it as an ordinary
function declaration. The model calls it, native mode has deliberately
withheld the local tool, and the run reports `Unknown tool: web_search`.
So the model family has to agree with the endpoint before native is
chosen.
A native search never reaches the tool pipeline, so nothing would be
approved, recorded or shown, and the assistant would cite pages the
transcript never mentions reaching. `recordNativeWebSearch` raises an
ordinary card for it after the fact — but that card is a record, not a
turn. Appending it to the model-facing transcript replays a
`function_call_output` whose call id matches nothing, which OpenAI
answers with `No tool call found for function call output`, and only on
the *second* request, because the first is what plants the orphan.
`priorMessages` is persisted before the request goes out, so an affected
conversation keeps the orphan across restarts; incoming history is now
filtered for tool results that answer no call, which repairs it.
Anthropic's `server_tool_use` streams `input_json_delta` exactly like
`tool_use`. Read as a tool call it produces one with an empty name, and
`AgentRuntime` rejects that — so a single native search failed the whole
assistant turn. `AnthropicStreamState` exists for this.
Bing is the keyless default, and reachability is the reason: it answers
from mainland China and DuckDuckGo does not. Its relevance is the
weaker half of that trade. Probed live, `mkt` is not optional — without
it Bing returns a well-formed page, correctly titled with the query, of
results for something else entirely, which is the worst failure
available here because the parse succeeds and the model is handed
confident nonsense. Even with it, an anonymous request degrades to
roughly first-token matching: "CVE-2024-3094 xz backdoor" returns
cve.org's landing page. Browser headers, a cookie jar, `form=` and
`setlang`/`cc` moved none of it. DuckDuckGo returns the NVD record, the
CVE record and the Wikipedia article for the same query and is offered
for anyone who can reach it.
Snippets only, from every backend. Each can return page text, and taking
it would make search a second way to read the web — one whose
destination is picked by the engine rather than classified by us.
Leaving it out keeps `fetch` the single door, with its address
classification, redirect budget and private-network escalation still on
the path. `web_search` is `.locallyReadOnly` for a public backend to
match what `fetch` became, and escalates for exactly the destinations
`fetch` escalates for.
DeepSeek v4 needed no code: its `/responses` implements the identical
tool type and returns identical `web_search_call` items, so it only had
to be recognised. Recognition is a regex over the model id, applied
outside the reported-abilities guard on purpose — no `/models` payload
carries server-side search and Pi's catalog has no field for it, so a
source listing abilities never means it ruled search out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
AI-driven remote workspace for Mac, iPhone, and iPad.
https://termind.akinokaede.com
23 MiB
0 Stars
1 Watchers
0 Forks
Languages
Swift
98.9%
Python
0.8%
Ruby
0.3%