Skip to content

feat(agents): add web_search tool over Cloudflare's Web Search API - #2502

Merged
ben-reitz merged 13 commits into
mainfrom
feat/websearch-tool
Oct 7, 2026
Merged

ben-reitz merged 13 commits into
mainfrom
feat/websearch-tool

Conversation

@ben-reitz

@ben-reitz ben-reitz commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A web_search tool over Cloudflare's Web Search API (open beta), for the pi harness, the AI SDK, and TanStack AI on one core.

packages/agents/src/websearch/
├── contract.ts          # tool name, description, input/output types, renderWebSearchResults()
├── source.ts            # WebSearchSource { search, provider? }, createAIWebSearch(), createHTTPWebSearch(), WebSearchError
├── tool.ts              # harness-neutral core: config validation, limit cap, timeout/abort, render, model-facing failure text
├── index.ts             # agents/websearch
└── tools/
    ├── pi.ts            # agents/websearch/pi           → pi-durable ToolRegistration (replay: "safe")
    ├── ai-sdk.ts        # agents/websearch/ai-sdk       → { execute, toModelOutput }
    ├── tanstack-ai.ts   # agents/websearch/tanstack-ai
    └── input-schema.ts  # zod schema shared by ai-sdk + tanstack (keeps zod out of pi)

Who decides what:

model  → { query, limit? }                 description says "at most N"; larger asks are capped, not rejected
host   → binding | source                  exclusive at the type level
         gateway, provider, byokAlias      the model can never switch provider or billing
         limit (5), maxDescriptionChars (600), timeoutMs (30s), description
         invalid values → RangeError at creation

What each side sees:

success  model → numbered list, descriptions trimmed at a word boundary (Ceramic returns ~8 kB/result)
         host  → untouched API response + provider  (pi details, AI SDK execute return)
failure  model → what to do next: fix the query | retry once | carry on without search
         host  → WebSearchError { status, code, retryable, requestId, apiCode }, operator message
                 pi: error result + details;  AI SDK / TanStack: thrown, original error as `cause`
abort    harness cancel propagates; timeout → retryable web_search_timeout

Also fixes AiSdkHarness: it called convertToModelMessages(messages) without { tools }, so toModelOutput never applied to earlier tool results and later turns resent full tool outputs. Separate patch changeset.

Docs: docs/agents/search-the-web.md, covering only what the SDK adds; everything about the service links out.

Ai.websearch is typed locally (AiWebSearchBinding in source.ts) rather than via @cloudflare/workers-types. Every workers-types since 5.20260812.1 emits declare const Buffer: any, which discards @types/node's Buffer and breaks packages/agents/src/vite.ts. See workerd#7026. The public option is still binding: Ai, and the shim goes when the repo bumps.

Evidence

  • packages/agents/src/tests/websearch.test.ts, 41 tests
  • packages/agents/src/tests-d/websearch-tool.test-d.ts: binding + source is rejected, and the adapters fit Tool / ServerTool.
  • src/tests/channels/ai-sdk-harness.test.ts: "shapes earlier tool results with toModelOutput on later turns". It fails without the harness fix
  • Live, HTTP source, against a funded gateway: ceramic, exa, and linkup all return 200. Rendered output, the missing-BYOK 400, the bad-gateway error, and the host cap were checked.
  • Not verified: env.AI.websearch through the binding, in wrangler dev or deployed. The agents account has no gateway credits, so expect web_search_payment_required until it is funded or a BYOK key is added.

Build, lint, format, export check, and agents typecheck are clean.

Merge Danger

Door: two-way

Four new @beta entry points (minor changeset), plus a one-line AiSdkHarness fix (patch changeset).

Blast Radius: small

The websearch code is opt-in, and nothing imports it yet. The harness fix changes what AiSdkHarness sends for earlier tool results whenever a tool defines toModelOutput. That is the intended behaviour, but it is a prompt change for existing users of such tools.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf0f57b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@agent-think

agent-think Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 4 entry points changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/websearch 11 new — 2.1 KiB
🆕 agents/websearch/ai-sdk 2 new — 88.2 KiB
🆕 agents/websearch/pi 2 new — 22.8 KiB
🆕 agents/websearch/tanstack-ai 2 new — 103.6 KiB
Changed exports (17)
Import Gzip change Size now
🆕 agents/websearch/tanstack-ai#webSearchTool — 103.6 KiB
🆕 agents/websearch/ai-sdk#webSearchTool — 88.2 KiB
🆕 agents/websearch/pi#webSearchTool — 22.8 KiB
🆕 agents/websearch#createAIWebSearch — 2.1 KiB
🆕 agents/websearch#createHTTPWebSearch — 2 KiB
🆕 agents/websearch#renderWebSearchResults — 678 B
🆕 agents/websearch/ai-sdk#WebSearchError — 462 B
🆕 agents/websearch/tanstack-ai#WebSearchError — 462 B
🆕 agents/websearch#WebSearchError — 461 B
🆕 agents/websearch/pi#WebSearchError — 461 B
🆕 agents/websearch#WEB_SEARCH_TOOL_NAME — 329 B
🆕 agents/websearch#MAX_WEB_SEARCH_QUERY_LENGTH — 327 B
🆕 agents/websearch#DEFAULT_WEB_SEARCH_TIMEOUT_MS — 325 B
🆕 agents/websearch#MAX_WEB_SEARCH_LIMIT — 324 B
🆕 agents/websearch#DEFAULT_WEB_SEARCH_DESCRIPTION_CHARS — 323 B
🆕 agents/websearch#DEFAULT_WEB_SEARCH_LIMIT — 323 B
🆕 agents/websearch#WEB_SEARCH_TOOL_DESCRIPTION — 316 B
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared f2f745b5 → bf0f57b1 · workflow run · reported by agent-think[bot]

@pkg-pr-new

pkg-pr-new Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2502

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2502

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2502

hono-agents

npm i https://pkg.pr.new/hono-agents@2502

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2502

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2502

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2502

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2502

commit: bf0f57b

@aron-cf aron-cf 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.

This is great. I think the docs need a pass and I left some comments on naming and structure. Once in I think an example with a webfetch tool would be helpful.

Comment thread .changeset/websearch-tool.md Outdated
Comment thread docs/agents/search-the-web.md Outdated
Comment thread docs/agents/search-the-web.md Outdated
Comment thread docs/agents/search-the-web.md Outdated
Comment thread docs/agents/search-the-web.md Outdated
Comment thread docs/agents/search-the-web.md Outdated
Comment thread packages/agents/src/websearch/tools/pi.ts
@ben-reitz
ben-reitz force-pushed the feat/websearch-tool branch from 973b0a1 to fdd3b39 Compare October 7, 2026 00:13
@ben-reitz ben-reitz changed the title feat(agents): add websearch tool over Cloudflare's Web Search API feat(agents): add web_search tool over Cloudflare's Web Search API Oct 7, 2026
@ben-reitz
ben-reitz marked this pull request as ready for review October 7, 2026 00:14
devin-ai-integration[bot]

This comment was marked as resolved.

ben-reitz added a commit that referenced this pull request Oct 7, 2026
From Devin review on #2502:
- The core races the whole source.search() against the deadline and the
  caller's signal, so a source that ignores its signal can no longer
  hang the tool call past timeoutMs.
- The binding source checks for an already-aborted signal before it
  starts a billed search.
- A 2xx body that is not a search response is a retryable
  web_search_unavailable, not a non-retryable failure.
- Docs: the host output is normalised, not untouched.
Adds agents/websearch with a harness-neutral core and adapters for the
pi harness (agents/websearch/pi), the AI SDK (agents/websearch/ai-sdk),
and TanStack AI (agents/websearch/tanstack-ai). The host fixes gateway,
provider, BYOK alias, and result cap; the model chooses query and count.
The model sees trimmed text, the host gets the full API response.
webSearchFromAI and webSearchFromRest search without a model.

Ai.websearch is typed locally until @cloudflare/workers-types can be
bumped past 5.20260812.1 (cloudflare/workerd#7026).
…ng binding support

The model's input schema now caps limit at the host's limit instead of the
API maximum of 10, so a model asking for 10 under a host cap of 5 is no
longer silently clamped. webSearchFromAI fails with
websearch_unsupported_runtime and the required workerd/wrangler versions
when the AI binding has no websearch() method, instead of a TypeError.
…under tools/

webSearchFromAI and webSearchFromRest become createAIWebSearch and
createHTTPWebSearch (option types AIWebSearchOptions and
HTTPWebSearchOptions), matching the package's create* factories. The pi,
AI SDK, and TanStack AI adapters move to src/websearch/tools/; import
paths are unchanged.
WebSearchToolOptions is now an exclusive union: with `source`, passing
`binding`, `gateway`, `provider`, or `byokAlias` is a type error rather
than silently ignored. Adds type tests.
AiSdkHarness called convertToModelMessages without tools, so a tool's
toModelOutput only shaped the turn that ran it; later turns got the raw
output as JSON. For websearch that meant every later turn resent the full
untrimmed response. Pass the tools, test it, and note the requirement in
the websearch AI SDK docs for hosts converting messages themselves.
replay: "safe" means an interrupted search reruns on recovery (a second
billed search), not that stored results are reused; say so in the pi JSDoc
and docs. The model's error text has no code suffix. WebSearchRequest.limit
is now optional and defaults to 5, matching the docs. Note the TanStack
adapter's extra `name` option.
- Thread each framework's abort signal to the source (fetch signal for
  HTTP, a race for the binding) and add a host `timeoutMs` (default 30 s).
  A caller abort propagates instead of becoming a retryable result; a
  timeout is a retryable `web_search_timeout` failure.
- Normalise 200 bodies: drop items without a URL, fall back to the URL for
  a missing title, keep only string optional fields, default metadata.
- Every WebSearchError has a code (validation -> invalid_web_search_input,
  gateway config -> web_search_gateway_not_configured, else from status),
  keeps the numeric API code as `apiCode`, and is retryable for 429/5xx
  unless the API says otherwise. Codes are typed as WebSearchErrorCode.
- The model gets guidance (fix the query / retry once / carry on without
  search) instead of the operator message. AI SDK and TanStack AI throw a
  WebSearchError with that text and the original as `cause`.
- TanStack AI throws on failure so it reports an error result; pi failure
  details include the message.
- Document the packages in packages/agents/AGENTS.md; update the docs.
- Tests: TanStack execute path, HTTP error paths, BYOK alias and baseUrl,
  malformed items, timeouts and aborts, description override, trimming
  through pi and TanStack, and type tests for the adapters.
…rces

- C2: the zod input schema is shared by the AI SDK and TanStack AI
  adapters (tools/input-schema.ts, kept out of the core so pi needs no
  zod); the core exposes `render(output)`.
- C3: WebSearchSource is an object `{ search(request, { signal }), provider? }`
  so wrappers keep `provider` by spreading.
- C4: the tool is named `web_search`, the name models know from
  Anthropic's and OpenAI's built-in search.
- C5: the description says there is no next page and only mentions
  fetching if a page-reading tool exists; no results suggests a broader
  query; the schema states the host limit in the description instead of
  rejecting larger values, which the core caps.
- C7: invalid limit, maxDescriptionChars, timeoutMs, or byokAlias throw a
  RangeError when the tool or source is created.
- C8: each adapter re-exports the WebSearchError class.
- Rename constants to WEB_SEARCH_* to match WebSearchError and
  webSearchTool, and the runtime error code to
  web_search_unsupported_runtime to match the API's prefix.
- Send the trimmed query that was validated.
- Encode the account id in the HTTP URL and drop a trailing "/" from
  baseUrl.
- The unsupported-runtime message names the workerd version and links
  the docs instead of listing wrangler and vite-plugin versions.
- Truncate descriptions at a word boundary when one is within 20
  characters.
From Devin review on #2502:
- The core races the whole source.search() against the deadline and the
  caller's signal, so a source that ignores its signal can no longer
  hang the tool call past timeoutMs.
- The binding source checks for an already-aborted signal before it
  starts a billed search.
- A 2xx body that is not a search response is a retryable
  web_search_unavailable, not a non-retryable failure.
- Docs: the host output is normalised, not untouched.
@ben-reitz
ben-reitz force-pushed the feat/websearch-tool branch from e8985cb to bf0f57b Compare October 7, 2026 08:45
@ben-reitz
ben-reitz merged commit 66ae955 into main Oct 7, 2026
16 checks passed
@ben-reitz
ben-reitz deleted the feat/websearch-tool branch October 7, 2026 09:22
@github-actions github-actions Bot mentioned this pull request Oct 7, 2026
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.

2 participants