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.