PAIPAI

Runs

Track one execution of agent work.

Every accepted thread.send() creates a run and returns its RunHandle.

const run = await thread.send("Summarize this thread");

The run may start immediately or remain queued while another run owns the thread's execution lease.

Reading A Run

Runs are normalized beside the transcript:

const state = thread.getState();
const runState = state.runs.find((candidate) => candidate.runId === run.runId);

Messages retain their durable association in metadata.pai.runId:

const messages = state.messages.filter(
  (message) => message.metadata.pai.runId === run.runId,
);

This grouping is a derived view over the same PaiMessage objects, not a parallel transcript DTO.

Waiting

await run.waitUntilIdle();
await run.waitUntilBlocked();
const state = await run.waitUntil("waiting");

These helpers return the latest owning ThreadState. For the lower-level run record, use getState() or waitUntilStatus():

const current = await run.getState();
const completed = await run.waitUntilStatus("completed");

waitUntilBlocked() resolves when a run becomes waiting or terminal, which is useful for suspension, client-tool, and human-in-the-loop flows.

Watching

for await (const state of run.watch({ stopOn: "blocked" })) {
  render(state.messages, state.runs);
}

run.watch() yields the same ThreadState as thread.watch() and stops at the selected lifecycle boundary.

To locate an action after a run blocks, resolve it from its tool part:

const state = thread.getState();

for (const message of state.messages) {
  for (const part of message.parts) {
    if (!isPaiToolPart(part)) continue;
    const action = thread.getPendingAction(message, part);
    if (action) await action.submit({ approved: true });
  }
}

Usage

const record = await run.getState();
renderUsage(record?.usage);

Run usage aggregates every model step in one accepted unit of work. Thread totals remain on state.usage; step usage is on model message metadata.pai.usage.

Stopping

await run.stop();

Stopping is cooperative. It targets this exact running or waiting run, while leaving sibling queued work unchanged. Recall a queue item directly to cancel it before admission, or use thread.stop({ continueWith }) to choose what happens to all pending work.

Regenerating

const replacementRun = await run.regenerate();

Regeneration rewinds the owning turn and preserves its complete ordered input set. Use thread.regenerate({ messageId, replacement }) for the lower-level edit operation.

Queued Runs

const queued = await thread.send("Continue later", {
  queue: { mode: "queue" },
});

While queued, (await queued.getState())?.status is "queued" and its input stays in state.queue.items. Admission moves it into the transcript without recasting its native message parts.

On this page