Elements

Elements · Agents

Handoff

Control passing between agents, with the reason and what came along.

TriageMaintainer

Triage reproduced the report and narrowed it to the converter, so the fix goes to the agent that can write and verify a patch.

carried overThe failing test and its outputThe two files the reader already narrowed to
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-agent-handoff"
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 handoff marks the moment control passes from one agent to another: which agent had it, which one has it now, why, and what context carried over. It is presentational throughout; you decide when a handoff happens and supply every field.

Getting started

Anatomy

<div data-slot="agent-handoff">
  <div>
    <span>{/* from pill */}</span>
    <svg>{/* arrow */}</svg>
    <span>{/* to pill */}</span>
  </div>
  <p>{reason}</p>
  <div>{/* one line per carried item, only when carried.length > 0 */}</div>
</div>

Before settled, the arrow and the to pill read in blue to mark the handoff as in transit, while the from pill stays at full opacity. Once settled, the from pill dims, the arrow turns gray, and the to pill switches to the neutral pill style at full opacity, since it is now the current speaker. Both transitions animate over 500ms. carried is required, not optional: pass an empty array to render no "carried over" section at all rather than an empty one.

Examples

Handoff with no carried context

<AgentHandoff from="Planner" to="Executor" reason="Plan is complete." carried={[]} settled />

Wiring settled to your own event

Nothing about the component listens for anything; setSettled(true) belongs wherever your app already knows the handoff finished, such as a websocket message or a state machine transition:

useEffect(() => {
  const unsubscribe = subscribeToAgentEvents((event) => {
    if (event.type === "agent_took_over" && event.agent === "Billing") {
      setSettled(true);
    }
  });
  return unsubscribe;
}, []);

Restyle the handoff

The from pill always uses the shared field surface; the to pill switches to field once settled and reads as a blue tint before that. The "carried over" label uses mono.

<AgentHandoff className="max-w-md gap-3" {...rest} />

API reference