Published actions
Pages register what can be done while they're mounted. One store feeds the command palette and the agent's client tools, so a person and the AI always have the same buttons.
The command palette, the AI’s client tools and the page’s own buttons tend to be three lists of the same things, kept in sync by hand. They drift. The AI offers an action the page no longer has, or the palette misses one the AI can do.
So pages publish their actions to one store, only while they’re mounted. Everything that wants to know “what can I do here?” reads that store.
interface Action<I> {
name: string;
title: string; // what a palette row says
description: string; // what the model reads
input: z.ZodType<I>;
annotations: { readOnly?: boolean; consequential?: boolean; payload?: boolean };
run(input: I, app: AppHandle): Promise<{ ok: true } | { ok: false; reason: string }>;
}
// in a page
useRegisterAction(EXPORT_CSV); // registered on mount, gone on unmount
┌──────────── ActionStore ─────────────┐
pages ───▶ │ actions (live, by name) │ ──▶ command palette rows
root ───▶ │ destinations (every route the nav has)│ ──▶ agent: client tools + catalog
│ screen (what's on it now) │ ──▶ payload cards
└───────────────────┬───────────────────┘
▼
execute(name, input): one at a time
Navigate is just an action
navigate is built in, and checks its target against the app’s published destinations before it goes:
async run({ destination, params, search }, app) {
if (!app.destinations.some((d) => d.id === destination))
return { ok: false, reason: `${destination} isn't a page you can open.` };
await app.go({ to: destination, params, search });
return { ok: true };
}
The model can only go where the nav can go. A made-up route comes back as a reason the model can read, not a broken page.
Rules
- Validate twice. The server checks a tool call against the declared schema, and
executechecks it again against the action’s own schema before running. - One at a time.
executequeues, so each action sees the URL the last one left. “Filter to critical, then sort by date” lands in order. - One way to the router. Every action reaches navigation through
go, so palette, buttons and AI all produce URL changes the back button can undo.
Why it works
The person and the AI see the same capabilities, because there’s only one list. A page that adds an action gets it in the palette and in the agent’s toolbag at once, with no extra wiring.