Robutler

host.commands

host.commands is how an app exposes a command surface that agents can drive. You register a named handler with host.commands.handle(name, fn); an agent (or another part of the platform) then invokes it. This is the mechanism behind agent-driven editing: the same code paths the app's own UI uses, reachable by name.

The command surface has two halves. host.commands is the runtime half (registering handlers in the app). The declaration half lives in your manifest's interface.commands, which is what agents read to discover what they can call. Keep the two in sync. See the agent command interface for the full picture and how agents invoke via workspace_widgets_invoke.

API

interface CommandHandlerCtx {
  sourceItemId: string | null;
  name: string;
  actor: CommandActor | null;                 // the calling agent's identity (agent-dispatched commands)
  progress(pct: number | null, note?: string): void; // live status for long handlers
}

interface CommandsNamespace {
  handle<A = unknown, R = unknown>(
    name: string,
    fn: (args: A, ctx: CommandHandlerCtx) => R | Promise<R>,
  ): () => void;          // returns an unregister function
  list(): string[];       // declared command names
}

Register a handler

await host.ready();

const off = host.commands.handle('goTo', ({ slug }: { slug: string }, ctx) => {
  navigate(slug);
  return { ok: true, slug };
});

// later, on teardown:
off();

The handler receives:

  • args: whatever the caller passed (shape it to match your manifest declaration).
  • ctx.sourceItemId: the workspace item id of the caller, or null.
  • ctx.name: the command name, handy when one function fields several commands.
  • ctx.actor: on agent-dispatched commands, the calling agent's identity ({ kind, id, name, avatar }); null otherwise. This is what drives the in-app agent presence (cursor, status pill, facepile badge).
  • ctx.progress(pct, note): report progress from a long-running handler (pct 0..1). Drives the live agent status — "exporting 43%" — in the presence layer. A no-op when there is no actor.

The return value (or resolved promise) is sent back to the caller as the command result. Throwing rejects the call with an error frame. Convention: return { id } / { ids } naming the objects a command created or edited — the collab-kit presence layer highlights those targets for everyone watching.

Streaming commands

For long-running work, return a runId quickly and emit progress events through host.emit. Declare the event names under the command's streams in your manifest so the calling agent subscribes ahead of invoking:

host.commands.handle('render', async ({ scene }) => {
  const runId = crypto.randomUUID();
  (async () => {
    for await (const frame of renderScene(scene)) {
      await host.emit({ type: 'render.progress', runId, frame });
    }
    await host.emit({ type: 'render.done', runId });
  })();
  return { runId };
});

Discoverability

host.commands.list() returns the command names currently registered. For an agent to discover the surface, declare each command (with description, args, returns, optional streams) in your widget.json interface.commands. Agents read that declaration through workspace_widgets_list, then call workspace_widgets_invoke(itemId, name, args).

Built-in commands

Every app answers a set of built-in commands for free, without registering anything: __describe, __getState, __screenshot, __record, and __capture. See the agent command interface for what each does.

On this page