feat(web-search): add WebSearchTask with pluggable search providers - #850
Open
sroussey wants to merge 14 commits into
Open
feat(web-search): add WebSearchTask with pluggable search providers#850sroussey wants to merge 14 commits into
sroussey wants to merge 14 commits into
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
…election Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Adds the package to the dependency graph and key-packages list, and corrects the vendor-subpath paragraph: providers/* is no longer only ./ai and ./ai-runtime now that anthropic ships ./web-search. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces a new @workglow/web-search package that standardizes web search behind a provider interface with capability-based routing, and adds an Anthropic grounded web-search provider entrypoint in @workglow/anthropic.
Changes:
- Adds
@workglow/web-searchwithWebSearchTask, provider registry, capability checks, and built-in HTTP providers (Brave, Tavily, SearXNG). - Adds
@workglow/anthropic/web-searchentrypoint implementing theIWebSearchProvidercontract. - Adds tests and wiring (exports/build scripts/tsconfig references) to support the new package and provider.
Reviewed changes
Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| providers/anthropic/tsconfig.json | Adds TS project reference to the new packages/web-search. |
| providers/anthropic/src/web-search/AnthropicWebSearchProvider.ts | Implements Anthropic grounded web-search provider via vendor SDK. |
| providers/anthropic/src/web-search.ts | Adds ./web-search entrypoint and registration helper for the Anthropic provider. |
| providers/anthropic/src/tests/AnthropicWebSearchProvider.test.ts | Unit tests for Anthropic web-search provider behavior (tool type, mapping, pause_turn, domain lists). |
| providers/anthropic/package.json | Exports ./web-search, builds it, and adds dependency on @workglow/web-search. |
| packages/web-search/tsconfig.json | Adds composite TS config for the new @workglow/web-search package. |
| packages/web-search/src/WebSearchTask.ts | Implements the WebSearchTask with provider routing, capability enforcement, and request adaptation. |
| packages/web-search/src/WebSearchProviderRegistry.ts | Adds a process-wide provider registry with routing and helpful error messages. |
| packages/web-search/src/queryOperators.ts | Implements site: query rewriting for providers that only support domain filtering via query operators. |
| packages/web-search/src/providers/TavilyWebSearchProvider.ts | Adds Tavily HTTP adapter with capability declaration and request/response mapping. |
| packages/web-search/src/providers/SearxngWebSearchProvider.ts | Adds SearXNG adapter with base URL validation and result normalization. |
| packages/web-search/src/providers/httpSearch.ts | Shared helper to execute provider HTTP calls via an owned FetchUrlTask. |
| packages/web-search/src/providers/BraveWebSearchProvider.ts | Adds Brave HTTP adapter including date-range mapping to freshness. |
| packages/web-search/src/node.ts | Node entrypoint that registers the task and built-in providers on import. |
| packages/web-search/src/IWebSearchProvider.ts | Defines provider interface, request/response types, and capability model. |
| packages/web-search/src/common.ts | Common exports plus registration helpers for task and built-in providers. |
| packages/web-search/src/capabilityCheck.ts | Implements capability/request gap detection used for routing and pinned-provider validation. |
| packages/web-search/src/browser.ts | Browser entrypoint that registers only the task (no providers). |
| packages/web-search/src/tests/WebSearchTask.test.ts | Tests routing behavior, pinned-provider validation, query rewriting, and maxResults clamping. |
| packages/web-search/src/tests/WebSearchProviderRegistry.test.ts | Tests registry semantics (register/get/route/require/clear) and error messages. |
| packages/web-search/src/tests/TavilyWebSearchProvider.test.ts | Tests Tavily adapter mapping, auth scheme, and option behavior. |
| packages/web-search/src/tests/SearxngWebSearchProvider.test.ts | Tests SearXNG adapter URL construction, credential suppression, normalization, and truncation. |
| packages/web-search/src/tests/SearxngWebSearchProvider.integration.test.ts | Optional live integration test for SearXNG behind env var gating. |
| packages/web-search/src/tests/queryOperators.test.ts | Tests site: query operator rewriting and normalization. |
| packages/web-search/src/tests/entries.test.ts | Tests that entrypoints register tasks/providers as intended. |
| packages/web-search/src/tests/capabilityCheck.test.ts | Tests capability gap detection across request options. |
| packages/web-search/src/tests/BraveWebSearchProvider.test.ts | Tests Brave adapter mapping, auth header scheme, and freshness date mapping. |
| packages/web-search/README.md | Documents server-only behavior, provider matrix, and usage patterns. |
| packages/web-search/package.json | Defines new package exports/build/test scripts and peer deps. |
| bun.lock | Adds workspace entry and dependency wiring for @workglow/web-search. |
| .claude/CLAUDE.md | Updates repo architecture and adds @workglow/web-search documentation section. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CodeQL flagged two high-severity polynomial-ReDoS alerts. `/\/+$/` is quadratic on a string holding a long run of slashes that is not at the end: the engine starts `\/+` at every slash position, consumes the whole run, fails `$`, and restarts one character along. Both call sites take untrusted text — a search domain from task input, and a configured base URL. Measured on a 200k-slash input: 49.7s before, under 1ms after. The regression test asserts a 1000ms budget, so only a reintroduction of the pattern can trip it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Two review findings. Owning a FetchUrlTask does not inherit the job queue's rate limiter. The task refuses credential_key on the queued path because a queued payload is persisted to durable storage, so every keyed provider runs inline and inline fetches are not rate limited. The JSDoc and CLAUDE.md said otherwise; bounding a fan-out is the caller's job and now says so. The Anthropic provider returned an answer whether or not the caller asked, while Tavily returns one only on request — so `answer` meant different things depending on which provider routing picked. It is now gated on includeAnswer in both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
Both tsconfigs compiled src/**/__tests__ into dist, and both packages publish files: ["dist"], so the new tests shipped as .d.ts to consumers. Adds the exclude packages/task-graph already uses for its co-located tests. Vitest collects via its own project config, so the tests still run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
The new web-search test made providers/anthropic a workspace that holds tests, and it had no `test` script. Turbo reports such a workspace's test task successful while running nothing, so the suite would have been skipped in CI while reporting green — which is what testDiscovery's "every workspace that holds tests has a test script" guard caught. Adds the documented per-package script. `--project anthropic` now collects and passes the 9 tests it was silently skipping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4wM782LCRMPFPKeGzrzu9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
@workglow/web-search— oneWebSearchTaskserving both plain search APIs and model-grounded search behind a single normalized output shape, routed by provider capability.Design spec and implementation plan live in the
prdrepo on the same branch name (docs/superpowers/specs/2026-08-22-web-search-task-design.md).Why one task for both families
The obvious split — "search APIs" vs "grounded search" — isn't real. Tavily returns a synthesized
answerwhen asked; Exa returns full pagecontent; Anthropic'sweb_searchreturns an answer plus citations that are structurally{title, url, snippet}. So this is one shape whose fields different providers can or cannot populate — a capability matrix, not a discriminated union.resultsis always present (for a grounded provider, those are its citations).answeris populated only when the caller asks for one, uniformly across providers.Server-side only
Every commercial search API authenticates with a request header, which forces a CORS preflight none of them answer. Google's Custom Search JSON API is the one exception that genuinely permits cross-origin XHR — and it is closed to new customers and sunsets 2027-01-01. Independently, a browser-executed search would put the API key where any visitor can read it.
The browser entry therefore registers the task (so a builder UI can render the node and validate a graph) and no providers.
Capability routing
provideris required with no default, mirroringresponse_typeonFetchUrlTask— which provider serves a request decides its cost, rate limit and quality."auto"opts into routing over the requested options; the provider that ran is always reported on theprovideroutput port. A pinned provider that cannot honor an option throws rather than silently rerouting.domainFilteris three-valued:"native","query-operator"(the task rewrites the query withsite:), orfalse. Date filtering is never emulated — post-filtering bypublishedDatebreaksmaxResultsand drops every result whose date the provider omitted, sodateFilter: falsemeans such a request is refused.Providers
braveX-Subscription-Tokensite:tavilysearxngsite:anthropicHTTP adapters execute by owning a
FetchUrlTask, inheriting credential resolution viacredential_key, SafeFetch's redirect/SSRF checks, retry/backoff, per-attempt timeouts and the response cache.They do not inherit the queue's rate limiter, and cannot:
FetchUrlTaskrefusescredential_keyon the queued path because a queued payload is persisted to durable storage, so every keyed provider runs inline. Search APIs are metered against hard monthly quotas, so bounding aMapTaskfan-out remains the caller's responsibility. (This corrects an earlier overclaim in this PR, caught in review.)The grounded adapter ships as a new
./web-searchsubpath on@workglow/anthropic, so@workglow/web-searchhas no dependency on@workglow/ai.Two Anthropic traps handled and tested: a
web_search_tool_resultblock carries a list on success and an error object on failure — at HTTP 200, raising nothing — so reading it unbranched records a quota failure as a search that found nothing; and a server-tool turn can stop withstop_reason: "pause_turn", which must be resumed or the answer truncates silently.Tests
77 tests. SearXNG needs no key and has no quota, so it is the only provider whose integration test can run unmocked (
.integration.test.ts, excluded from the default tier, skipped unlessWEB_SEARCH_SEARXNG_URLis set). The rest are fixture-driven.Known unrelated CI failure
test-vitest-ai-provider-apifails onPermanentJobError: Provider DEEPSEEK failed for StructuredGenerationTask: 402 Insufficient Balance. That is the DeepSeek account balance, not this diff — nothing here touches DeepSeek (the 31 changed files are the new package, the Anthropic subpath, and docs). It needs a top-up or a key rotation, not a code change.Not in scope
No streaming (a grounded
answercould stream, butresultscannot arrive incrementally usefully). No re-ranking, deduplication, or multi-provider fan-out — one query, one provider, one response.