Skip to content
Part 6

The Specialist Team — Multi-Agent Orchestration

One Agent Isn't Enough

The Availability Agent is useful, but it has blind spots. It can tell you Dylan is available Thursday, but it doesn't know that Dylan has already worked five shifts this week and giving him a sixth would be unfair. It doesn't know that Marcus has had closing shifts three weeks running. It doesn't check whether the store actually has enough people on Saturday.

Each of these is a different kind of reasoning. And each one deserves its own specialist.

The Fairness Agent

Step 40: Create the fairness tools

Create lib/tools/fairness-tools.ts:

// lib/tools/fairness-tools.ts

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

export const getShiftDistributionTool = tool({
  description: 'Analyze how shifts are distributed across employees for a given week.',
  inputSchema: z.object({
    weekStart: z.string().describe('Week start date (Sunday) in YYYY-MM-DD format'),
  }),
  execute: async ({ weekStart }) => {
    const [employees, schedule] = await Promise.all([
      getAllEmployees(), getWeekSchedule(weekStart),
    ])

    const distribution: Record<string, {
      total: number; opening: number; mid: number; closing: number
    }> = {}

    employees.forEach(emp => {
      distribution[emp.name] = { total: 0, opening: 0, mid: 0, closing: 0 }
    })

    schedule.forEach(entry => {
      if (entry.employee_name && distribution[entry.employee_name]) {
        distribution[entry.employee_name].total++
        distribution[entry.employee_name][entry.shift_type]++
      }
    })

    return { weekStart, distribution }
  },
})

export const getMultiWeekHistoryTool = tool({
  description: 'Get shift history across multiple weeks for fairness analysis.',
  inputSchema: z.object({
    weeksBack: z.number().min(1).max(8)
      .describe('Number of past weeks to analyze (1-8)'),
  }),
  execute: async ({ weeksBack }) => {
    // ... aggregates totalShifts, closingShifts, saturdayShifts per employee
    // ... across the specified number of weeks
    // ... returns history + fairnessNotes (auto-detected imbalances)
  },
})
Fairness analysis output — shift distribution across employees, with Marcus highlighted as having 3 closing shifts and Dylan having 0
Step 41: Create the Fairness Agent

Create lib/agents/fairness-agent.ts:

// lib/agents/fairness-agent.ts

import { ToolLoopAgent, InferAgentUIMessage } from 'ai'
import { getShiftDistributionTool, getMultiWeekHistoryTool } from '../tools/fairness-tools'

export const fairnessAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are the Fairness Agent for Fabulosa Books.

Your job is to analyze whether shift assignments are equitable
across employees. Fabulosa Books cares deeply about treating
everyone fairly — this is a core value of the store.

Fairness principles:
- Total shifts should be roughly equal across regular staff
- No one should get stuck with closing shifts repeatedly — rotate them
- Saturday shifts (which run an extra hour) should be shared
- On-call staff should only be used when regular staff can't cover
- Alvin and Melissa should not be disproportionately burdened

When asked to evaluate fairness, always check the multi-week history —
a single week snapshot can be misleading. Look for patterns over 3-4 weeks.

Be specific: don't just say "distribute more evenly" — say "Give Dylan
a closing shift this week since Marcus has had closing three weeks in a row."`,
  tools: {
    getShiftDistribution: getShiftDistributionTool,
    getMultiWeekHistory: getMultiWeekHistoryTool,
  },
})

Notice how the instructions encode Fabulosa's values, not just rules. The agent knows that fairness matters to the store's identity.

The Coverage Agent

Step 42: Create the coverage tools

Create lib/tools/coverage-tools.ts:

// lib/tools/coverage-tools.ts

import { tool } from 'ai'
import { z } from 'zod'
import { getWeekSchedule } from '@/lib/db'

export const validateCoverageTool = tool({
  description: 'Validate whether a week\'s schedule meets all staffing requirements.',
  inputSchema: z.object({
    weekStart: z.string().describe('Week start date (Sunday) in YYYY-MM-DD format'),
  }),
  execute: async ({ weekStart }) => {
    const schedule = await getWeekSchedule(weekStart)

    const gaps: { day: number; shift: string }[] = []
    const issues: string[] = []

    for (let day = 0; day < 7; day++) {
      for (const shift of ['opening', 'mid', 'closing']) {
        const entry = schedule.find(
          s => s.day_of_week === day && s.shift_type === shift
        )
        if (!entry || !entry.employee_name) {
          gaps.push({ day, shift })
        }
      }
    }

    // Check for double-booking
    for (let day = 0; day < 7; day++) {
      const dayEntries = schedule.filter(s => s.day_of_week === day && s.employee_name)
      const names = dayEntries.map(s => s.employee_name)
      const duplicates = names.filter((name, i) => names.indexOf(name) !== i)
      if (duplicates.length > 0) {
        issues.push(`Double-booked on day ${day}: ${[...new Set(duplicates)].join(', ')}`)
      }
    }

    return {
      weekStart, isFullyCovered: gaps.length === 0,
      gaps, issues,
      totalAssigned: schedule.filter(s => s.employee_name).length,
      totalSlots: 21,
    }
  },
})
Step 43: Create the Coverage Agent
// lib/agents/coverage-agent.ts

import { ToolLoopAgent, InferAgentUIMessage } from 'ai'
import { validateCoverageTool } from '../tools/coverage-tools'

export const coverageAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are the Coverage Agent for Fabulosa Books.

Coverage rules:
- Every day must have exactly 3 shifts: opening, mid, closing
- Each shift must have exactly one employee assigned
- No employee should be double-booked (two shifts on the same day)
- A fully covered week = 21 assigned shifts (3 shifts x 7 days)

When reporting gaps, be specific about which day and shift is uncovered.
When reporting issues, explain the impact.`,
  tools: {
    validateCoverage: validateCoverageTool,
  },
})
Three specialist agents: Availability, Fairness, Coverage

The Coordinator: Orchestrating It All

This is the heart of multi-agent orchestration. The Schedule Coordinator is an agent whose tools are the other agents.

Step 44: Create the coordinator agent

Create lib/agents/coordinator-agent.ts:

// lib/agents/coordinator-agent.ts

import { ToolLoopAgent, InferAgentUIMessage, tool } from 'ai'
import { z } from 'zod'
import { availabilityAgent } from './availability-agent'
import { fairnessAgent } from './fairness-agent'
import { coverageAgent } from './coverage-agent'
import { updateSchedule } from '@/lib/db'

// Wrap each specialist as a tool the coordinator can call
const checkAvailabilitySubagent = tool({
  description: 'Ask the Availability Agent about employee availability.',
  inputSchema: z.object({
    question: z.string().describe('The availability question to answer'),
  }),
  execute: async ({ question }) => {
    const result = await availabilityAgent.generate({
      messages: [{ role: 'user', content: question }],
    })
    return { answer: result.text }
  },
})

const checkFairnessSubagent = tool({
  description: 'Ask the Fairness Agent to evaluate shift distribution equity.',
  inputSchema: z.object({
    question: z.string().describe('The fairness question to evaluate'),
  }),
  execute: async ({ question }) => {
    const result = await fairnessAgent.generate({
      messages: [{ role: 'user', content: question }],
    })
    return { answer: result.text }
  },
})

const validateCoverageSubagent = tool({
  description: 'Ask the Coverage Agent to validate staffing.',
  inputSchema: z.object({
    question: z.string().describe('The coverage question to validate'),
  }),
  execute: async ({ question }) => {
    const result = await coverageAgent.generate({
      messages: [{ role: 'user', content: question }],
    })
    return { answer: result.text }
  },
})

const assignShiftTool = tool({
  description: 'Assign an employee to a specific shift.',
  inputSchema: z.object({
    weekStart: z.string(),
    dayOfWeek: z.number().min(0).max(6),
    shiftType: z.enum(['opening', 'mid', 'closing']),
    employeeName: z.string(),
  }),
  execute: async ({ weekStart, dayOfWeek, shiftType, employeeName }) => {
    await updateSchedule(weekStart, dayOfWeek, shiftType, employeeName)
    return { success: true, assigned: `${employeeName} → ${shiftType} on day ${dayOfWeek}` }
  },
})

export const coordinatorAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are the Schedule Coordinator for Fabulosa Books.

You coordinate scheduling by delegating to three specialist agents:
1. **Availability Agent** — checks who is free and when
2. **Fairness Agent** — ensures equitable shift distribution
3. **Coverage Agent** — validates store staffing requirements

When handling scheduling requests:
1. First, check availability to understand the current state
2. Then, consult the Fairness Agent to understand distribution patterns
3. Make assignment decisions that balance availability, fairness, and coverage
4. After making assignments, validate coverage to ensure no gaps
5. Report the final result clearly

You can also assign shifts directly using the assignShift tool.

Important guidelines:
- Always consult at least the Availability Agent before making suggestions
- For full schedule creation, consult all three specialists
- Prefer regular staff over on-call staff
- Be transparent about trade-offs
- Present proposed schedules in a table format when possible`,
  tools: {
    checkAvailability: checkAvailabilitySubagent,
    checkFairness: checkFairnessSubagent,
    validateCoverage: validateCoverageSubagent,
    assignShift: assignShiftTool,
  },
})

This is the key pattern. The coordinator's tools are the other agents. When it calls checkAvailability, it's actually running the Availability Agent as a subagent—a full agent with its own tools and reasoning loop. The coordinator receives a text summary back, reasons about it, and decides what to do next.

The orchestrator pattern: how the coordinator delegates to specialists

Updating the Chat Route and UI

Step 45: Update the chat API route to use the coordinator
// app/api/chat/route.ts

import { createAgentUIStreamResponse } from 'ai'
import { coordinatorAgent } from '@/lib/agents/coordinator-agent'

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

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

One line changed: availabilityAgentcoordinatorAgent. The chat interface is the same, but now the coordinator delegates to all three specialists.

Step 46: Update ChatPanel to show agent activity

Update components/ChatPanel.tsx to handle the coordinator's tool calls. The key upgrade: the chat panel now shows which agent is working. When the coordinator delegates to the Fairness Agent, you see "Fairness Agent" in the response, with a loading indicator while it runs.

const AGENT_LABELS: Record<string, string> = {
  'tool-checkAvailability': 'Availability Agent',
  'tool-checkFairness': 'Fairness Agent',
  'tool-validateCoverage': 'Coverage Agent',
  'tool-assignShift': 'Assigning Shift',
}
Step 47: Connect schedule changes back to the grid

Update app/page.tsx so the schedule grid refreshes when the coordinator assigns shifts. A refreshKey state triggers a re-fetch of schedule data after each agent interaction completes.

Step 48: Test multi-agent orchestration
npm run dev

Try these prompts:

  • "Is this week's schedule fair?" — The coordinator calls the Availability Agent (to get the current schedule), then the Fairness Agent (to analyze distribution). You'll see both agents activate in the chat panel.
  • "Find someone to cover Tuesday closing" — The coordinator checks availability for Tuesday, checks fairness to pick the best candidate, and suggests an assignment.
  • "Schedule next week" — The full orchestration: availability → fairness → coverage → assignments.
Full-page screenshot: schedule grid on the left with shifts filled in by the coordinator, chat panel on the right showing a multi-step orchestration with all three agent indicators
Step 49: Push and deploy
git add -A
git commit -m "Add multi-agent orchestration: Fairness, Coverage, and Coordinator agents"
git push

What You've Built

Take a moment to appreciate what just happened. You've built a multi-agent system where:

  1. A Coordinator Agent receives natural-language scheduling requests
  2. It delegates to an Availability Agent that queries the database
  3. It consults a Fairness Agent that analyzes historical patterns
  4. It validates with a Coverage Agent that checks staffing rules
  5. It synthesizes the results and either makes recommendations or assigns shifts directly
  6. The UI shows the orchestration happening in real-time
  7. The schedule grid updates automatically when shifts are assigned

That's multi-agent orchestration. Not one monolithic AI trying to do everything, but a team of specialists coordinating through a leader—just like Alvin delegates to his team at the bookstore.

Complete multi-agent system architecture deployed on Vercel