PAIPAI

Threads

The main client handle for messages, runs, actions, queueing, and files.

Thread is the normal interactive client API.

const thread = pai.thread(threadId);
const run = await thread.send("Draft a report");
await run.waitUntilIdle();

State

const state = await thread.refresh();

state.thread.status;
state.messages; // PaiMessage[]: the sole transcript
state.runs;
state.queue.items;

For a new local id with no server row, refresh() returns an empty unrealized state. The first accepted send creates the durable thread incarnation.

Watching

for await (const state of thread.watch()) {
  render(state.messages);
}

Watching and refreshing expose the same public shape. Native wire messages, reserved PAI data parts, and transport repair state remain private.

Sending

const run = await thread.send({
  parts: [{ type: "text", text: "Draft a report" }],
});

The client immediately adds one optimistic pending user message to state.messages. The canonical server message with the same id reconciles it without creating a second transcript entry.

If the thread is busy, choose an explicit queue policy:

await thread.send("Handle after current work", {
  queue: { mode: "queue" },
});

await thread.send("Correct the current direction", {
  queue: { mode: "steer" },
});

Queued messages stay in state.queue.items[].messages until admission.

Pending Actions

Actions are command sidecars resolved from the exact projected message and tool part. They are not stored on native tool parts or exposed as a second top-level action collection.

for (const message of thread.getState().messages) {
  for (const part of message.parts) {
    if (!isPaiToolPart(part)) continue;

    const action = thread.getPendingAction(message, part);
    if (action?.name === "requestApproval.approval") {
      await action.submit({ approved: true });
    }
  }
}

Sidecars also expose cancel({ reason }) and fail(error).

Stopping

await thread.stop();
await thread.stop({ continueWith: "steer" });

continueWith accepts "none" (the default), "steer", or "all". Retained work admits in normal priority order.

Runs And Metadata

const state = await run.waitUntilIdle();
const current = state.runs.find((candidate) => candidate.runId === run.runId);

renderRating(current?.metadata.rating);

Run and public thread metadata are server-written and contract-typed. Message metadata is available on message.metadata; PAI provenance is reserved under message.metadata.pai.

Deleting

await pai.threads.delete(threadId);

Deletion removes the durable thread, including messages, runs, and queued work. Cancellation of already-started external side effects remains cooperative, so tools should honor their AbortSignal and use idempotency where needed.

On this page