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
- Import
CreateAlertToolInputSchemaand wrap it withvalibotSchema()for the tool'sinputSchema - Register the tool with
tool()from the AI SDK - Handle
tool-resultevents 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:
- Import
CreateAlertToolInputSchemafrom$lib/schemas/alertand wrap it withvalibotSchema()from@ai-sdk/valibot - Register it as a
create_alerttool using the AI SDK'stool()function withinputSchema - Set
stopWhen: stepCountIs(3)to allow the model to call the tool and respond - Handle
tool-resultevents in the stream and emit analert_createdSSE event when a tool succeeds - Update the system prompt to instruct the model on how to parse natural language into alerts
Implementation hints:
- The Valibot schema
CreateAlertToolInputSchemaalready defines the three alert types (snowfall, temperature, conditions). Wrap it withvalibotSchema()for the AI SDK - The tool's
executefunction should validate the resort exists and return structured data - The
Chat.sveltecomponent already handlesalert_createdevents. It callscreateAlert()to save to localStorage - The system prompt should explain how to map phrases like "fresh powder" to condition types
Try It
-
Test natural language alert creation:
Alert me when Mammoth gets fresh powderThe AI should:
- Call the
create_alerttool with{ resortId: "mammoth", condition: { type: "conditions", match: "powder" } } - Return a confirmation message explaining the alert
- Call the
-
Test a numeric condition:
Notify me when Grand Targhee gets more than 6 inches of snowExpected tool call:
{ resortId: "grand-targhee", condition: { type: "snowfall", operator: "gt", value: 6, unit: "inches" } } -
Test a temperature condition:
Let me know when Steamboat drops below 10°FExpected tool call:
{ resortId: "steamboat", condition: { type: "temperature", operator: "lt", value: 10, unit: "fahrenheit" } } -
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 pushDone-When
- AI can create alerts from natural language requests
- Three condition types work: snowfall, temperature, conditions
- Created alerts appear on the
/alertspage - The AI responds with a confirmation after creating an alert
- Invalid resort names are handled gracefully
Solution
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:
- Added imports:
tool,stepCountIsalongsidecreateGatewayandstreamTextfromai,valibotSchemafrom@ai-sdk/valibot,CreateAlertToolInputSchemafrom the shared schemas - Used
valibotSchema()to wrap the existing Valibot schema for the AI SDK tool'sinputSchema - Registered the tool in
streamText()withtools: { create_alert: tool({ ... }) } - Added
stopWhen: stepCountIs(3)so the model can call the tool, get the result, and write a response - Handle
tool-resultevents in the stream to emitalert_createdSSE events - Updated system prompt with instructions for parsing natural language into alert conditions
Troubleshooting
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.
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.
Was this helpful?