---
title: "Vercel AI SDK v5 Agent Patterns: stopWhen, prepareStep, and Loop Control"
description: "AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026."
canonical: https://callsphere.ai/blog/vw3g-vercel-ai-sdk-v5-agent-patterns-stopwhen-preparestep
category: "AI Engineering"
tags: ["Vercel", "AI SDK", "TypeScript", "Agents", "useChat"]
author: "CallSphere Team"
published: 2026-05-04T00:00:00.000Z
updated: 2026-05-07T09:59:38.292Z
---

# Vercel AI SDK v5 Agent Patterns: stopWhen, prepareStep, and Loop Control

> AI SDK 5 ships fully typed chat for React, Svelte, Vue, and Angular plus first-class agent loop primitives. Here are the patterns that matter for shipping in 2026.

> **TL;DR** — Vercel AI SDK 5 (released July 31, 2025) is the TypeScript-first answer to agent loops. `stopWhen` controls when a tool-calling loop ends; `prepareStep` mutates the next step's settings; the redesigned `useChat` hook ships modular transports for WebSockets and SSE. The result is the cleanest TS path from prototype to production agent in 2026.

## What's actually in AI SDK 5

```mermaid
flowchart TD
  Client[MCP client · Claude Desktop] --> MCP[MCP server]
  MCP --> Tool1[Tool: Calendar]
  MCP --> Tool2[Tool: CRM]
  MCP --> Tool3[Tool: KB search]
  Tool1 --> SaaS1[(Calendly)]
  Tool2 --> SaaS2[(Salesforce)]
  Tool3 --> SaaS3[(Notion)]
```

CallSphere reference architecture

The headline numbers from the v5 release:

- **Fully typed chat hook** for React, Svelte, Vue, and Angular — same primitives, framework-native bindings.
- **Tool improvements** — dynamic tools, provider-executed functions, lifecycle hooks, type-safety throughout the tool calling process.
- **Modular `useChat`** with three extensibility patterns: flexible transports (WebSockets, SSE, custom), client-only support, and end-to-end typed message envelopes.
- **Agentic control** via `stopWhen` and `prepareStep`.
- **Speech and audio APIs** — consistent typed interface across OpenAI, ElevenLabs, DeepGram.

## The agent loop, demystified

The classic SDK loop runs `generateText` (or `streamText`) with a tools object. The model emits a tool call, the SDK runs the tool, appends the result, runs the model again. Repeat until the model emits text without a tool call — or until you stop it.

`stopWhen` controls the stop condition. Built-ins:

- `stepCountIs(20)` — stop after 20 steps. Default safety.
- `hasToolCall("submit")` — stop when a specific tool is called.
- `isLoopFinished()` — never trigger; let the agent run to natural completion.

You can compose them: `stopWhen: [stepCountIs(20), hasToolCall("submit")]` — stop on whichever fires first.

`prepareStep` is the per-step config callback. It runs before each step and can:

- Swap the model (cheaper model for the first step, smarter for the second).
- Mutate the messages (truncate history, inject a reminder).
- Change the tool set (different tools available at different steps).

This is your hook for adaptive cost and quality control mid-loop.

## A real production pattern

```typescript
import { generateText, stepCountIs, hasToolCall } from "ai";

const result = await generateText({
  model: openai("gpt-5"),
  tools: { lookup, schedule, submit },
  messages,
  stopWhen: [stepCountIs(15), hasToolCall("submit")],
  prepareStep: async ({ stepNumber, messages }) => {
    if (stepNumber === 0) {
      return { model: openai("gpt-5-mini") };  // cheap first pass
    }
    if (messages.length > 30) {
      return { messages: messages.slice(-20) };  // truncate context
    }
    return {};
  },
});
```

The first step runs on a cheaper model to triage. Later steps escalate to the bigger model. If history grows beyond 30 messages, we truncate. The loop stops at 15 steps or when `submit` is called. This pattern can cut your agent's per-conversation cost by 40-60% with no quality drop on common inputs.

## useChat redesigned

The new `useChat` is **transport-agnostic**. The default is HTTP streaming. You can plug in:

- **WebSocket transport** — push tokens from a long-lived connection.
- **SSE transport** — server-sent events for one-way streaming.
- **Custom transport** — your own protocol (CallSphere's voice agent uses a custom WebRTC-aware transport).

Each transport ships with full typed message envelopes — no `any`, no string-typing.

## How CallSphere uses it

CallSphere's [public-facing chat widgets](/demo) are built on AI SDK 5. The `useChat` hook in our Next.js 15 App Router pages connects to a Node Edge runtime endpoint that runs the agent loop. `stopWhen` keeps the loop bounded; `prepareStep` swaps to a cheaper model for the first triage step.

For our **post-call summarization** workflow (every voice call ends with a structured summary written to Salesforce / HubSpot), we use `generateObject` from AI SDK 5 with a Zod schema. The structured output is type-safe end-to-end — the same Zod schema validates on the server and types on the client.

For voice we don't use AI SDK directly — OpenAI Realtime + WebRTC is its own beast — but the *non-voice* surfaces of CallSphere all run on AI SDK 5.

Pricing: [$149 Starter / $499 Growth / $1499 Scale](/pricing). [14-day trial](/trial). [22% affiliate](/affiliate).

## Build steps — your first AI SDK 5 agent

1. `npm install ai @ai-sdk/openai zod`.
2. Define tools with `tool({ description, parameters: z.object({...}), execute: async (...) => ... })`.
3. Call `generateText` (one-shot) or `streamText` (streaming) with your tools.
4. Add `stopWhen` to bound the loop.
5. Add `prepareStep` for per-step control.
6. On the client, use `useChat` to render the streaming response.
7. Wire OpenInference instrumentation for traces in Phoenix or LangSmith.

## Code: typed structured output

```typescript
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const CallSummary = z.object({
  disposition: z.enum(["qualified", "not_interested", "callback", "voicemail"]),
  next_action: z.string(),
  mentioned_competitors: z.array(z.string()),
  buying_signals: z.array(z.string()),
  follow_up_at: z.coerce.date().nullable(),
});

const { object } = await generateObject({
  model: openai("gpt-5"),
  schema: CallSummary,
  prompt: `Summarize: ${transcript}`,
});

await crm.upsertCall(object);  // fully typed
```

## Tool lifecycle hooks — the underused power feature

AI SDK 5 ships per-tool lifecycle hooks: `onInputAvailable`, `onInputStart`, `onInputDelta`. These let you stream UI updates as the model is *building* the tool call's arguments, before it actually calls the tool.

Practical use: when the model is mid-way through building a "schedule_meeting" tool call, your UI can show "scheduling..." with the partial arguments rendered as placeholders. The user sees progress instead of a blank pause. We use this in the chat widget for any tool that takes more than a couple of seconds.

```typescript
const schedule = tool({
  parameters: scheduleSchema,
  execute: scheduleMeeting,
  onInputStart: ({ toolCallId }) => ui.show(toolCallId, "Building meeting..."),
  onInputDelta: ({ toolCallId, inputTextDelta }) => ui.update(toolCallId, inputTextDelta),
  onInputAvailable: ({ toolCallId, input }) => ui.previewMeeting(toolCallId, input),
});
```

## Provider-executed tools — keeping the loop server-side

A new v5 capability: tools that run on the model provider's side (OpenAI's web search, code interpreter, file search) without round-tripping to your server. `webSearch()` from `@ai-sdk/openai` is the most popular — your agent gets web search results without you running a search backend.

For CallSphere this is mostly relevant in our research agents. We let OpenAI's web search hold the search loop while we focus on the synthesis prompt; saves us from operating Brave or SerpAPI integrations.

## Streaming UI patterns — partialOutput and beyond

`generateObject` and `streamObject` support `output: 'partial-object'` mode: the schema validates as fields stream in, and your UI gets typed partial objects to render incrementally. Good for forms, tables, structured output that benefits from "showing up as it's generated."

```typescript
const { partialObjectStream } = await streamObject({
  model: openai("gpt-5"),
  schema: CallSummary,
  output: "partial-object",
  prompt: `Summarize: ${transcript}`,
});

for await (const partial of partialObjectStream) {
  ui.render(partial);
}
```

## Speech and audio — the v5 sleeper feature

The new speech and audio APIs give you a single typed interface to OpenAI, ElevenLabs, and DeepGram for both TTS and STT. Switching providers is a config change, not a refactor. For non-realtime audio (voicemail transcription, post-call TTS summaries) this is the cleanest abstraction we've seen.

## FAQ

**AI SDK 5 vs OpenAI Agents SDK (TypeScript)?** AI SDK 5 is more ergonomic for chat UI and structured outputs; OpenAI Agents SDK is more opinionated for multi-agent topology. We use both — Agents SDK for orchestration, AI SDK for UI hooks.

**Does it support MCP?** Yes — `experimental_createMCPClient` lets you mount MCP servers as tool sources.

**What models work?** Anything in the AI SDK provider ecosystem — OpenAI, Anthropic, Google, Mistral, Bedrock, Azure, Ollama, OpenRouter, and dozens more.

**Is v6 out yet?** Vercel announced AI SDK 6 development in 2026; v5 is the production stable line as of May 2026.

**Where do I see this on CallSphere?** Book a [demo](/demo) and we'll walk through the chat widget code.

## Sources

- [AI SDK 5 launch post](https://vercel.com/blog/ai-sdk-5)
- [vercel/ai on GitHub](https://github.com/vercel/ai)
- [AI SDK Agents — Loop Control](https://ai-sdk.dev/docs/agents/loop-control)
- [How to build AI Agents with Vercel and the AI SDK](https://vercel.com/kb/guide/how-to-build-ai-agents-with-vercel-and-the-ai-sdk)

---

Source: https://callsphere.ai/blog/vw3g-vercel-ai-sdk-v5-agent-patterns-stopwhen-preparestep
