Build your first AI agent in 5 minutes. Start simple, add tools, structured output, and tests.
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.
An Anthropic API key set in your environment: export ANTHROPIC_API_KEY="sk-ant-..."
1
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.
2
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!
3
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); // 22console.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.
4
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 suitesetAllowModelRequests(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:
deno test agent_test.ts
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.