Skip to content
Part 5

Your First Agent — The Availability Agent

From Clicking to Asking

Right now, filling the schedule means clicking twenty-one dropdowns. What if you could just ask the app a question instead?

"Who's available Thursday?"
"Is anyone free to cover the closing shift on Saturday?"
"Which on-call staff haven't worked this week?"

That's what an agent does. It takes a natural-language question, uses its tools to find the answer, and responds intelligently. We're going to build the Availability Agent—the first member of our specialist team.

Building Your First Tool

Step 33: Create the tools directory and the schedule query tool

Create lib/tools/schedule-tools.ts:

// lib/tools/schedule-tools.ts

import { tool } from 'ai'
import { z } from 'zod'
import { getAllEmployees, getWeekSchedule } from '@/lib/db'
import { getWeekStartDate } from '@/lib/utils'

export const getEmployeesTool = tool({
  description: 'Get all employees at Fabulosa Books with their role (regular or on-call)',
  inputSchema: z.object({}),
  execute: async () => {
    const employees = await getAllEmployees()
    return employees.map(emp => ({
      name: emp.name,
      type: emp.is_on_call ? 'on-call' : 'regular',
    }))
  },
})

export const getScheduleTool = tool({
  description: 'Get the current schedule for a specific week.',
  inputSchema: z.object({
    weekStart: z.string()
      .describe('The start date of the week (Sunday) in YYYY-MM-DD format.'),
  }),
  execute: async ({ weekStart }) => {
    const schedule = await getWeekSchedule(weekStart)
    return {
      weekStart,
      assignments: schedule.map(entry => ({
        day: entry.day_of_week,
        shift: entry.shift_type,
        employee: entry.employee_name,
      })),
      totalAssigned: schedule.filter(e => e.employee_name).length,
      totalSlots: 21,
    }
  },
})

export const checkAvailabilityTool = tool({
  description: 'Check which employees are available (not already scheduled) for a specific day.',
  inputSchema: z.object({
    weekStart: z.string().describe('Week start date (Sunday) in YYYY-MM-DD format'),
    dayOfWeek: z.number().min(0).max(6).describe('Day of week: 0=Sunday, ..., 6=Saturday'),
  }),
  execute: async ({ weekStart, dayOfWeek }) => {
    const [employees, schedule] = await Promise.all([
      getAllEmployees(),
      getWeekSchedule(weekStart),
    ])

    const assignedOnDay = schedule
      .filter(s => s.day_of_week === dayOfWeek && s.employee_name)
      .map(s => s.employee_name)

    const available = employees.filter(emp => !assignedOnDay.includes(emp.name))

    return {
      dayOfWeek, weekStart,
      available: available.map(emp => ({
        name: emp.name,
        type: emp.is_on_call ? 'on-call' : 'regular',
      })),
      alreadyScheduled: assignedOnDay,
    }
  },
})

Let's break down what's happening here:

  • getEmployeesTool — returns the full roster with regular/on-call designation
  • getScheduleTool — returns a week's assignments with a summary of how many slots are filled
  • checkAvailabilityTool — cross-references the roster against the schedule to find who's free on a given day

Each tool has a description (so the AI knows when to use it), an input schema (so the AI knows what to provide), and an execute function (your actual code).

Anatomy of an AI SDK tool: description, inputSchema, and execute

Building the Agent

Step 34: Create the Availability Agent

Create lib/agents/availability-agent.ts:

// lib/agents/availability-agent.ts

import { ToolLoopAgent, InferAgentUIMessage } from 'ai'
import { getEmployeesTool, getScheduleTool, checkAvailabilityTool } from '../tools/schedule-tools'

export const availabilityAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are the Availability Agent for Fabulosa Books,
an LGBTQIA+ bookstore in San Francisco's Castro neighborhood.

Your job is to answer questions about employee availability
and the current schedule.

The store has 8 employees:
- 6 regular staff: Alvin (owner), Melissa, Marcus, Dylan, Joel, Billy
- 2 on-call staff: Johnny Ray, Carly

The store operates 10am-8pm daily (10am-9pm Saturdays) with three shifts:
- Opening: 10am-4pm
- Mid: 12pm-6pm
- Closing: 2pm-8pm (2pm-9pm Saturdays)

Each shift is assigned to one employee. Use your tools to check the
actual schedule data before answering. Never guess or make up
availability — always query the database.

When reporting availability, distinguish between regular and on-call
staff. On-call staff should generally only be suggested when regular
staff aren't available.`,
  tools: {
    getEmployees: getEmployeesTool,
    getSchedule: getScheduleTool,
    checkAvailability: checkAvailabilityTool,
  },
})

export type AvailabilityAgentUIMessage = InferAgentUIMessage<typeof availabilityAgent>

Let's unpack the key parts:

  • ToolLoopAgent — The AI SDK's agent primitive. It wraps a model with instructions and tools, and manages the "think → use tool → observe → think again" loop automatically.
  • model — This uses the Vercel AI Gateway. The string anthropic/claude-sonnet-4.5 routes through the gateway to Anthropic's Claude.
  • instructions — The agent's identity and rules. It knows about Fabulosa, the team, the shifts.
  • tools — The capabilities the agent can use.
  • InferAgentUIMessage — Extracts the TypeScript type for UI messages, giving you type-safe tool results in React.

Wiring the Agent to an API Route

Step 35: Create the chat API route

Create app/api/chat/route.ts:

// app/api/chat/route.ts

import { createAgentUIStreamResponse } from 'ai'
import { availabilityAgent } from '@/lib/agents/availability-agent'

export async function POST(request: Request) {
  const { messages } = await request.json()

  return createAgentUIStreamResponse({
    agent: availabilityAgent,
    uiMessages: messages,
  })
}

That's it. Three lines of meaningful code. createAgentUIStreamResponse takes the agent and the conversation history, runs the agent's tool loop, and streams the response back to the browser in a format that useChat understands.

How the agent streams responses from server to browser

Adding Chat to the UI

Step 36: Create the ChatPanel component

Create components/ChatPanel.tsx:

// components/ChatPanel.tsx
'use client'

import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useState } from 'react'
import type { AvailabilityAgentUIMessage } from '@/lib/agents/availability-agent'

const transport = new DefaultChatTransport({ api: '/api/chat' })

export default function ChatPanel() {
  const [input, setInput] = useState('')
  const { messages, sendMessage, isLoading } = useChat<AvailabilityAgentUIMessage>({
    transport,
  })

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim()) return
    sendMessage({ text: input })
    setInput('')
  }

  return (
    <div className="bg-white rounded-lg border border-stone-200 shadow-sm">
      <div className="p-4 bg-stone-800 text-white rounded-t-lg">
        <h3 className="font-semibold">Schedule Assistant</h3>
      </div>

      <div className="h-96 overflow-y-auto p-4 space-y-4">
        {messages.map(message => (
          <div key={message.id} className={`flex ${
            message.role === 'user' ? 'justify-end' : 'justify-start'
          }`}>
            <div className={`max-w-[80%] rounded-lg p-3 text-sm ${
              message.role === 'user'
                ? 'bg-amber-500 text-white'
                : 'bg-stone-100 text-stone-800'
            }`}>
              {message.parts.map((part, i) => {
                switch (part.type) {
                  case 'text':
                    return <span key={i}>{part.text}</span>
                  case 'tool-getEmployees':
                  case 'tool-getSchedule':
                  case 'tool-checkAvailability':
                    return (
                      <div key={i} className="text-xs text-stone-400 italic my-1">
                        {part.state === 'output-available'
                          ? `✓ Checked ${part.type.replace('tool-', '')}`
                          : `⏳ Checking ${part.type.replace('tool-', '')}...`}
                      </div>
                    )
                  default:
                    return null
                }
              })}
            </div>
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit} className="p-4 border-t border-stone-200">
        <div className="flex gap-2">
          <input type="text" value={input}
            onChange={e => setInput(e.target.value)}
            placeholder="Ask about the schedule..."
            disabled={isLoading} />
          <button type="submit" disabled={isLoading || !input.trim()}>Ask</button>
        </div>
      </form>
    </div>
  )
}

Notice how the tool parts render: when the agent calls a tool, the UI shows "Checking..." while it runs, then "Checked" when it completes. The user can see the agent working—not just waiting for a final answer.

Chat panel showing agent interaction with tool activity indicators
Step 37: Add the ChatPanel to the main page

Update app/page.tsx to include the chat panel alongside the schedule grid:

// app/page.tsx
import ScheduleGrid from '@/components/ScheduleGrid'
import ChatPanel from '@/components/ChatPanel'

export default function Home() {
  return (
    <main className="min-h-screen bg-stone-50 p-8">
      <div className="max-w-7xl mx-auto">
        <header className="mb-8">
          <h1 className="text-3xl font-bold text-stone-800">Fabulosa Books</h1>
          <p className="text-stone-500 mt-1">Employee Schedule</p>
        </header>
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
          <div className="lg:col-span-2"><ScheduleGrid /></div>
          <div><ChatPanel /></div>
        </div>
      </div>
    </main>
  )
}
Step 38: Test your first agent
npm run dev

Open http://localhost:3000. You should see the schedule grid on the left and the chat panel on the right. Try these prompts:

  • "Who's available this Thursday?"
  • "Show me this week's schedule"
  • "Which on-call staff are available Saturday?"
Full-page screenshot showing the schedule grid (left, ~2/3 width) and the chat panel (right, ~1/3 width) side by side

The agent queries the database, reasons about the results, and responds in natural language. It's not guessing—it's checking real data with real tools.

Step 39: Push and deploy
git add -A
git commit -m "Add Availability Agent with chat interface"
git push

Vercel deploys automatically. In about a minute, your live app has an AI scheduling assistant.

What Just Happened

You built an AI agent. Not a chatbot that makes things up—an agent that:

  1. Receives a natural-language question
  2. Decides which tools to call
  3. Queries a real database
  4. Reasons about the results
  5. Responds with an accurate, helpful answer

Billy shows Alvin. "That's cool," Alvin says. "But it only tells me who's available. It doesn't know if the schedule is fair."

He's right. The Availability Agent answers "who can work." But it doesn't answer "who should work." For that, we need more agents.