Web search and fetch tools

Mock tools teach you the shape. Real tools teach you the failure modes. A web search tool hits an external API that sometimes rate limits you. A fetch tool reads arbitrary URLs and has to defend against huge responses and timeouts. Both patterns show up in every production agent you will ship.

lib/tools/web-search.ts
typescript
import { tool } from 'ai';
import { z } from 'zod';

export const webSearch = tool({
  description:
    'Search the web and return a ranked list of result snippets. Use for recent news or facts beyond the model training cutoff.',
  parameters: z.object({
    query: z.string().min(1).max(200).describe('The search query'),
  }),
  execute: async ({ query }) => {
    const res = await fetch('https://api.tavily.com/search', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        api_key: process.env.TAVILY_API_KEY,
        query,
        max_results: 5,
      }),
      signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) {
      return { error: `Search failed with status ${res.status}`, query };
    }
    const data = await res.json();
    return { query, results: data.results };
  },
});

Tavily is a solid default search provider for agents. Note the query length cap, the timeout, and the error path. Tools that never fail are tools that have never been used in production.

lib/tools/fetch-url.ts
typescript
import { tool } from 'ai';
import { z } from 'zod';

const MAX_BYTES = 200_000;

export const fetchUrl = tool({
  description:
    'Fetch a URL and return the response body as text. Use to read articles or docs the search tool found.',
  parameters: z.object({
    url: z.string().url().describe('Absolute https URL to fetch'),
  }),
  execute: async ({ url }) => {
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      return { error: 'Only https URLs are allowed', url };
    }
    const res = await fetch(url, { signal: AbortSignal.timeout(8_000) });
    if (!res.ok) {
      return { error: `Fetch failed with status ${res.status}`, url };
    }
    const body = await res.text();
    return { url, body: body.slice(0, MAX_BYTES), truncated: body.length > MAX_BYTES };
  },
});

Stacked guardrails on a single tool: https only, timeout, byte cap. The model does not get to read a 10MB response and blow your context window.

Quiz: Quiz

Loading practice…