Run one web query through the first available search provider and return LLM-formatted answer, source URLs, and optional citations.
Source
- Entry:
packages/coding-agent/src/web/search/index.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/web-search.md - Key collaborators:
packages/coding-agent/src/web/search/provider.tsβ lazy provider registry; availability chain.packages/coding-agent/src/web/search/types.tsβ unifiedSearchResponse/SearchProviderErrortypes.packages/coding-agent/src/web/search/render.tsβ TUI renderer details type.packages/coding-agent/src/web/search/providers/base.tsβ provider interface and shared params contract.packages/coding-agent/src/web/search/providers/utils.tsβ credential lookup; source normalization.packages/coding-agent/src/web/search/providers/anthropic.tsβ Claude web-search provider.packages/coding-agent/src/web/search/providers/brave.tsβ Brave Search API adapter.packages/coding-agent/src/web/search/providers/codex.tsβ OpenAI Codex SSE adapter.packages/coding-agent/src/web/search/providers/exa.tsβ Exa API or MCP adapter.packages/coding-agent/src/web/search/providers/gemini.tsβ Gemini grounding SSE adapter.packages/coding-agent/src/web/search/providers/jina.tsβ Jina Reader search adapter.packages/coding-agent/src/web/search/providers/kagi.tsβ Kagi provider wrapper.packages/coding-agent/src/web/search/providers/kimi.tsβ Kimi search adapter.packages/coding-agent/src/web/search/providers/parallel.tsβ Parallel provider wrapper.packages/coding-agent/src/web/search/providers/perplexity.tsβ Perplexity API / OAuth adapter.packages/coding-agent/src/web/search/providers/searxng.tsβ self-hosted SearXNG adapter.packages/coding-agent/src/web/search/providers/synthetic.tsβ Synthetic search adapter.packages/coding-agent/src/web/search/providers/tavily.tsβ Tavily search adapter.packages/coding-agent/src/web/search/providers/zai.tsβ Z.AI remote MCP adapter.packages/coding-agent/src/web/parallel.tsβ Parallel search/extract HTTP client.packages/coding-agent/src/web/kagi.tsβ Kagi HTTP client.packages/coding-agent/src/tools/index.tsβ built-in tool registration and enable flag.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Search query, passed to providers unchanged. |
recency | "day" | "week" | "month" | "year" | No | Time filter. Only providers that implement it use it; code maps it for Brave, Perplexity, Tavily, SearXNG, and Kagi. |
limit | number | No | Max results to return. Usually becomes the provider requestβs result-count parameter when num_search_results is absent. |
max_tokens | number | No | Passed through as maxOutputTokens / max_tokens only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
temperature | number | No | Passed through only by Anthropic, Gemini, and Perplexity API-key mode. Ignored by the other providers. |
num_search_results | number | No | Requested upstream search breadth. For most providers this is the same count used for returned sources. Perplexity is the only adapter that keeps it distinct from limit. |
Outputs
The tool returns a single text content block plus structured details.
content:[{ type: "text", text: string }]details:SearchRenderDetailsfrompackages/coding-agent/src/web/search/render.tsresponse: SearchResponseerror?: string
text is produced by formatForLLM() in packages/coding-agent/src/web/search/index.ts:
- If
response.answerexists, it is emitted first. - If sources exist, one entry per source follows (the
## Sourcesheader with a source count is emitted only when an answer was also produced):[n] <title> (<formatted age or published date>)<url>- optional snippet line truncated to 240 chars.
- If citations exist, a
## Citationssection follows with URL/title plus optional cited text truncated to 240 chars. - If related questions exist, a
## Relatedbullet list follows. - If search queries exist, a
Search queries: <n>section follows, capped to the first 3 queries and 120 chars each.
Failure output is not thrown at the tool boundary when providers are unavailable or provider attempts fail. Instead the tool returns:
content[0].text = "Error: ..."details.response.provider = <last attempted provider> | "none"details.error = ...
Streaming: none. WebSearchTool.execute() forwards its AbortSignal into executeSearch(), and executeSearch() passes it to providers. If the signal is aborted during fallback handling, throwIfAborted(signal) rethrows the cancellation instead of returning an "Error: ..." text result.
Flow
WebSearchTool.execute()inpackages/coding-agent/src/web/search/index.tsdelegates directly toexecuteSearch().executeSearch()chooses a provider list:- if
params.provideris set and not"auto", it loads that provider withgetSearchProvider(); ifisExplicitlyAvailable()returns true, the list is[that provider], otherwise it falls back toresolveProviderChain(authStorage, "auto"). - otherwise it calls
resolveProviderChain()with the module-global preferred provider frompackages/coding-agent/src/web/search/provider.ts.
- if
resolveProviderChain()lazily loads each provider module on demand and returns only available providers. If a preferred provider is set, it is tried first (gated byisExplicitlyAvailable()), then the staticSEARCH_PROVIDER_ORDERexcluding that provider, each gated byisAvailable(). Providers in the excluded set (setExcludedSearchProviders()) are skipped entirely, including as the preferred candidate.- If no providers are available,
executeSearch()returnsError: No web search provider configured.withdetails.response.provider = "none". - For each provider in order,
executeSearch()callsprovider.search()with:query,limit,recency,temperature,maxOutputTokens,numSearchResults,systemPromptfrompackages/coding-agent/src/prompts/system/web-search.md.
- A
SearchResponsewith no renderable content (hasRenderableSearchContent()returns false) is rejected as aSearchProviderError(status204) so the loop advances to the next provider. On the first response that has renderable content,formatForLLM()renders answer/sources/citations/related/search-queries into one text block and returns it withdetails.response. - If a provider throws,
executeSearch()records the error and tries the next provider. There is no provider-level parallel fan-out; fallback is sequential. - After all candidates fail,
formatProviderError()normalizes each error:- Anthropic
404becomesAnthropic web search returned 404 (model or endpoint not found). 401/403become<Provider> authorization failed ...except Z.AI, which preserves its raw message.- other
SearchProviderErrors surfaceerror.message.
- Anthropic
- If more than one provider was attempted, the final message is
All web search providers failed: <provider/error>; ...; otherwise it is just the normalized last error.
Modes / Variants
- Provider selection
- Forced provider: internal callers may pass
provider; unavailable forced providers fall back to the auto chain instead of hard-failing (packages/coding-agent/src/web/search/index.ts). This field is not in the model-facing schema. - Preferred provider:
setPreferredSearchProvider()sets a module-global default used byresolveProviderChain().packages/coding-agent/src/sdk.tsandpackages/coding-agent/src/modes/controllers/selector-controller.tswire this from settings. - Excluded providers:
setExcludedSearchProviders()records providersresolveProviderChain()must never return, including as fallbacks. Wired from theproviders.webSearchExcludesetting (providers.webSearchdrives the preferred provider) inpackages/coding-agent/src/sdk.ts,packages/coding-agent/src/modes/interactive-mode.ts, andpackages/coding-agent/src/modes/controllers/selector-controller.ts. - Auto chain order:
perplexity,gemini,anthropic,codex,zai,exa,jina,kagi,tavily,brave,kimi,parallel,synthetic,searxng(SEARCH_PROVIDER_ORDERinpackages/coding-agent/src/web/search/types.ts).
- Forced provider: internal callers may pass
- Provider adapters
- Tavily β
packages/coding-agent/src/web/search/providers/tavily.ts- Availability: API key from env or
agent.dbviafindCredential(). - Querying: POST
https://api.tavily.com/search. recencymaps to Tavilytime_range; code explicitly keepstopicat default general scope instead of narrowing to news.limit/num_search_results: adapter usesparams.numSearchResults ?? params.limit, clamped to5..20with default5.- Output:
answer,sources,requestId,authMode: "api_key".
- Availability: API key from env or
- Perplexity β
packages/coding-agent/src/web/search/providers/perplexity.ts- Availability: auth precedence is
PERPLEXITY_COOKIES-> OAuth token inagent.db->PERPLEXITY_API_KEY/PPLX_API_KEY-> anonymous ask-endpoint fallback.isAvailable()gates the auto chain on credentials, butisExplicitlyAvailable()is always true, so explicit selection works unauthenticated. - OAuth/cookie/anonymous mode: POSTs to
https://www.perplexity.ai/rest/sse/perplexity_ask, consumes SSE, merges partial events, extracts answer and source URLs, setsauthMode: "oauth"("anonymous"for the unauthenticated fallback). - API-key mode: POSTs to
https://api.perplexity.ai/chat/completionswithmodel: "sonar-pro",search_mode: "web",num_search_results, optionalsearch_recency_filter,max_tokens,temperature. num_search_resultscontrols upstream API breadth only in API-key mode.limitis preserved separately asnum_resultsand slices returnedsourcesafter parsing in both auth modes.- Output may include
answer,sources,citations,usage,model,requestId,authMode.
- Availability: auth precedence is
- Brave β
packages/coding-agent/src/web/search/providers/brave.ts- Availability:
BRAVE_API_KEYonly. - Querying: GET
https://api.search.brave.com/res/v1/web/searchwithcount,extra_snippets=true, andfreshness=pd|pw|pm|pyforrecency. limit/num_search_results:params.numSearchResults ?? params.limit, clamped to1..20, default10.- Output:
sources,requestId.
- Availability:
- Jina β
packages/coding-agent/src/web/search/providers/jina.ts- Availability:
JINA_API_KEYonly. - Querying: GET-like fetch to
https://s.jina.ai/<encoded query>with bearer auth. - Ignores
recency,max_tokens, andtemperature. limit/num_search_results: adapter slices sources toparams.numSearchResults ?? params.limitwhen provided; otherwise returns all payload items.- Output:
sourcesonly.
- Availability:
- Kimi β
packages/coding-agent/src/web/search/providers/kimi.ts- Availability:
MOONSHOT_SEARCH_API_KEY,KIMI_SEARCH_API_KEY,MOONSHOT_API_KEY, oragent.dbcredentials formoonshot/kimi-code. - Querying: POST to
MOONSHOT_SEARCH_BASE_URL/KIMI_SEARCH_BASE_URL/ defaulthttps://api.kimi.com/coding/v1/searchwithtext_query,limit,enable_page_crawling,timeout_seconds: 30. limit/num_search_results:params.numSearchResults ?? params.limit, clamped to1..20, default10.- Output:
sources,requestId.
- Availability:
- Anthropic β
packages/coding-agent/src/web/search/providers/anthropic.ts- Availability:
ANTHROPIC_SEARCH_API_KEYenv var, otherwiseauthStorage.hasAuth("anthropic"); search credentials come fromauthStorage.getApiKey("anthropic")when no search-specific key is set. - Env overrides specific to search (do not affect chat completions):
ANTHROPIC_SEARCH_API_KEYβ highest-priority search auth; overridesANTHROPIC_API_KEY/ OAuth /ANTHROPIC_FOUNDRY_API_KEYfor the search call only.ANTHROPIC_SEARCH_BASE_URLβ search-only base URL for eitherANTHROPIC_SEARCH_API_KEYor fallback Anthropic credentials; overridesANTHROPIC_BASE_URL(andFOUNDRY_BASE_URLin Foundry mode); defaults tohttps://api.anthropic.com.ANTHROPIC_SEARCH_MODELβ search model; defaults toclaude-haiku-4-5.
- Querying: Claude Messages API with web-search tool enabled.
max_tokensandtemperaturepass through.limitandnum_search_resultsare collapsed together before dispatch:num_results = params.numSearchResults ?? params.limit.- Output may include
answer,sources,citations,searchQueries,usage.searchRequests,model,requestId.
- Availability:
- Gemini β
packages/coding-agent/src/web/search/providers/gemini.ts- Availability: OAuth credentials in
agent.dbforgoogle-gemini-cliorgoogle-antigravity. - Querying: SSE
streamGenerateContentcall with Google Search grounding enabled. Antigravity auth tries two fallback endpoints and retries401/403/400 invalid authonce after token refresh;429/5xxretry with exponential backoff and server-provided retry delay, capped by a5 * 60 * 1000ms rate-limit budget. max_tokensandtemperaturepass through asgenerationConfig.maxOutputTokens/generationConfig.temperature.limitandnum_search_resultsare collapsed together before dispatch.- Output may include
answer,sources,citations,searchQueries,usage,model.
- Availability: OAuth credentials in
- Codex β
packages/coding-agent/src/web/search/providers/codex.ts- Availability: OAuth credential for
openai-codexinagent.db(hasOAuth(); expiry is not checked here β refresh is lazy insearchCodex). - Querying: SSE POST to
https://chatgpt.com/backend-api/codex/responseswithtool_choice: { type: "web_search" }andsearch_context_size: "high"by default. - Ignores
recency,max_tokens, andtemperaturein this tool path. limitandnum_search_resultsare collapsed together before dispatch.- Output may include
answer,sources,usage,model,requestId. If the streamed response has nourl_citationannotations, the adapter falls back to scraping markdown links and bare URLs from the answer text.
- Availability: OAuth credential for
- Z.AI β
packages/coding-agent/src/web/search/providers/zai.ts- Availability: env or
agent.dbcredential forzai. - Querying: JSON-RPC
tools/callagainsthttps://api.z.ai/api/mcp/web_search_prime/mcpfor remote MCP toolweb_search_prime. - Fallback chain inside the provider: tries
{query,count}, then{search_query,count}, then{search_query, search_engine:"search-prime", count}when earlier attempts fail with argument-shape errors. limitandnum_search_resultsare collapsed together before dispatch.- Output may include parsed free-text
answer,sources,requestId.
- Availability: env or
- Exa β
packages/coding-agent/src/web/search/providers/exa.ts- Availability: env or
agent.dbcredential forexaadmits Exa to the auto chain; settings must not explicitly disableexa.enabledorexa.enableSearch. Explicit selection (providers.webSearch: exa) reaches Exa even without a credential and falls back to public MCP. - Querying: POST
https://api.exa.ai/searchwith the resolved Exa API key, otherwise JSON-RPCtools/callagainsthttps://mcp.exa.ai/mcpfor remote MCP toolweb_search_exa. limitandnum_search_resultsare collapsed together before dispatch.- Output: synthesized
answerfrom up to 3 result summaries,sources,requestId.
- Availability: env or
- Parallel β
packages/coding-agent/src/web/search/providers/parallel.ts,packages/coding-agent/src/web/parallel.ts- Availability: env or
agent.dbcredential forparallel. - Querying: POST
https://api.parallel.ai/v1beta/searchwithobjective=query,search_queries=[query],mode:"fast",max_chars_per_result: 10000, beta headersearch-extract-2025-10-10. - There is no provider fan-out here despite the name; the current adapter always sends a one-element
search_queriesarray. limitandnum_search_resultsare collapsed together before dispatch, clamped to1..40, default10.- Output:
sources,requestId.
- Availability: env or
- Kagi β
packages/coding-agent/src/web/search/providers/kagi.ts,packages/coding-agent/src/web/kagi.ts- Availability: env or
agent.dbcredential forkagi. - Querying: POST
https://kagi.com/api/v1/searchwithAuthorization: Bearer <key>and JSON body{ query, workflow: "search", limit, filters?: { after } }.recencymaps tofilters.afteras a UTCYYYY-MM-DDstring (day/week/month/year). limitandnum_search_resultsare collapsed together before dispatch, clamped to1..40, default10.- Output:
sources(concatenateddata.search+data.video+data.news+data.infobox, with video/news/infobox results tagged in the title),relatedQuestions(data.adjacent_question+data.related_searchprops.question),answer(data.direct_answer[0].snippet ?? title),requestId(meta.trace).
- Availability: env or
- Synthetic β
packages/coding-agent/src/web/search/providers/synthetic.ts- Availability: env or
agent.dbcredential forsynthetic. - Querying: POST
https://api.synthetic.new/v2/searchwith{ query }. - Ignores
recency,max_tokens, andtemperature. limitandnum_search_resultsare collapsed together before dispatch.- Output:
sourcesonly.
- Availability: env or
- SearXNG β
packages/coding-agent/src/web/search/providers/searxng.ts- Availability: endpoint from
searxng.endpointsetting orSEARXNG_ENDPOINTenv. - Querying: GET
<endpoint>/search?format=json&q=...; optional settings addcategoriesandlanguage. - Auth precedence: Basic auth (
searxng.basicUsername/searxng.basicPasswordor env equivalents) over bearer token (searxng.token/SEARXNG_TOKEN). Basic credentials are validated for RFC 7617 restrictions. recencymaps totime_range;weekis downgraded tomonthbecause SearXNG does not support week.limitandnum_search_resultsare collapsed together before dispatch, clamped to1..20, default10.- Output:
sources,relatedQuestionsfromsuggestions.
- Availability: endpoint from
- Tavily β
Side Effects
- Network
- Calls one or more external search providers over HTTPS until one succeeds or all fail.
- Provider-specific transports include JSON POST, JSON GET, SSE streaming (Perplexity OAuth/API, Gemini, Codex), and JSON-RPC over HTTP (Z.AI).
- Subprocesses / native bindings
- None.
- Session state (transcript, memory, jobs, checkpoints, registries)
- Uses a module-global provider-instance cache in
packages/coding-agent/src/web/search/provider.ts. - Uses a module-global preferred-provider setting in the same file.
packages/coding-agent/src/tools/index.tsgates tool availability behindsession.settings.get("web_search.enabled").
- Uses a module-global provider-instance cache in
- Background work / cancellation
- Many provider adapters accept
AbortSignal;WebSearchTool.execute()passes the tool call signal intoexecuteSearch(), which forwards it asparams.signalto providers and rethrows cancellation during fallback.
- Many provider adapters accept
Limits & Caps
- Provider auto-order length: 14 providers (
SEARCH_PROVIDER_ORDERinpackages/coding-agent/src/web/search/types.ts). formatForLLM()truncates source snippets and citation text to 240 chars (packages/coding-agent/src/web/search/index.ts).formatForLLM()emits at most 3 search queries, each truncated to 120 chars (packages/coding-agent/src/web/search/index.ts).- Brave result count: default
10, max20(DEFAULT_NUM_RESULTS,MAX_NUM_RESULTSinpackages/coding-agent/src/web/search/providers/brave.ts). - Tavily result count: default
5, max20(packages/coding-agent/src/web/search/providers/tavily.ts). - Kimi result count: default
10, max20; request timeout field fixed to30seconds (packages/coding-agent/src/web/search/providers/kimi.ts). - Parallel result count: default
10, max40; per-result excerpt cap10_000chars (packages/coding-agent/src/web/search/providers/parallel.ts,packages/coding-agent/src/web/parallel.ts). - Kagi result count: default
10, max40(packages/coding-agent/src/web/search/providers/kagi.ts). - SearXNG result count: default
10, max20(packages/coding-agent/src/web/search/providers/searxng.ts). - Perplexity API-key mode defaults:
max_tokens = 8192,temperature = 0.2,num_search_results = 20(packages/coding-agent/src/web/search/providers/perplexity.ts). - Anthropic defaults: model
claude-haiku-4-5,DEFAULT_MAX_TOKENS = 4096when the provider omitsmax_tokens(packages/coding-agent/src/web/search/providers/anthropic.ts). - Gemini retries: up to
3retries per endpoint, base delay1000ms, rate-limit delay budget5 * 60 * 1000ms (packages/coding-agent/src/web/search/providers/gemini.ts).
Errors
- Tool-level no-provider case returns a normal tool result with
Error: No web search provider configured.; it does not throw. - Tool-level all-failed case also returns a normal tool result with
Error: ...; the message is either the single normalized provider error or a semicolon-separated summary of all failed providers. - Provider adapters usually throw
SearchProviderError(provider, message, status)for HTTP or protocol failures. - Availability probes intentionally swallow lookup errors and report
falsein many providers viaisApiKeyAvailable(). - Per-provider notable failures:
- Anthropic: missing credentials throw a plain
Error; a404is remapped to a special final message byformatProviderError(). - Perplexity: missing auth throws a plain
Error; OAuth streamerror_codeevents becomeSearchProviderError("perplexity", ...). - Gemini: auth refresh, endpoint fallback, and retry logic are internal; final exhausted failures surface as
SearchProviderError("gemini", ...). - Codex and Gemini both fail if the HTTP response has no body after a
200. - Z.AI treats malformed SSE/JSON-RPC payloads as provider errors and retries only argument-shape failures across request variants.
- SearXNG
findAuth()can throw configuration errors before any HTTP call if Basic auth fields are incomplete or invalid.
- Anthropic: missing credentials throw a plain
Notes
- The model-facing schema does not expose
provider, but internal callers can force one throughSearchQueryParams. resolveProviderChain()lazily imports provider modules and caches singleton instances. Just asking for labels viagetSearchProviderLabel()does not trigger those imports.- Most providers treat
limitandnum_search_resultsas the same number because adapters passparams.numSearchResults ?? params.limit. Perplexity is the only implementation that preserves both concepts. recencyis implemented by Brave, Perplexity, Tavily, SearXNG, and Kagi; the model-facing prompt does not name specific providers.packages/coding-agent/src/config/settings-schema.tsuses the sharedSEARCH_PROVIDER_PREFERENCES/SEARCH_PROVIDER_OPTIONSmetadata, so the settings selector and setup wizard exposeautoplus every provider in the auto chain.- Exa uses
authStorage.getApiKey("exa"), thenEXA_API_KEY, then unauthenticatedhttps://mcp.exa.ai/mcpfallback.