PAIPAI

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.

server/commands/rate-response.ts
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.

Next

On this page