Threads And Runs
A thread is one conversation. A run is one send.
A thread is one durable conversation. Messages, tool calls, files, and pending approvals live here.
A run is one turn of work on that thread — what you get back from thread.send().
import { createPaiHttpClient } from "@pai/client-http";
import type { AssistantPai } from "./http-server";
const paiClient = createPaiHttpClient<AssistantPai>({
url: "http://localhost:3001/api/pai",
headers: () => ({
"x-user-id": "dev-user",
"x-workspace-id": "dev",
}),
});
const assistant = paiClient.agent("main");
const thread = assistant.thread(assistant.newThreadId());
const run = await thread.send("Draft a report");
const state = await run.waitUntilIdle();
console.log(state.messages);newThreadId() is just an id. The first accepted send creates the stored thread.
Send
A string is enough:
await thread.send("Summarize this thread");Or parts, when you attach files:
await thread.send({
text: "Review this document.",
attachments: [uploaded],
});If the thread is already running, a second send throws ThreadBusyError. Wait until the thread is idle, or stop the run, then send again.
Wait, stop, refresh
await run.waitUntilIdle(); // completed, failed, or cancelled
await run.waitUntilBlocked(); // waiting for input, or terminal
await thread.stop(); // cancel the active run
await thread.refresh(); // pull the latest snapshotthread.getState() is the same shape refresh() and watch() return:
const state = thread.getState();
state.messages; // the transcript
state.runs; // runs and their current lifecycle status
state.activeRunId;Group messages by message.metadata.pai.runId if you want turn containers in the UI. That grouping is a view, not a second transcript.
Watch
for await (const state of thread.watch()) {
render(state.messages);
}React does this for you through ThreadProvider. You only need watch() in Node or a non-React client.
List threads
const page = await assistant.threads.list();
for (const head of page.items) {
console.log(head.threadId, head.title);
}In React, useThreads() is the same list, kept live.
Rename and delete
await assistant.threads.rename(thread.threadId, "Q3 report");
await assistant.threads.delete(thread.threadId);useThreads() exposes the same rename / delete. After delete, route the UI to a new newThreadId().