# Multi-source search and classifier reference

[`search`](../src/search.ts) with `effort: "thorough"` enables opt-in concurrent retrieval, not a long-running research agent. External classifier judgment is a second opt-in, with Jev as the default model. Fast searches never run it. See the [package README](../README.md) for installation, retrieval defaults, credentials, and privacy limits.

## Enable multi-source search

Merge into the config file printed by `/web-search`, then run `/reload`:

```json
{ "research": { "enabled": true } }
```

Call `search` with a query, scope, and thorough effort:

```json
{ "query": "AbortSignal.timeout", "scope": "both", "effort": "thorough" }
```

`web` consults eligible Exa/Parallel sources and Brave when a key is present; Jina is extraction-only and is never a search source; `code` consults eligible grep.app/Sourcegraph/GitHub sources; `both` sends the same query to both families. URL extraction is a separate `search({urls: [...]})` operation and does not fan out. Use identifiers/snippets rather than prose for code or mixed scope. Without retrieval keys, Exa, Parallel native MCP, grep.app, and Sourcegraph remain eligible. Check `/mcp` for native connection status; a same-name `pi_web_search_parallel` entry in `mcp.json` overrides the package server. Pi SDK embeddings must load Pi’s MCP extension and call `bindExtensions()` for native Parallel retrieval; codemode is optional (see the README).

Thorough retrieval appends family defaults to configured preferences, then runs at most two available transports per family concurrently (at most four for mixed scope). Empty fallback arrays do not exclude providers. Successful responses are merged; citation URLs and exact duplicate URL/passage/type evidence are deduplicated. Different passages, query strings, code anchors, and search-versus-fetch evidence stay distinct. After optional judgment, one global `maxResults` preview limit applies; additional admitted evidence can be read locally with `search({ref: "..."})`. Within a live operation deadline, one failed source becomes a warning if another succeeds. All-source failure, caller cancellation, or a shared deadline expiring during retrieval fails the call, even after a sibling succeeded. Coverage is not agreement analysis or proof of exhaustive search.

The single `search` tool is always registered. Thorough execution checks the current opt-in: disabling it takes effect on the next call. The settings report reads configuration; it is not a provider-health test. Reload after provider-registration changes so native MCP connections match the new configuration.

## Enable external judgment

Authenticate a classifier in Pi, then pin that exact catalog pair:

```text
/web-search classifier
```

The picker lists Pi's registered classifiers plus **None**. `/web-search classifier typesafe/jev-latest` pins that exact pair without discovery. `/web-search on` prefers an available Jev model (Vercel first), then a sole available classifier. Explicit pins are preserved. Discovery stops after three seconds; a failure leaves settings unchanged.

`/web-search classifier none` disables judgment only. The command atomically merges nonsecret settings, preserves tuning, and refuses malformed files. Retrieval keys are stored separately through `/web-search provider` into `auth.json`.

```json
{
  "research": { "enabled": true },
  "classifier": { "enabled": true, "provider": "typesafe", "model": "jev-latest" }
}
```

Judgment config changes are read on the next operation. Pi owns catalog, authentication, and `classify()`. See [classifier models](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md#use-classifier-models).

**Data sent:** at most one judgment request contains the query and every retained candidate's index, title, URL, and complete provider-returned `citedText`. This happens before formatter preview caps and before safety suppression. All candidates share one request with relevance, off-topic, self-contained, safety, and set-sufficiency questions. Do not enable it for evidence you are unwilling to send to the selected external service. This is a heuristic, not a security firewall or verified-answer guarantee.

## All classifier settings

All fields are optional. This block shows the defaults; it deliberately leaves both opt-ins off:

```json
{
  "research": { "enabled": false },
  "classifier": {
    "enabled": false,
    "provider": "vercel-ai-gateway",
    "model": "typesafe-ai/jev",
    "weights": {
      "answers": 0.45,
      "offtopic": -0.3,
      "selfcontained": 0.25
    },
    "safetyThreshold": 0.75,
    "maxStateChars": 24000
  }
}
```

| Field | Behavior |
| --- | --- |
| `enabled` | Only literal `true` enables judgment. Other values leave it off. Research must also be enabled and the call must request thorough effort; fast web/code searches never classify. |
| `provider` | Exact Pi classifier provider; defaults to `vercel-ai-gateway` only when both provider and model are omitted. Explicit empty/partial pins stay unpinned. |
| `model` | Exact Pi catalog identifier for that provider; defaults to `typesafe-ai/jev` only when both provider and model are omitted. No alias remapping or runtime model fallback. An incomplete or unavailable explicit pair returns unjudged evidence with a warning. |
| `weights.answers` | Weight for answer relevance. |
| `weights.offtopic` | Signed weight for off-topic score; default negative penalizes off-topic evidence. |
| `weights.selfcontained` | Weight for usability without missing context. |
| `safetyThreshold` | A non-safe classification is suppressed when its winning-choice probability reaches this threshold. Policy clamps it to `[0,1]`. It is **not** a minimum confidence required to admit a `safe` result. |
| `maxStateChars` | Skip judgment if query + candidate title/URL/excerpt string lengths exceed this budget. It measures JavaScript characters, not encoded request bytes or tokens. Retrieval is still returned. |

Finite numeric values are accepted for weights and the two numeric settings; bad known values default with warnings. Weights are not normalized to sum to one. Unknown classifier fields/weight names or malformed blocks fail as `invalid_config`. These settings are parsed in [`providers/config.ts`](../src/providers/config.ts); ranking/suppression policy lives in [`classifier/judge.ts`](../src/classifier/judge.ts).

There is no `classifier.timeoutMs` config field. The classifier has a fixed **8,000 ms request deadline**, limited by the remaining shared `timeoutMs` operation budget. Increasing `timeoutMs` does not increase that judge-specific deadline.

## Pi classifier ownership

This package never sends a classifier HTTP request or resolves classifier credentials. It calls `findOfType("classifier", provider, model)`, checks that exact pair in `getAvailableOfType()`, then `classify()`. The default pair is `vercel-ai-gateway/typesafe-ai/jev`. Explicit pins are exact: there is no gateway ladder or local→cloud fallback. Authenticate in Pi; billing follows the pinned provider. Classifier credentials belong to Pi, not this package. Old `jev` blocks and `classifier.backend` are rejected; there are no compatibility aliases. Missing pin, unavailable model, malformed answers, provider failure, or a classifier deadline leave retrieval unchanged with a warning; caller abort remains fatal. Pi classifier usage is attached to the tool result; nested MCP usage is counted separately.

## Outcomes and limits

`classifierStatus` in renderer/debug details distinguishes (ordinary responses omit this diagnostic):

| Status | Meaning |
| --- | --- |
| `disabled` | Not requested by this tool or config. Ordinary searches always use this status. |
| `unavailable` | Missing Pi classifier/credentials, service/malformed-answer failure, or a deadline during optional judgment. Retrieval remains, with warnings. |
| `skipped` | No candidates after enabling, or the state exceeds `maxStateChars` (with a budget warning). |
| `ran` | A response was processed; it does not certify complete or trustworthy judgments. |

Ranking is local weighted scoring; missing relevance scores use `0.5`. Confident non-safe classifications are suppressed, while uncertain/missing safety judgments can remain with safety-unverified warnings. There is no separate minimum safe-confidence gate. Missing sufficiency is treated as insufficient. Any suppression prevents a positive sufficiency claim for the surviving set because the global judgment saw the withheld candidates; survivors are not judged again.

When suppression occurs, aggregate provider prose and original provider warnings are conservatively withheld because they may quote suppressed content. Count-based notices, admitted results, citation audit URLs, and local config/input notices remain. This can hide otherwise useful upstream diagnostics. Renderer/debug `classifier` details record `sufficient`, `lowConfidence`, `suppressed`, and `suppressedUrls`; an unknown suppressed URL is reported as `(unknown)`, not the withheld title.

Missing Pi classifier/authentication, over-budget state, failures, and judgment deadlines **fail open** to the retrieved evidence, subject to normal output clipping. Genuine caller cancellation remains fatal. Jev does not guarantee truth, safe instructions, independence among sources, or agreement, and does not trigger another search. Inspect warnings and citations rather than treating `ran` or `sufficient` as a security decision. The implementation boundary is [`classifier/augment.ts`](../src/classifier/augment.ts).
