# web_search > 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` — unified `SearchResponse` / `SearchProviderError` types. - `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. `executeSearch()` rewrites any `2020`-`2029` substring to the current year before dispatch. | | `recency` | `"day" \| "week" \| "month" \| "year"` | No | Time filter. Only providers that implement it use it. Prompt text says Brave and Perplexity; code also maps it for Tavily and SearXNG. | | `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`: `SearchRenderDetails` from `packages/coding-agent/src/web/search/render.ts` - `response: SearchResponse` - `error?: string` `text` is produced by `formatForLLM()` in `packages/coding-agent/src/web/search/index.ts`: - If `response.answer` exists, it is emitted first. - If sources exist, a `## Sources` section follows with a source count, then one entry per source: - `[n] (<formatted age or published date>)` - ` <url>` - optional snippet line truncated to 240 chars. - If citations exist, a `## Citations` section follows with URL/title plus optional cited text truncated to 240 chars. - If related questions exist, a `## Related` bullet 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 1. `WebSearchTool.execute()` in `packages/coding-agent/src/web/search/index.ts` delegates directly to `executeSearch()`. 2. `executeSearch()` chooses a provider list: - if `params.provider` is set and not `"auto"`, it loads that provider with `getSearchProvider()`; if `isAvailable()` returns true, the list is `[that provider]`, otherwise it falls back to `resolveProviderChain("auto")`. - otherwise it calls `resolveProviderChain()` with the module-global preferred provider from `packages/coding-agent/src/web/search/provider.ts`. 3. `resolveProviderChain()` lazily loads each provider module on demand, checks `isAvailable()`, and returns only available providers. If a preferred provider is set, it is tried first, then the static `SEARCH_PROVIDER_ORDER` excluding that provider. 4. If no providers are available, `executeSearch()` returns `Error: No web search provider configured.` with `details.response.provider = "none"`. 5. For each provider in order, `executeSearch()` calls `provider.search()` with: - `query` after year-rewrite, - `limit`, `recency`, `temperature`, `maxOutputTokens`, `numSearchResults`, - `systemPrompt` from `packages/coding-agent/src/prompts/tools/web-search.md`. 6. On the first successful `SearchResponse`, `formatForLLM()` renders answer/sources/citations/related/search-queries into one text block and returns it with `details.response`. 7. If a provider throws, `executeSearch()` records the error and tries the next provider. There is no provider-level parallel fan-out; fallback is sequential. 8. After all candidates fail, `formatProviderError()` normalizes each error: - Anthropic `404` becomes `Anthropic web search returned 404 (model or endpoint not found).` - `401`/`403` become `<Provider> authorization failed ...` except Z.AI, which preserves its raw message. - other `SearchProviderError`s surface `error.message`. 9. 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 by `resolveProviderChain()`. `packages/coding-agent/src/sdk.ts` and `packages/coding-agent/src/modes/controllers/selector-controller.ts` wire this from settings. - **Auto chain order**: `tavily`, `perplexity`, `brave`, `jina`, `kimi`, `anthropic`, `gemini`, `codex`, `zai`, `exa`, `parallel`, `kagi`, `synthetic`, `searxng` (`SEARCH_PROVIDER_ORDER` in `packages/coding-agent/src/web/search/provider.ts`). - **Provider adapters** - **Tavily** — `packages/coding-agent/src/web/search/providers/tavily.ts` - Availability: API key from env or `agent.db` via `findCredential()`. - Querying: POST `https://api.tavily.com/search`. - `recency` maps to Tavily `time_range`; code explicitly keeps `topic` at default general scope instead of narrowing to news. - `limit` / `num_search_results`: adapter uses `params.numSearchResults ?? params.limit`, clamped to `5..20` with default `5`. - Output: `answer`, `sources`, `requestId`, `authMode: "api_key"`. - **Perplexity** — `packages/coding-agent/src/web/search/providers/perplexity.ts` - Availability: auth precedence is `PERPLEXITY_COOKIES` -> OAuth token in `agent.db` -> `PERPLEXITY_API_KEY` / `PPLX_API_KEY`. - OAuth/cookie mode: POSTs to `https://www.perplexity.ai/rest/sse/perplexity_ask`, consumes SSE, merges partial events, extracts answer and source URLs, sets `authMode: "oauth"`. - API-key mode: POSTs to `https://api.perplexity.ai/chat/completions` with `model: "sonar-pro"`, `search_mode: "web"`, `num_search_results`, optional `search_recency_filter`, `max_tokens`, `temperature`. - `num_search_results` controls upstream API breadth only in API-key mode. `limit` is preserved separately as `num_results` and slices returned `sources` after parsing in both auth modes. - Output may include `answer`, `sources`, `citations`, `usage`, `model`, `requestId`, `authMode`. - **Brave** — `packages/coding-agent/src/web/search/providers/brave.ts` - Availability: `BRAVE_API_KEY` only. - Querying: GET `https://api.search.brave.com/res/v1/web/search` with `count`, `extra_snippets=true`, and `freshness=pd|pw|pm|py` for `recency`. - `limit` / `num_search_results`: `params.numSearchResults ?? params.limit`, clamped to `1..20`, default `10`. - Output: `sources`, `requestId`. - **Jina** — `packages/coding-agent/src/web/search/providers/jina.ts` - Availability: `JINA_API_KEY` only. - Querying: GET-like fetch to `https://s.jina.ai/<encoded query>` with bearer auth. - Ignores `recency`, `max_tokens`, and `temperature`. - `limit` / `num_search_results`: adapter slices sources to `params.numSearchResults ?? params.limit` when provided; otherwise returns all payload items. - Output: `sources` only. - **Kimi** — `packages/coding-agent/src/web/search/providers/kimi.ts` - Availability: `MOONSHOT_SEARCH_API_KEY`, `KIMI_SEARCH_API_KEY`, `MOONSHOT_API_KEY`, or `agent.db` credentials for `moonshot` / `kimi-code`. - Querying: POST to `MOONSHOT_SEARCH_BASE_URL` / `KIMI_SEARCH_BASE_URL` / default `https://api.kimi.com/coding/v1/search` with `text_query`, `limit`, `enable_page_crawling`, `timeout_seconds: 30`. - `limit` / `num_search_results`: `params.numSearchResults ?? params.limit`, clamped to `1..20`, default `10`. - Output: `sources`, `requestId`. - **Anthropic** — `packages/coding-agent/src/web/search/providers/anthropic.ts` - Availability: `findAnthropicAuth()` from `@oh-my-pi/pi-ai`. - Querying: Claude Messages API with web-search tool enabled. - `max_tokens` and `temperature` pass through. - `limit` and `num_search_results` are collapsed together before dispatch: `num_results = params.numSearchResults ?? params.limit`. - Output may include `answer`, `sources`, `citations`, `searchQueries`, `usage.searchRequests`, `model`, `requestId`. - **Gemini** — `packages/coding-agent/src/web/search/providers/gemini.ts` - Availability: OAuth credentials in `agent.db` for `google-gemini-cli` or `google-antigravity`. - Querying: SSE `streamGenerateContent` call with Google Search grounding enabled. Antigravity auth tries two fallback endpoints and retries `401/403/400 invalid auth` once after token refresh; `429/5xx` retry with exponential backoff and server-provided retry delay, capped by a `5 * 60 * 1000` ms rate-limit budget. - `max_tokens` and `temperature` pass through as `generationConfig.maxOutputTokens` / `generationConfig.temperature`. - `limit` and `num_search_results` are collapsed together before dispatch. - Output may include `answer`, `sources`, `citations`, `searchQueries`, `usage`, `model`. - **Codex** — `packages/coding-agent/src/web/search/providers/codex.ts` - Availability: non-expired OAuth credential for `openai-codex` in `agent.db`. - Querying: SSE POST to `https://chatgpt.com/backend-api/codex/responses` with `tool_choice: { type: "web_search" }` and `search_context_size: "high"` by default. - Ignores `recency`, `max_tokens`, and `temperature` in this tool path. - `limit` and `num_search_results` are collapsed together before dispatch. - Output may include `answer`, `sources`, `usage`, `model`, `requestId`. If the streamed response has no `url_citation` annotations, the adapter falls back to scraping markdown links and bare URLs from the answer text. - **Z.AI** — `packages/coding-agent/src/web/search/providers/zai.ts` - Availability: env or `agent.db` credential for `zai`. - Querying: JSON-RPC `tools/call` against `https://api.z.ai/api/mcp/web_search_prime/mcp` for remote MCP tool `web_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. - `limit` and `num_search_results` are collapsed together before dispatch. - Output may include parsed free-text `answer`, `sources`, `requestId`. - **Exa** — `packages/coding-agent/src/web/search/providers/exa.ts` - Availability: always true unless settings explicitly disable `exa.enabled` or `exa.enableSearch`; the adapter can use public MCP even without `EXA_API_KEY`. - Querying: with `EXA_API_KEY`, POST `https://api.exa.ai/search`; otherwise call MCP tool `web_search_exa`. - `limit` and `num_search_results` are collapsed together before dispatch. - Output: synthesized `answer` from up to 3 result summaries, `sources`, `requestId`. - **Parallel** — `packages/coding-agent/src/web/search/providers/parallel.ts`, `packages/coding-agent/src/web/parallel.ts` - Availability: env or `agent.db` credential for `parallel`. - Querying: POST `https://api.parallel.ai/v1beta/search` with `objective=query`, `search_queries=[query]`, `mode:"fast"`, `max_chars_per_result: 10000`, beta header `search-extract-2025-10-10`. - There is no provider fan-out here despite the name; the current adapter always sends a one-element `search_queries` array. - `limit` and `num_search_results` are collapsed together before dispatch, clamped to `1..40`, default `10`. - Output: `sources`, `requestId`. - **Kagi** — `packages/coding-agent/src/web/search/providers/kagi.ts`, `packages/coding-agent/src/web/kagi.ts` - Availability: env or `agent.db` credential for `kagi`. - Querying: GET `https://kagi.com/api/v0/search?q=<query>&limit=<n>` with `Authorization: Bot <key>`. - `limit` and `num_search_results` are collapsed together before dispatch, clamped to `1..40`, default `10`. - Output: `sources`, `relatedQuestions`, `requestId`. - **Synthetic** — `packages/coding-agent/src/web/search/providers/synthetic.ts` - Availability: env or `agent.db` credential for `synthetic`. - Querying: POST `https://api.synthetic.new/v2/search` with `{ query }`. - Ignores `recency`, `max_tokens`, and `temperature`. - `limit` and `num_search_results` are collapsed together before dispatch. - Output: `sources` only. - **SearXNG** — `packages/coding-agent/src/web/search/providers/searxng.ts` - Availability: endpoint from `searxng.endpoint` setting or `SEARXNG_ENDPOINT` env. - Querying: GET `<endpoint>/search?format=json&q=...`; optional settings add `categories` and `language`. - Auth precedence: Basic auth (`searxng.basicUsername` / `searxng.basicPassword` or env equivalents) over bearer token (`searxng.token` / `SEARXNG_TOKEN`). Basic credentials are validated for RFC 7617 restrictions. - `recency` maps to `time_range`; `week` is downgraded to `month` because SearXNG does not support week. - `limit` and `num_search_results` are collapsed together before dispatch, clamped to `1..20`, default `10`. - Output: `sources`, `relatedQuestions` from `suggestions`. ## 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.ts` gates tool availability behind `session.settings.get("web_search.enabled")`. - Background work / cancellation - Many provider adapters accept `AbortSignal`; `WebSearchTool.execute()` passes the tool call signal into `executeSearch()`, which forwards it as `params.signal` to providers and rethrows cancellation during fallback. ## Limits & Caps - Provider auto-order length: 14 providers (`SEARCH_PROVIDER_ORDER` in `packages/coding-agent/src/web/search/provider.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`, max `20` (`DEFAULT_NUM_RESULTS`, `MAX_NUM_RESULTS` in `packages/coding-agent/src/web/search/providers/brave.ts`). - Tavily result count: default `5`, max `20` (`packages/coding-agent/src/web/search/providers/tavily.ts`). - Kimi result count: default `10`, max `20`; request timeout field fixed to `30` seconds (`packages/coding-agent/src/web/search/providers/kimi.ts`). - Parallel result count: default `10`, max `40`; per-result excerpt cap `10_000` chars (`packages/coding-agent/src/web/search/providers/parallel.ts`, `packages/coding-agent/src/web/parallel.ts`). - Kagi result count: default `10`, max `40` (`packages/coding-agent/src/web/search/providers/kagi.ts`). - SearXNG result count: default `10`, max `20` (`packages/coding-agent/src/web/search/providers/searxng.ts`). - Perplexity API-key mode defaults: `max_tokens = 8192`, `temperature = 0.2`, `num_search_results = 10` (`packages/coding-agent/src/web/search/providers/perplexity.ts`). - Anthropic defaults: model `claude-haiku-4-5`, `DEFAULT_MAX_TOKENS = 4096` when the provider omits `max_tokens` (`packages/coding-agent/src/web/search/providers/anthropic.ts`). - Gemini retries: up to `3` retries per endpoint, base delay `1000` ms, rate-limit delay budget `5 * 60 * 1000` ms (`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 `false` in many providers via `isApiKeyAvailable()`. - Per-provider notable failures: - Anthropic: missing credentials throw a plain `Error`; a `404` is remapped to a special final message by `formatProviderError()`. - Perplexity: missing auth throws a plain `Error`; OAuth stream `error_code` events become `SearchProviderError("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. ## Notes - The model-facing schema does not expose `provider`, but internal callers can force one through `SearchQueryParams`. - `resolveProviderChain()` lazily imports provider modules and caches singleton instances. Just asking for labels via `getSearchProviderLabel()` does not trigger those imports. - Most providers treat `limit` and `num_search_results` as the same number because adapters pass `params.numSearchResults ?? params.limit`. Perplexity is the only implementation that preserves both concepts. - The prompt says `recency` is for Brave and Perplexity, but code also implements it for Tavily and SearXNG. - The year rewrite in `executeSearch()` is blunt: any `2020`-`2029` substring is replaced with the current year. - `packages/coding-agent/src/config/settings-schema.ts` exposes provider preferences for `auto`, `exa`, `brave`, `jina`, `kimi`, `perplexity`, `anthropic`, `zai`, `tavily`, `kagi`, `synthetic`, `parallel`, and `searxng`. Gemini and Codex are in the registry and auto chain but not in that settings enum. - Exa availability is optimistic. Unless settings disable it, the provider stays in the chain even without an API key because it can fall back to MCP.