Migrations

Migrating Tools to Toolkits

Move makeAssistantTool, useAssistantTool, makeAssistantToolUI, and useAssistantToolUI registrations to the toolkit API.

The component and hook based tool APIs are deprecated:

  • makeAssistantTool
  • useAssistantTool
  • makeAssistantToolUI
  • useAssistantToolUI

Use a toolkit registered with Tools({ toolkit }) instead. Toolkits keep the model contract, browser execution, and tool-call rendering in one named map, which avoids duplicate registrations and makes the client/backend split explicit.

Before

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

const WeatherTool = makeAssistantTool({
  toolName: "get_weather",
  description: "Get the current weather for a city.",
  parameters: z.object({
    city: z.string(),
  }),
  execute: async ({ city }) => fetchWeather(city),
  render: ({ args, result }) => (
    <WeatherCard city={args.city} weather={result} />
  ),
});

export function App() {
  const runtime = useChatRuntime({ api: "/api/chat" });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <WeatherTool />
      <Thread />
    </AssistantRuntimeProvider>
  );
}

After

app/toolkit.tsx
"use generative";

import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";

export default defineToolkit({
  get_weather: {
    description: "Get the current weather for a city.",
    parameters: z.object({
      city: z.string(),
    }),
    execute: async ({ city }) => {
      "use client";
      return fetchWeather(city);
    },
    render: ({ args, result }) => (
      <WeatherCard city={args.city} weather={result} />
    ),
  },
});
app/MyRuntimeProvider.tsx
"use client";

import {
  AssistantRuntimeProvider,
  Tools,
  useAui,
} from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import toolkit from "./toolkit";

export function App() {
  const runtime = useChatRuntime({ api: "/api/chat" });
  const aui = useAui({
    tools: Tools({ toolkit }),
  });

  return (
    <AssistantRuntimeProvider aui={aui} runtime={runtime}>
      <Thread />
    </AssistantRuntimeProvider>
  );
}

Mechanical Steps

  1. Create a Toolkit object.
  2. Move each toolName into the toolkit key.
  3. Move description, parameters, execute, providerOptions, render, renderText, and display onto the toolkit entry.
  4. Register the toolkit once with useAui({ tools: Tools({ toolkit }) }).
  5. Remove <Tool />, <ToolUI />, useAssistantTool(...), and useAssistantToolUI(...) registrations.

UI-Only Tool Renderers

If you used makeAssistantToolUI or useAssistantToolUI for a backend, MCP, or LangGraph tool, move the renderer onto a backend toolkit entry:

// app/toolkit.tsx
"use generative";

import { defineToolkit } from "@assistant-ui/react";

export default defineToolkit({
  web_search: {
    type: "backend",
    render: ({ args, result }) => (
      <SearchResults query={args.query} results={result?.results ?? []} />
    ),
  },
});

Backend entries do not upload a schema or execute in the browser. They only attach UI for matching tool-call message parts.

For a one-off renderer that should only affect a particular message surface, use MessagePrimitive.Parts inline tool render overrides instead of a global registration.

Dynamic Tools

If a tool needs component state or props, keep the toolkit contract in a "use generative" file and use stubTool() for the executor. The component that owns the state supplies the real executor with useAuiToolOverrides(...):

useAuiToolOverrides is experimental and its API may change.

task-board-toolkit.tsx
"use generative";

import { defineToolkit, stubTool } from "@assistant-ui/react";
import { z } from "zod";

export type Task = { id: string; title: string };

export default defineToolkit({
  add_task: {
    description: "Add a task to the board.",
    parameters: z.object({ title: z.string() }),
    execute: stubTool(),
    renderText: {
      running: "Adding task",
      complete: "Task added",
    },
  },
});
TaskBoard.tsx
import { AuiProvider, Tools, useAui, useAuiToolOverrides } from "@assistant-ui/react";
import { useState, type Dispatch, type SetStateAction } from "react";
import type { Task } from "./task-board-toolkit";
import toolkit from "./task-board-toolkit";

function TaskBoard() {
  const [tasks, setTasks] = useState<Task[]>([]);

  const aui = useAui({
    tools: Tools({ toolkit }),
  });

  return (
    <AuiProvider value={aui}>
      <TaskBoardToolOverrides setTasks={setTasks} />
      <TaskList tasks={tasks} />
    </AuiProvider>
  );
}

function TaskBoardToolOverrides({
  setTasks,
}: {
  setTasks: Dispatch<SetStateAction<Task[]>>;
}) {
  useAuiToolOverrides({
    add_task: {
      execute: async ({ title }) => {
        setTasks((prev) => [
          ...prev,
          { id: crypto.randomUUID(), title },
        ]);
        return { ok: true };
      },
    },
  });
  return null;
}

This keeps the model-facing contract in the toolkit file while the component owns the stateful executor.

Generative Toolkits

For tools authored in a "use generative" file, export a toolkit with defineToolkit(...). The compiler splits backend, frontend, and human tools for you. This is the default shape to use for docs and copy-paste examples:

"use generative";

import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";

export default defineToolkit({
  create_chart: {
    description: "Create a chart for the user.",
    parameters: z.object({
      title: z.string(),
      values: z.array(z.number()),
    }),
    execute: async ({ title, values }) => {
      "use client";
      return renderChart(title, values);
    },
    render: ChartTool,
  },
});

Frontend and human tools produced by the generative compiler already have their schema defaults on the backend, so the client no longer re-uploads those schemas.