> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nimbleway.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Vercel AI SDK

> Integrate Nimble with the Vercel AI SDK to give your AI agents real-time web search, page extraction, and cited deep research.

## Overview

The [`@nimble-way/ai-sdk`](https://www.npmjs.com/package/@nimble-way/ai-sdk) package provides pre-built tools for Vercel's AI SDK v6. Register them on an agent and the model decides when to search the web, read a page, or commission deeper research. Nimble runs the request and returns clean, structured results to cite.

* **Five tools, zero boilerplate**: search, extract, and the three deep-research run tools.
* **Works with any model**: OpenAI, Anthropic, Google, and others supported by the AI SDK.
* **Two search depths**: `lite` for fast metadata, `deep` for full page content.
* **Asynchronous deep research**: start a Web Search Agent run, collect the cited answer minutes later.
* **Type-safe**: written in TypeScript with typed options and output.

These are app-side AI SDK `tool()` definitions, not provider-executed search. They behave the same whether the model call routes through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) or a provider SDK directly. The gateway, when present, routes only the model call.

<Note>
  Ships **Search**, **Extract**, and **Web Search Agent** runs. Map and Crawl are planned as follow-ups.
</Note>

<Note>
  Building on [eve](https://eve.dev), Vercel's agent framework? Use the [Vercel eve connector](/integrations/connectors/vercel-eve) instead. It mounts the same capabilities as an eve extension and can replace eve's built-in `web_search` and `web_fetch`.
</Note>

## Prerequisites

<AccordionGroup>
  <Accordion title="Node.js 18 or later">
    The package targets the Node.js runtime. Edge and serverless runtimes are expected to work but are not yet verified, so prefer the Node runtime.
  </Accordion>

  <Accordion title="Vercel AI SDK v6">
    `ai` (v6) and `zod` (`^3.25.76` or `^4.1.8`) are peer dependencies. Your app supplies both.
  </Accordion>

  <Accordion title="NIMBLE_API_KEY (server-side)">
    Required by every tool. Get a key from the [dashboard](https://online.nimbleway.com/settings/api-keys). Keep it server-side: the key never appears in model inputs or tool outputs.
  </Accordion>

  <Accordion title="NIMBLE_AGENT_ID (deep research only)">
    Required by the three agent run tools. A Web Search Agent instance, created once in the dashboard or through `POST /v2/agents`. The ID looks like `wsa_01j9x8...`. The search and extract tools do not need it.
  </Accordion>
</AccordionGroup>

## Quick Start

<Steps>
  <Step title="Install">
    ```bash theme={"system"}
    npm install @nimble-way/ai-sdk ai @ai-sdk/openai
    ```

    `ai` (v6) and `zod` are peer dependencies. The examples use OpenAI via [`@ai-sdk/openai`](https://www.npmjs.com/package/@ai-sdk/openai), but `nimbleSearch` works with any AI SDK model provider.
  </Step>

  <Step title="Set your API keys">
    Get a Nimble key from the [dashboard](https://online.nimbleway.com/settings/api-keys) (free trial available), then set both keys:

    ```bash theme={"system"}
    export NIMBLE_API_KEY="your-api-key"
    export OPENAI_API_KEY="your-openai-api-key"
    ```

    You can also pass the Nimble key inline: `nimbleSearch({ apiKey: '...' })`.

    For [deep research](#deep-research-with-web-search-agents), also set `NIMBLE_AGENT_ID`.
  </Step>

  <Step title="Add the tool to an agent">
    ```ts theme={"system"}
    import { generateText, stepCountIs } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import { nimbleSearch } from '@nimble-way/ai-sdk';

    const { text } = await generateText({
      model: openai('gpt-5'),
      prompt: 'What are the latest developments in agentic web search? Cite sources.',
      tools: {
        webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }),
      },
      stopWhen: stepCountIs(3),
    });

    console.log(text);
    ```
  </Step>
</Steps>

## How it works

<Steps>
  <Step title="The model receives the tool">
    `nimbleSearch()` registers a `webSearch` tool the model can call when it needs current information.
  </Step>

  <Step title="The model decides to search">
    When the prompt needs live data, the model emits a tool call with a `query` (and optional `maxResults`).
  </Step>

  <Step title="Nimble runs the search">
    The query goes to Nimble's Web Search API, which returns clean, structured results.
  </Step>

  <Step title="The model answers">
    Results are fed back to the model, which uses them to write a grounded, citable answer. `stopWhen: stepCountIs(n)` caps how many search rounds a single turn can take.
  </Step>
</Steps>

<Warning>
  Use `stepCountIs`, not `isStepCount`. The latter does not exist in `ai` v6. Set it on every agent to prevent runaway loops and unbounded cost: `3`–`5` for chat, higher for autonomous agents.
</Warning>

## Search

`nimbleSearch()` grounds an answer in live web results. Configure it once; the model only ever supplies `{ query, maxResults? }`.

### Next.js route handler

For a streaming chat app, swap `generateText` for `streamText` inside a route handler and return `toUIMessageStreamResponse()`. The client connects with the AI SDK [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) hook, with no extra wiring needed.

```ts theme={"system"}
// app/api/chat/route.ts
import {
  convertToModelMessages,
  streamText,
  stepCountIs,
  type UIMessage,
} from 'ai';
import { openai } from '@ai-sdk/openai';
import { nimbleSearch } from '@nimble-way/ai-sdk';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai('gpt-5'),
    messages: await convertToModelMessages(messages),
    tools: {
      webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }),
    },
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}
```

### Search options

<AccordionGroup>
  <Accordion title="apiKey">
    Nimble API credentials. Defaults to `process.env.NIMBLE_API_KEY`.
  </Accordion>

  <Accordion title="searchDepth">
    `'lite'` returns metadata only (fast); `'deep'` returns full page content. Default `'lite'`.
  </Accordion>

  <Accordion title="maxResults">
    Default number of results per search. Type `number`, default `5`.
  </Accordion>

  <Accordion title="maxResultsCap">
    Hard upper limit on results the model can request. Type `number`, default `10`.
  </Accordion>

  <Accordion title="maxContentLength">
    Per-result content truncation, in characters. Type `number`, default `10_000`.
  </Accordion>

  <Accordion title="country">
    Two-letter country code for localization. Type `string`, default `'US'`.
  </Accordion>

  <Accordion title="locale">
    Language preference. Type `string`, default `'en'`.
  </Accordion>

  <Accordion title="client">
    Injectable `NimbleSearchClient` for testing. Optional.
  </Accordion>
</AccordionGroup>

### Search response

Each tool call returns a structured result the model can reason over:

```ts theme={"system"}
{
  query: string;
  requestId?: string;
  totalResults?: number;
  results: Array<{
    title: string;
    url: string;
    description?: string;
    content?: string;     // deep searches only
    position?: number;
    entityType?: string;
  }>;
}
```

## Extract

`nimbleExtract()` registers an `extract` tool that takes a single URL and returns clean page content, markdown by default, for the model to read, quote, or summarize. The model only ever supplies `{ url }`; all policy below is developer-controlled.

```ts theme={"system"}
import { generateText, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { nimbleExtract } from '@nimble-way/ai-sdk';

const { text } = await generateText({
  model: openai('gpt-5'),
  prompt: 'Summarize https://en.wikipedia.org/wiki/Web_scraping',
  tools: { extract: nimbleExtract({ format: 'markdown' }) },
  // Allow a step after the tool call so the model can read the page.
  stopWhen: stepCountIs(2),
});

console.log(text);
```

Register both tools together so the model can search, then read the best result:

```ts theme={"system"}
tools: {
  webSearch: nimbleSearch(),
  extract: nimbleExtract(),
}
```

### Extract options

Configure `nimbleExtract()` once; the model only ever supplies `{ url }`.

<AccordionGroup>
  <Accordion title="apiKey">
    Nimble API credentials. Defaults to `process.env.NIMBLE_API_KEY`.
  </Accordion>

  <Accordion title="format">
    Content format returned to the model: `'markdown'` or `'html'`. Default `'markdown'`.
  </Accordion>

  <Accordion title="country">
    Two-letter ISO country code for geolocation / proxy. Type `string`, optional.
  </Accordion>

  <Accordion title="maxContentLength">
    Extracted content truncation, in characters. Type `number`, default `50_000`.
  </Accordion>

  <Accordion title="client">
    Injectable `NimbleExtractClient` for testing. Optional.
  </Accordion>
</AccordionGroup>

### Extract response

```ts theme={"system"}
{
  url: string;
  status: string;        // e.g. 'success'
  statusCode?: number;
  format: 'markdown' | 'html';
  content: string;       // truncated to maxContentLength
  links?: string[];
}
```

## Deep research with Web Search Agents

`nimbleSearch()` and `nimbleExtract()` are synchronous building blocks: one call, one response, seconds. A Web Search Agent run is a different capability. An autonomous agent plans, searches, reads, and cross-checks many sources, then returns a final answer with per-claim citations and confidence. It takes minutes, not seconds.

| | Search / Extract | Web Search Agent run |
| - | - | - |
| Latency | Seconds | Minutes, depending on effort |
| Shape | Request, then response | Start, check status, fetch result |
| Output | Ranked results or page content | Final answer plus sources, citations, confidence |
| Best for | Grounding a chat turn | Briefs, due diligence, monitoring, enrichment |

Because a run outlives any sensible HTTP request, the package splits the lifecycle across three tools. By default, no chat request is held open for the research duration.

<CardGroup cols={3}>
  <Card title="nimbleAgentStartRun()" icon="play">
    Starts a run. Returns the run ID immediately, without waiting for the research.
  </Card>

  <Card title="nimbleAgentRunStatus()" icon="gauge">
    Reports the current status. Instant, and never waits.
  </Card>

  <Card title="nimbleAgentRunResult()" icon="file-check">
    Fetches the answer once the run completes.
  </Card>
</CardGroup>

### Start now, answer later

Point `NIMBLE_AGENT_ID` at your agent instance, then start the run and persist the returned `runId`.

```ts theme={"system"}
// Request 1: start the run. Returns without waiting for the research.
import { generateText, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { nimbleAgentStartRun } from '@nimble-way/ai-sdk';
import type { NimbleAgentStartRunOutput } from '@nimble-way/ai-sdk';

const { text, steps } = await generateText({
  model: openai('gpt-5'),
  prompt:
    'Start deep research on the EU AI Act enforcement timeline, then tell ' +
    'the user research is underway and the answer takes a few minutes.',
  tools: { startResearch: nimbleAgentStartRun({ effort: 'medium' }) },
  stopWhen: stepCountIs(2),
});

// Pull the run ID out of the tool result and persist it.
const started = steps
  .flatMap((step) => step.toolResults)
  .find((result) => result.toolName === 'startResearch')?.output as
  | NimbleAgentStartRunOutput
  | undefined;

if (started) {
  // Persist the run ID however your app stores state: database,
  // session, or job queue. `saveRunId` here is your own function.
  await saveRunId(started.runId); // e.g. 'task_run_8f3c...'
}
```

The start tool returns the handle you need to resume:

```ts theme={"system"}
{
  runId: 'task_run_8f3c...',   // pass this to the status and result tools
  agentId: 'wsa_01j9x8...',
  interactionId: 'int_...',
  status: 'queued',
  effort: 'medium',
  createdAt: '2026-07-31T10:00:00Z',
}
```

<Warning>
  Preserve `runId` outside the request. Write it to your database, session, or job queue. Research runs take minutes, so a run started in one request is almost never finished in that same request. Holding an ordinary chat request open until completion times out the request and wastes the async design.
</Warning>

Minutes later, in a different request, server, or process, only the `runId` crosses over.

```ts theme={"system"}
// Request 2: minutes later, a different request or process.
import { generateText, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { nimbleAgentRunResult } from '@nimble-way/ai-sdk';
import type { NimbleAgentRunResultOutput } from '@nimble-way/ai-sdk';

const { text, steps } = await generateText({
  model: openai('gpt-5'),
  prompt:
    `The user is back. Fetch research run ${runId}. If it is not ready, say ` +
    `so briefly. If it is, answer from it and keep the citation callouts.`,
  tools: { getResearchResult: nimbleAgentRunResult() },
  stopWhen: stepCountIs(2),
});

const result = steps
  .flatMap((step) => step.toolResults)
  .find((r) => r.toolName === 'getResearchResult')?.output as
  | NimbleAgentRunResultOutput
  | undefined;

if (result?.ready) {
  const { confidence, sources, claims } = result.output.trust;
  console.log(`${confidence} confidence, ${sources.length} sources, ${claims.length} claims`);
}
```

Register all three tools together when the model should also be able to check progress:

```ts theme={"system"}
tools: {
  startResearch: nimbleAgentStartRun({ effort: 'medium' }),
  checkResearch: nimbleAgentRunStatus(),
  getResearchResult: nimbleAgentRunResult(),
}
```

### Runs that are still working

A run that has not finished is a normal state, not an error. `nimbleAgentRunResult()` returns `ready: false` so the model can tell the user to check back.

```ts theme={"system"}
{
  ready: false,
  runId: 'task_run_8f3c...',
  agentId: 'wsa_01j9x8...',
  status: 'running',        // or 'queued'
  isActive: true,
  effort: 'medium',
  createdAt: '2026-07-31T10:00:00Z',
  startedAt: '2026-07-31T10:00:01Z',
}
```

Use `nimbleAgentRunStatus()` for cheap progress checks that never fetch the result. Its output adds `isActive`, `startedAt`, `completedAt`, and `error` on failed runs.

<AccordionGroup>
  <Accordion title="Optional bounded waiting">
    By default the result tool never blocks. Behind a queue worker, rather than a chat route, you can let a single call ride out a short remainder:

    ```ts theme={"system"}
    nimbleAgentRunResult({ wait: { timeoutMs: 120_000, pollIntervalMs: 2_000 } })
    ```

    Pass `wait: true` for the defaults: a `300_000` ms timeout and a `2_000` ms poll interval, with a `100` ms floor on the interval. On timeout the tool returns `ready: false` and the run stays healthy. The package never polls without a bound.
  </Accordion>

  <Accordion title="Abort handling">
    Waiting honors the AI SDK's per-call `AbortSignal`. Aborting stops the wait only. The run keeps going on Nimble's side and stays resumable from the same `runId`.
  </Accordion>

  <Accordion title="Failed and cancelled runs">
    A terminally failed or cancelled run throws a typed `NimbleAgentRunError`. Its `reason` is `'failed'`, `'cancelled'`, `'protocol'`, or `'request'`, and it always carries `runId` so your code can still reference the run. An HTTP status, when there is one, is on `error.status`.

    ```ts theme={"system"}
    import { NimbleAgentRunError } from '@nimble-way/ai-sdk';

    try {
      // ...
    } catch (error) {
      if (error instanceof NimbleAgentRunError) {
        console.error(error.reason, error.runId, error.status);
      }
      throw error;
    }
    ```

    A still-active run and a wait timeout are not errors.
  </Accordion>
</AccordionGroup>

### Results and citations

A completed run returns prose or structured data, along with `trust` metadata passed through verbatim from the API so citation markers stay aligned with the answer.

```ts theme={"system"}
{
  ready: true;
  runId: string;
  agentId: string;
  status: 'completed';
  effort: 'low' | 'medium' | 'high' | 'x-high' | 'max';
  createdAt: string;
  startedAt?: string;
  completedAt?: string;
  output:
    | { type: 'text'; text: string; trust: NimbleAgentTrust }
    | { type: 'json'; json: object | unknown[]; trust: NimbleAgentTrust };
}
```

`trust` carries the sources consulted, the per-claim citations, and confidence:

```ts theme={"system"}
{
  confidence: 'high' | 'medium' | 'low' | 'pre_existing';
  reasoning: string;
  sources: Array<{
    url: string;
    type: 'primary' | 'secondary';
    title?: string;
    source_category?: 'official' | 'news' | 'social' | 'academic' | 'aggregator' | 'other';
  }>;
  claims: Array<{
    callout?: number;   // text answers: the [1]-style marker in the prose
    path?: string;      // json answers: the JSON path of the value
    confidence: 'high' | 'medium' | 'low' | 'pre_existing';
    reasoning: string;
    citations: Array<{ url: string; title?: string; excerpts?: string[] }>;
  }>;
}
```

Text answers key each claim by `callout`, matching the numeric markers in the prose. Structured answers key each claim by `path`, the JSON path of the value. Exactly one of the two is present. See [Trust and citations](/nimble-sdk/web-search-agents/trust) for how confidence is graded.

### Agent run options

All three factories share this configuration. Every field is optional.

<AccordionGroup>
  <Accordion title="agentId">
    The Web Search Agent instance to run, in the form `wsa_...`. Defaults to `process.env.NIMBLE_AGENT_ID`. Resolved when the tool executes, so the model can never choose the agent.
  </Accordion>

  <Accordion title="apiKey">
    Nimble API credentials. Defaults to `process.env.NIMBLE_API_KEY`.
  </Accordion>

  <Accordion title="client">
    Injectable `NimbleAgentRunsClient` for testing. Optional.
  </Accordion>

  <Accordion title="clientOptions">
    Passthrough for `baseURL`, `fetch`, `timeout`, and `maxRetries` on the Nimble client. Optional.
  </Accordion>
</AccordionGroup>

`nimbleAgentStartRun()` adds two more:

<AccordionGroup>
  <Accordion title="effort">
    Effort used when the model does not choose one: `'low'`, `'medium'`, `'high'`, `'x-high'`, or `'max'`. Leave it unset to use the agent instance's own default. Higher tiers research more sources and take longer. See [Efforts](/nimble-sdk/web-search-agents/efforts).
  </Accordion>

  <Accordion title="effortCap">
    Upper bound on the effort the model may request. Model choices above the cap are clamped down to it. Default `'high'`, so a model cannot trigger the `x-high` or `max` cost tiers on its own. It never limits the developer-set `effort`.
  </Accordion>
</AccordionGroup>

`nimbleAgentRunResult()` adds `wait`, documented under [bounded waiting](#runs-that-are-still-working).

The model-facing inputs stay small: `{ task, effort? }` for the start tool and `{ runId }` for the status and result tools. The agent identity, credentials, and wait policy are never model-controlled.

## Limitations

* **Search, Extract, and Web Search Agent runs** ship today. Map and Crawl are planned follow-ups.
* **Agent runs need a pre-created agent instance.** Set `NIMBLE_AGENT_ID`. Creating and managing agents, and managing templates, are deliberately not model-callable tools in this release.
* **Run event streaming (SSE) is not exposed** by this release. Use the status and result tools.
* **No built-in answer generation in Search.** The tool returns results and the model writes the answer.
* **`searchDepth: 'fast'` is not available** in this package.
* **Node.js runtime** (18 or later) is the supported target. Edge and serverless compatibility is unverified.

## Resources

<CardGroup cols={2}>
  <Card title="npm Package" icon="npm" href="https://www.npmjs.com/package/@nimble-way/ai-sdk">
    `@nimble-way/ai-sdk` on npm.
  </Card>

  <Card title="GitHub Repository" icon="github" href="https://github.com/Nimbleway/ai-sdk">
    Source, README, and issues.
  </Card>

  <Card title="Web Search API" icon="magnifying-glass" href="/nimble-sdk/web-tools/search">
    Nimble's underlying search capability.
  </Card>

  <Card title="Extract API" icon="file-lines" href="/nimble-sdk/web-tools/extract/quickstart">
    Nimble's underlying page extraction capability.
  </Card>

  <Card title="Web Search Agent" icon="robot" href="/nimble-sdk/web-search-agents/overview">
    The deep-research capability behind the run tools.
  </Card>

  <Card title="Example Cookbook" icon="book" href="https://github.com/Nimbleway/cookbook">
    Runnable integration examples.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.