You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The gateway-owned web_search_preview tool is hardwired to You.com. This RFC proposes making the search
backend selectable through a typed WebSearchProviderKind config enum, adding Brave Search as the first
alternative provider, and — as a prerequisite — replacing the current untyped pass-through of provider JSON with
a typed normalization contract (WebSearchResult). The work is split into two PRs: a behavior-preserving refactor,
then the Brave provider with configuration, tests, and docs. SearXNG (keyless, self-hosted) and Tavily follow as
separate PRs against the same contract.
The design keeps the existing WebSearchProvider trait private, keeps the public WebSearchHandler constructors
unchanged, and adds only additive configuration. When no provider is selected, behavior is identical to today.
Motivation
web_search_preview is executed by the gateway (ARCHITECTURE.md, docs/design/tool-framework.md), which is a
real differentiator for a self-hosted, OpenAI-compatible stack. Today it can only be enabled by setting YOU_API_KEY and YOU_API_BASE_URL
(crates/agentic-server-core/src/tool/web_search.rs:20-21, docs/deploying/kubernetes.md:419-420).
Splits the You.com HTTP integration behind an internal WebSearchProvider abstraction so additional search
providers can be added without changing the OpenAI tool adapter.
The abstraction exists (web_search.rs:253-260), but nothing else was ever plugged into it. Practical consequences:
Barrier to entry. As of this writing, You.com's Search API is a paid product without a self-serve free
developer tier. Brave Search offers a free plan (≈2,000 queries/month), Tavily ≈1,000/month. A personal or
evaluation deployment of Agentic API cannot exercise web_search at all without a commercial contract.
Air-gapped / private-cloud deployments cannot use web_search because the only supported backend is a public
SaaS endpoint. A SearXNG instance (or an internal search API behind the same contract) is the realistic answer for
those environments.
Vendor coupling in the tool contract. The function schema shown to the model, the argument struct, and the
model-facing tool output are all You.com-shaped (details below), so even an embedder who implements their own
executor inherits You.com semantics.
Goals
Select the search backend by configuration; you remains the default.
Add Brave Search as the first alternative provider.
Define a typed, provider-neutral result contract so no provider builds serde_json::json! blobs.
Keep every existing public constructor, env var, config key, and public output item working unchanged.
Keep the provider trait private; extension for embedders remains GatewayExecutorRegistration::WebSearch.
Non-goals
Pagination (not exposed by the function schema or search_context_size).
Automatic retries on HTTP 429 (see Concurrency and rate limits).
A public plugin ABI for providers.
Query rewriting with site: operators to emulate domain filters (possible later optimization; Phase 1 post-filters).
Cargo features per provider. All providers compile into the single binary.
Proposed solution
Implementation plan
Phrase 1 — refactor and typed contract (no configuration or public API change)
Split crates/agentic-server-core/src/tool/web_search.rs (807 lines) into tool/web_search/{mod.rs, args.rs, you.rs}: mod.rs holds the handler, trait, typed result, and public output
mapping; args.rs holds WebSearchArguments, Freshness, and the domain post-filter; you.rs holds the You.com
provider and request shaping.
Introduce WebSearchResult, WebSearchProviderMetadata, typed WebSearchProviderResponse; the You.com provider
deserializes into them.
Add WebSearchProviderKind with only You, WebSearchProviderConfig.provider, and WebSearchHandler::from_config; wire ToolExecutors::from_config.
Add a test that serializes the handler output for the existing mock fixture
(tests/web_search_tool_test.rs:291) and asserts it byte-for-byte; add a redacted real You.com response as a
fixture so the mapping is checked against actual upstream shape, not only the hand-written mock.
Existing unit and integration tests pass unmodified.
WebSearchProviderKind::Brave; file/env/generated-config plumbing in config_file.rs and main.rs.
Tests (see below), docs, CHANGELOG.md.
Roadmap (separate RFC-lite issues, same contract)
SearXNG — keyless, self-hosted, GET /search?format=json&categories=general,news&time_range=…. This is the
air-gapped story and the proof that the abstraction handles a provider with no credential and heterogeneous
result quality.
Tavily — native include_domains / exclude_domains, LLM-oriented content; news is a separate topic=news request, which exercises the "one request per query" policy differently.
DuckDuckGo is not planned: there is no official web search API, and scraping is out of scope for this project.
Testing
All provider tests run against a local Axum mock bound to 127.0.0.1:0, following spawn_mock_you_with_response
(tests/web_search_tool_test.rs:291-322). No external network access in CI. No new replay cassettes are needed.
Phrase 2 coverage:
200 with mixed web + news → both sections mapped; sources derived from both.
Empty results → empty sections, no error.
401 / 403 → failed web_search_call, message names the key env var, credential absent from output.
429 with Retry-After → failed call, no retry (single captured request), header value in message.
Query-parameter assertions: q, count (clamped from 50 → 20), freshness=pw, country, search_lang, safesearch, result_filter=web,news; X-Subscription-Token present; Accept-Encoding absent.
Concurrency ceiling: reuse the ConcurrencyTrackingProvider pattern to assert max_active == 1 for Brave under a
five-query batch even when the gateway limit is 5.
Config: AGENTIC_WEB_SEARCH_PROVIDER=Brave parses case-insensitively; unknown value fails startup; [web_search] provider round-trips through FileConfig; generated file contains provider = "you".
Mock hygiene: use try_send (or hold the receiver for the test's lifetime) instead of tx.send(..).await.unwrap() inside handlers (tests/web_search_tool_test.rs:311), to avoid the class of hang fixed
in CHANGELOG.md:51.
Gates: cargo test, cargo clippy --all-targets -- -D warnings (pedantic is warn in Cargo.toml and is
promoted by -D warnings), cargo fmt -- --check, pre-commit run --all-files.
Additional context
Open questions for maintainers
page_age vs published_at. Keeping page_age makes PR 1 byte-identical for You.com; published_at is
provider-neutral. We propose published_at and would like a decision before PR 1.
Default base URL for You.com. Adding provider.default_base_url() for You.com means a config that fails
today (YOU_API_KEY set, YOU_API_BASE_URL unset) starts working. Acceptable? If so, which host is canonical — README.md:219 says https://api.ydc-index.io, docs/deploying/kubernetes.md:420 says https://ydc-index.io.
Base URL override for non-You providers: a generic AGENTIC_WEB_SEARCH_BASE_URL, or per-provider BRAVE_API_BASE_URL mirroring YOU_API_BASE_URL? We lean generic, with YOU_API_BASE_URL retained as-is.
Location of WebSearchProviderKind:config.rs (proposed, alongside SqliteTempStore) or tool/web_search/mod.rs with a re-export?
Should the provider name be surfaced to the model in metadata[] (proposed) or kept out of the model-facing
output?
Problem statement / motivation
Summary
The gateway-owned
web_search_previewtool is hardwired to You.com. This RFC proposes making the searchbackend selectable through a typed
WebSearchProviderKindconfig enum, adding Brave Search as the firstalternative provider, and — as a prerequisite — replacing the current untyped pass-through of provider JSON with
a typed normalization contract (
WebSearchResult). The work is split into two PRs: a behavior-preserving refactor,then the Brave provider with configuration, tests, and docs. SearXNG (keyless, self-hosted) and Tavily follow as
separate PRs against the same contract.
The design keeps the existing
WebSearchProvidertrait private, keeps the publicWebSearchHandlerconstructorsunchanged, and adds only additive configuration. When no provider is selected, behavior is identical to today.
Motivation
web_search_previewis executed by the gateway (ARCHITECTURE.md,docs/design/tool-framework.md), which is areal differentiator for a self-hosted, OpenAI-compatible stack. Today it can only be enabled by setting
YOU_API_KEYandYOU_API_BASE_URL(
crates/agentic-server-core/src/tool/web_search.rs:20-21,docs/deploying/kubernetes.md:419-420).PR #85 (@franciscojavierarceo) anticipated this:
The abstraction exists (
web_search.rs:253-260), but nothing else was ever plugged into it. Practical consequences:developer tier. Brave Search offers a free plan (≈2,000 queries/month), Tavily ≈1,000/month. A personal or
evaluation deployment of Agentic API cannot exercise
web_searchat all without a commercial contract.web_searchbecause the only supported backend is a publicSaaS endpoint. A SearXNG instance (or an internal search API behind the same contract) is the realistic answer for
those environments.
model-facing tool output are all You.com-shaped (details below), so even an embedder who implements their own
executor inherits You.com semantics.
Goals
youremains the default.serde_json::json!blobs.GatewayExecutorRegistration::WebSearch.Non-goals
search_context_size).site:operators to emulate domain filters (possible later optimization; Phase 1 post-filters).Proposed solution
Implementation plan
Phrase 1 — refactor and typed contract (no configuration or public API change)
crates/agentic-server-core/src/tool/web_search.rs(807 lines) intotool/web_search/{mod.rs, args.rs, you.rs}:mod.rsholds the handler, trait, typed result, and public outputmapping;
args.rsholdsWebSearchArguments,Freshness, and the domain post-filter;you.rsholds the You.comprovider and request shaping.
WebSearchResult,WebSearchProviderMetadata, typedWebSearchProviderResponse; the You.com providerdeserializes into them.
WebSearchProviderKindwith onlyYou,WebSearchProviderConfig.provider, andWebSearchHandler::from_config; wireToolExecutors::from_config.(
tests/web_search_tool_test.rs:291) and asserts it byte-for-byte; add a redacted real You.com response as afixture so the mapping is checked against actual upstream shape, not only the hand-written mock.
Phrase 2 — Brave Search provider
tool/web_search/brave.rs(~300 lines): request shaping, minimal response structs, mapping, domain post-filterapplication, count clamp, freshness rendering,
max_concurrent_requests() = 1.WebSearchProviderKind::Brave; file/env/generated-config plumbing inconfig_file.rsandmain.rs.CHANGELOG.md.Roadmap (separate RFC-lite issues, same contract)
GET /search?format=json&categories=general,news&time_range=…. This is theair-gapped story and the proof that the abstraction handles a provider with no credential and heterogeneous
result quality.
include_domains/exclude_domains, LLM-orientedcontent; news is a separatetopic=newsrequest, which exercises the "one request per query" policy differently.Testing
All provider tests run against a local Axum mock bound to
127.0.0.1:0, followingspawn_mock_you_with_response(
tests/web_search_tool_test.rs:291-322). No external network access in CI. No new replay cassettes are needed.Phrase 2 coverage:
200with mixedweb+news→ both sections mapped;sourcesderived from both.401/403→ failedweb_search_call, message names the key env var, credential absent from output.429withRetry-After→ failed call, no retry (single captured request), header value in message.q,count(clamped from 50 → 20),freshness=pw,country,search_lang,safesearch,result_filter=web,news;X-Subscription-Tokenpresent;Accept-Encodingabsent.trailing dot, unparsable URL dropped.
ConcurrencyTrackingProviderpattern to assertmax_active == 1for Brave under afive-query batch even when the gateway limit is 5.
AGENTIC_WEB_SEARCH_PROVIDER=Braveparses case-insensitively; unknown value fails startup;[web_search] providerround-trips throughFileConfig; generated file containsprovider = "you".try_send(or hold the receiver for the test's lifetime) instead oftx.send(..).await.unwrap()inside handlers (tests/web_search_tool_test.rs:311), to avoid the class of hang fixedin
CHANGELOG.md:51.Gates:
cargo test,cargo clippy --all-targets -- -D warnings(pedantic iswarninCargo.tomland ispromoted by
-D warnings),cargo fmt -- --check,pre-commit run --all-files.Additional context
Open questions for maintainers
page_agevspublished_at. Keepingpage_agemakes PR 1 byte-identical for You.com;published_atisprovider-neutral. We propose
published_atand would like a decision before PR 1.provider.default_base_url()for You.com means a config that failstoday (
YOU_API_KEYset,YOU_API_BASE_URLunset) starts working. Acceptable? If so, which host is canonical —README.md:219sayshttps://api.ydc-index.io,docs/deploying/kubernetes.md:420sayshttps://ydc-index.io.AGENTIC_WEB_SEARCH_BASE_URL, or per-providerBRAVE_API_BASE_URLmirroringYOU_API_BASE_URL? We lean generic, withYOU_API_BASE_URLretained as-is.WebSearchProviderKind:config.rs(proposed, alongsideSqliteTempStore) ortool/web_search/mod.rswith a re-export?metadata[](proposed) or kept out of the model-facingoutput?
References
feat: add web search gateway tool(commitbf5fe8b).docs/design/tool-framework.md— tool ownership model;web_searchis gateway-owned.docs/design/messages-gateway-tool-classification.md— Claude CodeWebSearchaliasing intoweb_search.crates/agentic-server-core/tests/cassettes/README.md— recorder workflow (no new cassettes required here).