Commands
Let the UI call server work without starting a model turn.
A command is a typed server function on a thread. Use it when the UI should do something deterministic: rename, rate a response, open a workflow, reset a sandbox.
The model does not call commands. Tools are for the model. Commands are for your UI.
import { defineCommand } from "@pai/core";
import { z } from "zod";
import { ratings } from "../ratings";
export const rateResponse = defineCommand({
expose: "client",
input: z.object({
runId: z.string(),
rating: z.enum(["up", "down"]),
}),
run: async (ctx) => {
const identity = ctx.identity.identity as { userId: string };
await ratings.write({
threadId: ctx.thread.threadId,
runId: ctx.input.runId,
rating: ctx.input.rating,
userId: identity.userId,
});
return { ok: true };
},
});The command name is the key in the agent map, not a field on defineCommand:
defineAgent({
commands: { rateResponse },
});expose: "client" puts it on the public manifest so the browser can call it. Omit expose for server-only commands.
Then from React:
const { thread } = Assistant.useThread();
await thread.commands.rateResponse({ runId, rating: "up" });Commands do not receive runtimeContext. Close over your services (as ratings above), or declare capabilities when the command needs a sandbox.
ctx.identity.identity is typed as unknown. Narrow it against your agent identity schema. Tools never get ctx.identity at all — they use ctx.context.