Skip to content

Lend a tool

A tool is a capability beyond pressing an action: reading or changing what the page shows, or going elsewhere. It runs in the browser. Lend one with useAssistedTool(name, { description, parameters, execute }).

Call useAssistedTool in a component rendered under the AssistedPage. The tool belongs to the page: the page’s note lists it, and it leaves with the page. From examples/app/app/orders/page.tsx:

useAssistedTool("filter_orders", {
description: "Show only the orders with one status, or all of them.",
parameters: {
type: "object",
properties: { status: { type: "string", enum: filters } },
required: ["status"],
},
execute: ({ status }: { status: Filter }) => {
setFilter(status);
return `Showing ${status === "all" ? "all orders" : `the orders that are ${status}`}.`;
},
});

The tool’s hook must render under the AssistedPage, while content needs the same state. So a page with tools is two components: the state and the AssistedPage above, the tools below. From examples/app/app/orders/page.tsx:

export default function Orders() {
const shop = useShop();
const [filter, setFilter] = useState<Filter>("all");
const money = moneyOf(shop.settings.currency);
const rows = shop.orders
.filter((order) => filter === "all" || order.status === filter)
.map((order) => rowOf(order, money));
return (
<AssistedPage
name="Orders"
instructions="Every order of the shop, as filtered by status with filter_orders. Ship or cancel an order that is waiting to ship by its id. New order opens a dialog that introduces itself. Each order has a page of its own with its note and refund."
content={() => ({ filter, orders: rows })}
>
<OrdersTable filter={filter} setFilter={setFilter} rows={rows} />
</AssistedPage>
);
}

OrdersTable lends filter_orders, shown above.

Call useAssistedTool outside any AssistedPage, under AssistedProvider. The tool is global: it works from every page and no note lists it. From examples/app/app/shell.tsx:

useAssistedTool("get_shop_summary", {
description: "How many orders and customers the shop has. Works from any page.",
parameters: { type: "object", properties: {} },
execute: () => ({ orders: shop.orders.length, customers: shop.customers.length }),
});

parameters takes a JSON Schema object or a Standard Schema such as a zod schema. With JSON Schema, type the arguments of execute yourself, as above. With zod, they are inferred:

const refund = z.object({ id: z.string().describe("The order's id.") });
export function RefundTool({ refundOrder }: { refundOrder: (id: string) => Promise<void> }) {
useAssistedTool("refund_order", {
description: "Refund an order.",
parameters: refund,
execute: async ({ id }) => {
await refundOrder(id);
return `Refunded ${id}.`;
},
});
return null;
}

Define the schema at module scope.

The tool is lent again when its description or its parameters’ JSON Schema changes. execute always runs the latest closure.

Throw from execute when the tool cannot do its job. The model gets { error: "<name> failed: <message>" } and the turn goes on. Arguments the schema rejects answer with an { error } too.

render takes a tool UI component for the tool’s calls in the thread. Define it at module scope, so the same component is passed on every render.

To ask the user before the tool runs, see Make an action consequential.