# Web search and web fetch providers

Every agent has two built-in tools, `web_search` and `web_fetch`. By default they run on AgentParley's own engine.
A web-provider plugin replaces the engine behind one or both — a user's own Brave key, a private search index, a
reader service that turns pages into clean text. Agents keep calling the same tools; only what answers them
changes.

`search` and `fetch` are **independent**: implement one or both. Each becomes its own capability that the account
owner selects separately under **Settings → Engines** ("Web search" and "Reading web pages").

## `web.search`

```ts
web.search(input: { query: string; maxResults: number })
  : { results: { title: string; url: string; snippet?: string }[] }
```

The official **Brave Search** plugin, complete:

```ts
// plugins/brave-search/index.ts
globalThis.manifest = {
  egress: ["api.search.brave.com"],
  settings: [
    {
      key: "apiKey",
      title: "Brave API key",
      description: "From https://api-dashboard.search.brave.com — the free tier works.",
      type: "secret",
      required: true,
    },
  ],
};

globalThis.web.search = async (input) => {
  const apiKey = await host.settings.getSecret("apiKey");
  const count = Math.min(Math.max(input.maxResults, 1), 20); // Brave caps count at 20
  const response = await host.fetch({
    url: `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(input.query)}&count=${count}`,
    headers: { "X-Subscription-Token": apiKey, Accept: "application/json" },
  });
  if (response.status !== 200) throw new Error(`Brave search API returned ${response.status}`);
  const parsed = JSON.parse(response.body) as { web?: { results?: { title?: string; url?: string; description?: string }[] } };
  const results = (parsed.web?.results ?? [])
    .filter((result) => result.title && result.url)
    .slice(0, input.maxResults)
    .map((result) => ({ title: result.title!, url: result.url!, snippet: result.description ?? "" }));
  return { results };
};
```

Whatever you return, the platform re-caps it: at most **10 results**, titles cut to 300 characters, snippets to
500. The handler must return an object whose `results` is an array.

## `web.fetch`

```ts
web.fetch(input: { url: string }) : { content: string; url?: string; truncated?: boolean }
```

`url` in the result defaults to the input URL; `truncated` defaults to `false`. The platform caps `content` at
50,000 characters and sets `truncated` itself when it cuts.

**The sandbox can't reach an arbitrary URL** — `host.fetch` only reaches hosts you declared. A fetch provider
therefore goes through a vendor that fetches on your behalf. **Wiki Web** uses a reader service:

```ts
// plugins/wiki-web/index.ts
globalThis.manifest = {
  egress: ["en.wikipedia.org", "r.jina.ai"],
};

globalThis.web.search = async (input) => {
  const url =
    "https://en.wikipedia.org/w/api.php?action=opensearch" +
    `&search=${encodeURIComponent(input.query)}&limit=${input.maxResults}&format=json`;
  const response = await host.fetch({ url });
  const [, titles, snippets, urls] = JSON.parse(response.body) as [string, string[], string[], string[]];
  return {
    results: titles.map((title, index) => ({
      title,
      url: urls[index],
      snippet: snippets[index] ?? "",
    })),
  };
};

globalThis.web.fetch = async (input) => {
  const response = await host.fetch({ url: `https://r.jina.ai/${input.url}` });
  return { content: response.body };
};
```

## Failures are visible, never swapped

If your handler throws or times out, the agent sees a *provider failed* error from `web_search` / `web_fetch`. The
platform does **not** quietly fall back to the built-in engine: the user chose your vendor, your egress and your
quota, and that choice is respected. Throw a clear message — it's what the model reads.

## What a web provider can reach

The same host surface as a middleware hook with the same permissions: `host.fetch`, `host.settings`,
`host.crypto`, `host.log`, and anything else your code's calls derive. 30 s wall clock per call.

## Setup for the user

1. Install the plugin and fill in its settings (for example the API key).
2. **Settings → Engines**: choose it for **Web search** and/or **Reading web pages**.

The selection is per account: every agent's `web_search` / `web_fetch` then goes through your plugin.

See the [Brave Search walkthrough](example-brave-search.md).
