Skip to content
Elements

Agent plan

A checklist the agent works through, with progress you can glance.

Composer draft0 of 5
  • Read existing composer state in progressTrace the current data flow before changing it.
  • Design the draft store not started
  • Wire runtime persistence not started
  • Add regression tests not started
  • Update the docs not started
fig. 01 ยท plays once, replay from the corner

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-agent-plan"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A checklist with a title, header count, accessible progress bar, and each step marked done, active, or still ahead. With a runtime the plan comes from a tool call the model drives; standalone you hold the step list and the active index yourself.

Getting started

A plan like this is a natural shape for a tool call: the model calls a tool with the full step list and the index of the step it is currently on.

Render the tool call

Register a toolkit entry whose render maps the call straight onto AgentPlan. The title, step list, and active index come from args, so the card needs nothing beyond what the model already sent.

app/toolkit.tsx
"use client";

import { defineToolkit } from "@assistant-ui/react";
import { AgentPlan } from "@/components/assistant-ui/elements/agent-plan";

export const toolkit = defineToolkit({
  update_plan: {
    type: "backend",
    render: ({ args }) => (
      <AgentPlan
        title={args.title}
        steps={args.steps}
        activeIndex={args.activeIndex}
      />
    ),
  },
});

The model calls update_plan again each time it checks a step off, so a plan reads down the transcript as a short sequence of cards rather than one card mutating in place. The schema and the tool that produces these calls live on your server; only the renderer is shown here.

Register the toolkit

app/MyRuntimeProvider.tsx
import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
import { toolkit } from "./toolkit";

const config = AuiConfig({ tools: Tools({ toolkit }) });

export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

Every update_plan call now renders as an AgentPlan card wherever it lands in the message.

Anatomy

<div data-slot="agent-plan">
  <div>
    <span>{/* title */}</span>
    <span>{/* n of m */}</span>
  </div>
  <div role="progressbar">{/* accessible progress */}</div>
  <ul>
    <li>
      <span>{/* check, spinner, or dot */}</span>
      <span>{/* step label, active description, and screen-reader status */}</span>
    </li>
  </ul>
</div>

activeIndex is clamped into 0โ€ฆsteps.length before anything is drawn from it, so an out-of-range value never breaks the layout: a value at or past steps.length marks every step done, and NaN falls back to 0. A step is done when its index is before the clamped active index or every step is already done, active when it sits exactly at that index, and otherwise still ahead. Object steps may add a stable id and a description; only the active step shows its description. Every row includes screen-reader status text, while the progress bar announces both percentage and the completed-step count. With an empty steps array the header reads 0 of 0, the bar sits at 0%, and the list renders nothing.

Examples

Add detail to the active step

Object steps use id for stable keys, label for the visible row, and an optional description beneath the active label. A custom title names both the heading and the progress bar for screen readers.

<AgentPlan
  title="Release checklist"
  steps={[
    { id: "review", label: "Review the release notes" },
    {
      id: "publish",
      label: "Publish the package",
      description: "Wait for the release job to finish.",
    },
    { id: "announce", label: "Share the release" },
  ]}
  activeIndex={1}
/>

Restarting a plan

A plan restarts with another update_plan call whose activeIndex is 0. There is no separate reset action.

render: ({ args }) => (
  <AgentPlan title={args.title} steps={args.steps} activeIndex={args.activeIndex} />
),

API reference

Render props

PropTypeDescription
args.titlestring | undefinedOptional title for the card and its progress bar.
args.steps(string | AgentPlanStep)[]The plan's steps, in order.
args.activeIndexnumberIndex of the step currently in progress.