PAIPAI

Client Tool Snapshots

Low-level client transport shape for frontend-defined tools.

Package: @pai/client

Most React apps should register stable client tools with Pai.agent(..., { clientTools }) config, and use useClientTool() for component-scoped handlers. The framework-agnostic client exposes the lower-level transport shape for clients that build their own registration and execution layer.

ClientToolDefinitionSnapshot

type ClientToolDefinitionSnapshot = {
  /** Public tool name advertised for this request. */
  name: string;

  /** Model-facing tool description. */
  description: string;

  /** JSON Schema for model-supplied tool input. */
  inputSchema: JsonValue;

  /** JSON Schema for client-submitted tool output. */
  outputSchema: JsonValue;

  /** Optional JSON Schemas for ToolData channels produced by this tool. */
  dataSchemas?: Record<string, JsonValue>;
};

Snapshots are strict-JSON definitions for tools owned by the current client. They are request-scoped: send them when starting or resuming work, and the runtime captures that request's snapshot for queued run context and action resume flows.

await thread.send("Read my selected text", {
  clientTools: [
    {
      name: "client.getSelectedText",
      description: "Read selected text from this browser tab",
      inputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {},
      },
      outputSchema: {
        type: "object",
        additionalProperties: false,
        properties: {
          text: { type: "string" },
        },
        required: ["text"],
      },
    },
  ],
});

The low-level client does not upload executable code. If the model calls one of these tools, the runtime exposes a native tool part in ThreadState.messages. A custom client can watch thread state, resolve the command sidecar with thread.getPendingAction(message, part), execute local code, and call action.submit(output).

React API

React wraps the snapshot and pending-action loop in agent-binding config:

export const AssistantAI = Pai.agent("main", {
  clientTools: {
    "client.getSelectedText": {
      description: "Read selected text from this browser tab",
      inputSchema: z.object({}),
      outputSchema: z.object({
        text: z.string(),
      }),
      execute: async () => ({
        text: window.getSelection()?.toString() ?? "",
      }),
    },
  },
});

<Pai.Provider client={paiClient}>
  <AssistantChat />
</Pai.Provider>

React serializes the Zod schemas to JSON Schema, advertises the latest snapshot on each run/action request while the provider is mounted, resolves the action sidecar for the native tool part, validates payloads with the same schemas, and submits results. The execute and render functions stay in the browser.

Server-Declared Client-Routed Tools

Server-declared client-routed tools are not included in clientTools. The agent already owns their name, description, and schemas through defineAgent({ tools }). A React client registers a local handler by string name:

AssistantAI.useClientTool("getSelectedText", {
  execute: async () => ({ text: window.getSelection()?.toString() ?? "" }),
});

When the model calls a server-declared tool without backend execute, the runtime creates the same native tool state and command sidecar. The local hook can answer it automatically, or any authorized UI can answer it manually with submit, cancel, or fail.

Safety

  • Client tool snapshots do not grant backend permissions.
  • Names cannot collide with backend tools.
  • Schemas must be JSON-serializable and model-compatible.
  • Submitted output is validated before it is persisted as tool output.
  • Definitions and outputs are treated as untrusted client-supplied model context.

On this page