In this tutorial you will build a weather agent from scratch. You will start with a bare agent, add a tool, add structured output, and write a test - all in one progressive flow. By the end, you will have a fully tested, type-safe agent.
Prerequisites
- Vibes installed (Installation guide)
- An Anthropic API key set in your environment:
export ANTHROPIC_API_KEY="sk-ant-..."
Create a bare agent
Start with the simplest possible agent: a model and a system prompt.import { Agent } from "@vibesjs/sdk";
import { anthropic } from "@ai-sdk/anthropic";
const agent = new Agent({
model: anthropic("claude-haiku-4-5-20251001"),
systemPrompt: "You are a helpful weather assistant.",
});
const result = await agent.run("What's the weather like?");
console.log(result.output);
// The agent responds with text (no tools yet, so it can only guess)
Agent takes a model and system prompt. agent.run() sends a message and returns a result. result.output is the text response.Without tools, the agent can only respond from its training data. Let’s fix that.
Add a tool
Give the agent real capabilities by defining a type-safe tool with Zod parameter validation.import { Agent, tool } from "@vibesjs/sdk";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({
city: z.string().describe("City name"),
}),
execute: async (_ctx, { city }) => {
// In production, call a real weather API
return `${city}: 22°C, sunny`;
},
});
const agent = new Agent({
model: anthropic("claude-haiku-4-5-20251001"),
systemPrompt: "You are a helpful weather assistant.",
tools: [getWeather],
});
const result = await agent.run("What's the weather in Tokyo?");
console.log(result.output);
// "The weather in Tokyo is currently 22°C and sunny."
tool() creates a type-safe tool with Zod parameter validation. The agent automatically calls tools when relevant. The _ctx parameter is the RunContext - we will use it later for dependency injection.The tool’s description helps the model decide when to call it. Be specific!
Add structured output
Force the agent to return typed data by providing an outputSchema.import { Agent, tool } from "@vibesjs/sdk";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({
city: z.string().describe("City name"),
}),
execute: async (_ctx, { city }) => {
return `${city}: 22°C, sunny`;
},
});
const WeatherReport = z.object({
city: z.string(),
temperature: z.number().describe("Temperature in Celsius"),
condition: z.string().describe("e.g. sunny, cloudy, rainy"),
summary: z.string().describe("A brief human-readable summary"),
});
const agent = new Agent({
model: anthropic("claude-haiku-4-5-20251001"),
systemPrompt: "You are a helpful weather assistant.",
tools: [getWeather],
outputSchema: WeatherReport,
});
const result = await agent.run("What's the weather in Tokyo?");
// result.output is typed as { city: string; temperature: number; condition: string; summary: string }
console.log(result.output.city); // "Tokyo"
console.log(result.output.temperature); // 22
console.log(result.output.condition); // "sunny"
outputSchema forces the agent to return data matching your Zod schema. result.output is now typed as { city: string; temperature: number; condition: string; summary: string } instead of string. The agent will retry if validation fails.Structured output uses tool-based extraction by default. The agent calls a special output tool internally.
Test your agent
Test the agent without making real API calls using TestModel.import { assertEquals } from "@std/assert";
import { Agent, tool, TestModel, setAllowModelRequests } from "@vibesjs/sdk";
import { z } from "zod";
import { anthropic } from "@ai-sdk/anthropic";
// Block accidental real API calls in your test suite
setAllowModelRequests(false);
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({
city: z.string().describe("City name"),
}),
execute: async (_ctx, { city }) => {
return `${city}: 22°C, sunny`;
},
});
const WeatherReport = z.object({
city: z.string(),
temperature: z.number().describe("Temperature in Celsius"),
condition: z.string().describe("e.g. sunny, cloudy, rainy"),
summary: z.string().describe("A brief human-readable summary"),
});
const agent = new Agent({
model: anthropic("claude-haiku-4-5-20251001"),
systemPrompt: "You are a helpful weather assistant.",
tools: [getWeather],
outputSchema: WeatherReport,
});
Deno.test("weather agent returns structured output", async () => {
const result = await agent
.override({ model: new TestModel() })
.run("What's the weather in Tokyo?");
// TestModel auto-calls tools and produces schema-valid output
assertEquals(typeof result.output.city, "string");
assertEquals(typeof result.output.temperature, "number");
});
Run the test:TestModel simulates model responses without API calls. agent.override() creates a copy of the agent with the test model - the original agent is unchanged (immutable). setAllowModelRequests(false) blocks any accidental real API calls in your test suite.
What’s next?
These concept deep-dives are coming soon.