Vercel Logo

Tools and Multi-Step Agents

Streaming text is useful, but it's not enough. When a user says "alert me when Mammoth gets fresh powder," you need the AI to do something: parse that into a structured alert and save it. AI SDK tools give the model the ability to call functions with validated parameters.

Outcome

Add a create_alert tool to the chat endpoint so the AI can create structured ski alerts from natural language.

Fast Track

  1. Import CreateAlertToolInputSchema and wrap it with valibotSchema() for the tool's inputSchema
  2. Register the tool with tool() from the AI SDK
  3. Handle tool-result events in the SSE stream to notify the frontend

How Tools Work

User: "Alert me when Grand Targhee gets more than 6 inches of snow"
  │
  ▼
Claude sees the create_alert tool is available
  │
  ▼
Claude calls create_alert({ resortId: "grand-targhee", condition: { type: "snowfall", operator: "gt", value: 6, unit: "inches" }})
  │
  ▼
Tool execute() runs → returns result
  │
  ▼
Claude gets the tool result → writes a confirmation message
  │
  ▼
User sees: "I've created an alert for Grand Targhee. You'll be notified when snowfall exceeds 6 inches."

The stopWhen: stepCountIs(3) parameter controls how many tool-call/result rounds the model can do before finishing. With 3 steps, the model can call a tool, get the result, and then respond, or chain multiple tool calls.

Hands-on exercise 2.2

Let's extend the streaming chat endpoint from lesson 2.1 with tool support:

Requirements:

  1. Import CreateAlertToolInputSchema from $lib/schemas/alert and wrap it with valibotSchema() from @ai-sdk/valibot
  2. Register it as a create_alert tool using the AI SDK's tool() function with inputSchema
  3. Set stopWhen: stepCountIs(3) to allow the model to call the tool and respond
  4. Handle tool-result events in the stream and emit an alert_created SSE event when a tool succeeds
  5. Update the system prompt to instruct the model on how to parse natural language into alerts

Implementation hints:

  • The Valibot schema CreateAlertToolInputSchema already defines the three alert types (snowfall, temperature, conditions). Wrap it with valibotSchema() for the AI SDK
  • The tool's execute function should validate the resort exists and return structured data
  • The Chat.svelte component already handles alert_created events. It calls createAlert() to save to localStorage
  • The system prompt should explain how to map phrases like "fresh powder" to condition types

Try It

  1. Test natural language alert creation:

    Alert me when Mammoth gets fresh powder
    

    The AI should:

    • Call the create_alert tool with { resortId: "mammoth", condition: { type: "conditions", match: "powder" } }
    • Return a confirmation message explaining the alert
  2. Test a numeric condition:

    Notify me when Grand Targhee gets more than 6 inches of snow
    

    Expected tool call: { resortId: "grand-targhee", condition: { type: "snowfall", operator: "gt", value: 6, unit: "inches" } }

  3. Test a temperature condition:

    Let me know when Steamboat drops below 10°F
    

    Expected tool call: { resortId: "steamboat", condition: { type: "temperature", operator: "lt", value: 10, unit: "fahrenheit" } }

  4. Check the Alerts page: Navigate to /alerts. Your created alerts should appear there, saved to localStorage.

Commit

git add -A
git commit -m "feat(chat): add create_alert tool with multi-step agent"
git push

Done-When

  • AI can create alerts from natural language requests
  • Three condition types work: snowfall, temperature, conditions
  • Created alerts appear on the /alerts page
  • The AI responds with a confirmation after creating an alert
  • Invalid resort names are handled gracefully

Solution

src/routes/api/chat/+server.ts
import { createGateway, streamText, tool, stepCountIs } from 'ai';
import { valibotSchema } from '@ai-sdk/valibot';
import { resorts } from '$lib/data/resorts';
import { CreateAlertToolInputSchema } from '$lib/schemas/alert';
import { AI_GATEWAY_API_KEY } from '$env/static/private';
import type { RequestHandler } from './$types';
 
const gateway = createGateway({
  apiKey: AI_GATEWAY_API_KEY
});
 
export const POST: RequestHandler = async ({ request }) => {
  const { message } = await request.json();
 
  const resortList = resorts
    .map((r) => `- ${r.name} (id: ${r.id})`)
    .join('\n');
 
  const result = streamText({
    model: gateway('anthropic/claude-sonnet-4'),
    system: `You are a helpful ski conditions assistant. Users want to create alerts for ski resort conditions.
 
Available resorts:
${resortList}
 
When a user asks you to create an alert, use the create_alert tool with the appropriate parameters.
Parse natural language like "notify me when Mammoth gets fresh powder" into structured alert conditions.
 
For "fresh powder" or "new snow", use the conditions type with match: "powder".
For specific snow amounts like "more than 6 inches", use snowfall type with the appropriate operator.
For temperature conditions, use the temperature type.
 
Always confirm the alert was created and explain what conditions will trigger it.`,
    messages: [{ role: 'user', content: message }],
    tools: {
      create_alert: tool({
        description: 'Create a new alert for ski resort conditions',
        inputSchema: valibotSchema(CreateAlertToolInputSchema),
        execute: async ({ resortId, condition }) => {
          const resort = resorts.find((r) => r.id === resortId);
          if (!resort) {
            return {
              success: false,
              error: `Resort "${resortId}" not found`
            };
          }
 
          return {
            success: true,
            alert: {
              resortId,
              resortName: resort.name,
              condition,
              message: `Alert created for ${resort.name}`
            }
          };
        }
      })
    },
    stopWhen: stepCountIs(3)
  });
 
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for await (const part of result.fullStream) {
        if (part.type === 'text-delta') {
          controller.enqueue(
            encoder.encode(
              `data: ${JSON.stringify({ type: 'text', content: part.text })}\n\n`
            )
          );
        } else if (part.type === 'tool-result') {
          const toolResult = part.output as {
            success: boolean;
            alert?: unknown;
          };
          if (toolResult.success && toolResult.alert) {
            controller.enqueue(
              encoder.encode(
                `data: ${JSON.stringify({ type: 'alert_created', alert: toolResult.alert })}\n\n`
              )
            );
          }
        }
      }
      controller.enqueue(encoder.encode('data: [DONE]\n\n'));
      controller.close();
    }
  });
 
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    }
  });
};

What changed from lesson 2.1:

  1. Added imports: tool, stepCountIs alongside createGateway and streamText from ai, valibotSchema from @ai-sdk/valibot, CreateAlertToolInputSchema from the shared schemas
  2. Used valibotSchema() to wrap the existing Valibot schema for the AI SDK tool's inputSchema
  3. Registered the tool in streamText() with tools: { create_alert: tool({ ... }) }
  4. Added stopWhen: stepCountIs(3) so the model can call the tool, get the result, and write a response
  5. Handle tool-result events in the stream to emit alert_created SSE events
  6. Updated system prompt with instructions for parsing natural language into alert conditions

Troubleshooting

AI responds with text instead of calling the tool

Check your system prompt. The model needs explicit instructions about when to use create_alert vs. when to just answer a question. If the prompt doesn't mention the tool or describe when to use it, the model will default to text responses.

Valibot validation error in server logs

The model generated a condition that doesn't match the schema. The stopWhen: stepCountIs(3) gives it multiple rounds to self-correct, but if it keeps failing, make the tool description more specific about the expected condition shapes.

Advanced: Multiple Tools

You can register multiple tools. For example, adding a check_conditions tool that fetches live weather:

tools: {
  create_alert: tool({ /* ... */ }),
  check_conditions: tool({
    description: 'Check current conditions at a ski resort',
    inputSchema: valibotSchema(v.object({
      resortId: v.string()
    })),
    execute: async ({ resortId }) => {
      const resort = resorts.find((r) => r.id === resortId);
      if (!resort) return { error: 'Resort not found' };
      const weather = await fetchWeather(resort);
      return { resort: resort.name, ...weather };
    }
  })
}

With stopWhen: stepCountIs(3), the model could check conditions first, then create an alert based on what it finds.