Skip to main content
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-..."
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); // 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.
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 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:
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.

What’s next?

These concept deep-dives are coming soon.