ToolLoopAgent (stable since v6, replacing Experimental_Agent) encapsulates the model, tools, and loop behavior into a reusable unit.

import { ToolLoopAgent, tool, isStepCount } from 'ai';
import { z } from 'zod';

const agent = new ToolLoopAgent({
  model: 'your-model-id',
  instructions: 'You are an expert support engineer.',
  tools: {
    searchTickets: tool({
      description: 'Search support tickets',
      inputSchema: z.object({ query: z.string() }),
      contextSchema: z.object({ tenantId: z.string() }),
      execute: async ({ query }, { toolContext }) => {
        return searchTickets(query, toolContext.tenantId);
      },
    }),
  },
  stopWhen: isStepCount(10),
});

const result = await agent.generate({ prompt: 'Find ticket about refunds' });

Rules:
1. instructions is the v6/v7 name (was system). The default stopWhen is isStepCount(20); set it explicitly so the loop bound is visible.
2. runtimeContext is shared agent state flowing through the loop: available in prepareStep, lifecycle callbacks, and results. Use it for request ids, user roles, feature flags.
3. toolsContext carries server-side values individual tools need (tenant ids, credentials, scoped permissions). Declare them with the tool's contextSchema so they are typed, and pass toolsContext at generate/stream time. Never put secrets in the model-visible prompt; toolsContext is the channel for them.
4. prepareStep runs before each step and can adjust tools, instructions, and context per step. In v7, instructions returned from prepareStep carry forward to later steps (v6 applied them to one step only). If your logic depended on one-step overrides, rebuild from initialInstructions explicitly.
5. For UI streaming, agent.stream() plus toUIMessageStream gives the same wire format as streamText. The route handler barely changes.
6. Prefer ToolLoopAgent over hand-rolled while loops when the config is reused. Hand-rolled loops are fine for one-offs but scatter stopWhen and context handling.