Building
Web search and web fetch providers
Replace the engine behind the built-in web_search and web_fetch tools.
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 "Web fetch").
web.search
web.search(input: { query: string; maxResults: number })
: { results: { title: string; url: string; snippet?: string }[] }
The official Brave Search plugin, complete:
// 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
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:
// 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
- Install the plugin and fill in its settings (for example the API key).
- Settings → Engines: choose it for Web search and/or Web fetch.
The selection is per account: every agent's web_search / web_fetch then goes through your plugin.
See the Brave Search walkthrough.