PAIPAI

Client And Threads

The @pai/client facade, native message state, thread, and run API.

Package: @pai/client

@pai/client exposes one application transcript: ThreadState.messages as PaiMessage[]. Runs, queued work, usage, and thread attributes are normalized beside those messages. There is no second render-item or run-container message grammar.

createAgentClient

function createAgentClient<TContract extends AgentContract = AgentContract>(
  transport: AgentClientTransport<TContract>,
): AgentClient<TContract>;

createAgentClient() builds the shared high-level facade over a transport. The agent contract types application message data, static tool inputs and outputs, client-readable metadata, commands, and client request data.

import { createHttpAgentTransport } from "@pai/client-http";
import { createAgentClient } from "@pai/client";

const assistant = createAgentClient<AssistantAgentContract>(
  createHttpAgentTransport({
    url: "http://localhost:3001/api/pai/main",
  }),
);

HTTP applications usually use createPaiHttpClient() from @pai/client-http when the server exposes a multi-agent PAI registry.

AgentClientTransport

type AgentClientTransport<TContract extends AgentContract = AgentContract> = {
  getManifest(): Promise<AgentManifest<TContract>>;
  listThreads(options?: ThreadListOptions): Promise<ThreadListResult>;
  renameThread(input: ThreadRenameInput): Promise<ThreadRenameResult>;
  deleteThread(input: { threadId: ThreadId }): Promise<void>;
  getThreadState(
    input: GetThreadStateInput,
  ): Promise<PaiThreadTransportState<TContract> | null>;
  send(input: ThreadSendInput<TContract>): Promise<ThreadSendResult<TContract>>;
  regenerate(
    input: ThreadRegenerateInput<TContract>,
  ): Promise<ThreadRegenerateResult<TContract>>;
  stopThread(input: StopThreadInput): Promise<ThreadMutationResult<TContract>>;
  stopRun(input: StopRunInput): Promise<ThreadMutationResult<TContract>>;
  respondToAction(
    input: RespondToActionInput<TContract>,
  ): Promise<RespondToActionResult<TContract>>;
  writeToolData(
    input: WriteToolDataInput<TContract>,
  ): Promise<WriteToolDataResult<TContract>>;
  recallQueuedItem(
    input: RecallQueuedItemInput,
  ): Promise<RecallQueuedItemResult<TContract>>;
  executeCommand<TName extends CommandName<TContract>>(
    input: ExecuteCommandInput<TContract, TName>,
  ): Promise<CommandOutput<TContract, TName>>;
  getRun(input: GetRunInput): Promise<RunStateDTO<TContract> | null>;
  uploadFile(input: FileUploadInput): Promise<UploadedFile>;
  createFileUrl(input: ClientCreateFileUrlInput): Promise<FileUrlResult | null>;
  readFile(input: ClientReadFileInput): Promise<StoredFileBody>;
  watchThread?(
    input: ThreadWatchTransportInput,
  ): AsyncIterable<PaiThreadTransportEvent<TContract>>;
  watchThreadHeads?(
    input: ThreadHeadWatchInput,
  ): AsyncIterable<ThreadHeadWatchEvent>;
  close?(): Promise<void>;
};

This is the lower-level delivery SPI for packages such as @pai/client-http and the direct runtime transport. Its native wire state and event types come from @pai/protocol/internal; they are inputs to PAI's private reducer, not an application observation API.

Transport generation events carry exact AI SDK UIMessageChunk values inside PAI's thread, run, message, version, and sequence envelope. The client folds those chunks with a long-lived native reducer, projects reserved PAI data onto the resulting parts, and exposes only ThreadState from watch().

If watchThread() is absent, the facade polls snapshots. If a native stream has a sequence gap, changes thread incarnation, or cannot be reduced safely, the facade repairs from an authoritative snapshot. Transport choice changes latency, not the public state shape.

Both transport watch inputs include an optional onConnectionEstablished() callback. Invoke it after the subscription is usable, even if the stream is still quiet. The client uses this boundary to report connection state and to reconcile delta-only thread-head watches.

AgentClient

type AgentClient<TContract extends AgentContract = AgentContract> = {
  getManifest(): Promise<AgentManifest<TContract>>;
  threads: ThreadCollectionClient<TContract>;
  files: {
    upload(input: FileUploadInput): Promise<UploadedFile>;
    url(input: ClientCreateFileUrlInput): Promise<FileUrlResult | null>;
    read(input: ClientReadFileInput): Promise<StoredFileBody>;
  };
  thread(
    threadId: ThreadId,
    options?: ThreadHandleOptions<TContract>,
  ): Thread<TContract>;
  newThreadId(): ThreadId;
  close?(): Promise<void>;
};

client.thread(threadId) creates a local handle synchronously. It does not assert that the durable thread already exists. The first accepted send can create it, and refresh() returns an empty unrealized state until then.

Use client.threads.find(threadId) when existence should be checked through I/O and a missing thread should return null.

ThreadCollectionClient

type ThreadCollectionClient<TContract extends AgentContract = AgentContract> = {
  list(options?: ThreadListOptions): Promise<ThreadListResult>;
  watchHeads?(
    options?: ThreadHeadsWatchOptions,
  ): AsyncIterable<ThreadHeadWatchEvent>;
  rename(threadId: ThreadId, title: string): Promise<ThreadRenameResult>;
  delete(threadId: ThreadId): Promise<void>;
  find(
    threadId: ThreadId,
    options?: ThreadHandleOptions<TContract>,
  ): Promise<Thread<TContract> | null>;
};

type ThreadListOptions = {
  parentThreadId?: ThreadId;
  category?: string;
  pageToken?: PageToken;
  limit?: number;
};

type ThreadListResult = {
  items: ThreadSummary[];
  nextPageToken?: PageToken;
};

Thread lists are scoped to the resolved runtime identity and agent. Trusted runtime clients can additionally list hidden threads and filter by agent.

Deletion removes the complete durable thread aggregate and queued work. Cancellation of already-started model, tool, or external-service work remains cooperative. Await deletion before deliberately reusing the same thread ID so thread-scoped cleanup can settle.

ThreadHandleOptions

type ThreadHandleOptions<
  TContract extends AgentContract = AgentContract,
> = {
  clientData?: JsonCompatible<TContract["clientData"]>;
  clientTools?: ClientToolDefinitionSnapshot[];
  requestContext?(): {
    clientData?: JsonCompatible<TContract["clientData"]>;
    clientTools?: ClientToolDefinitionSnapshot[];
  };
  /** Receives application-authored native data parts only. */
  onData?(part: DataUIPart<TContract["uiData"]>): void;
};

Static values apply to later sends, regeneration, actions, and ToolData writes. requestContext() is evaluated for each request and can provide current session data or mounted client-tool snapshots. An explicit request option wins over the dynamic and static values.

onData receives application data-${name} parts inferred from the agent's UI-data schemas. Framework-reserved data-pai-* parts are consumed by PAI and never reach this callback.

ThreadState

type ThreadState<TContract extends AgentContract = AgentContract> = {
  thread: PaiThreadHead<TContract>;
  messages: PaiMessage<TContract>[];
  runs: RunState<TContract>[];
  activeRunId: RunId | null;
  queue: ThreadQueueView<TContract>;
  usage: UsageSummary;
  capabilities: ThreadCapabilityView;
  error: ThreadErrorRecord | null;
};

type PaiThreadHead<TContract extends AgentContract = AgentContract> = {
  threadId: ThreadId;
  /** Null only before this thread ID has a persisted incarnation. */
  version: ThreadVersion | null;
  status: "idle" | "running" | "waiting";
  title?: ThreadTitleRecord;
  metadata: Partial<ThreadMetadataOf<TContract>>;
};

type ThreadCapabilityView = {
  canStop: boolean;
  canRespondToActions: boolean;
};

messages is the canonical render order. runs is a normalized control collection; group messages by message.metadata.pai.runId only when a UI wants turn-like containers. Queued inputs stay in queue.items until admission.

activeRunId identifies the run that currently owns execution. It is null while a run waits for external input even though that waiting run remains in state.runs.

thread.metadata is partial because an unrealized handle exists before server defaults and schema transforms have produced an authoritative snapshot. Server-only thread metadata is never projected here.

PaiMessage

type PaiMessage<TContract extends AgentContract = AgentContract> =
  | PaiCanonicalMessage<TContract>
  | PaiPendingInputMessage<TContract>;

PaiMessage is the only ordinary consumer message type. It retains the AI SDK UIMessage layout:

type PaiCanonicalMessageShape<TContract extends AgentContract> = {
  id: string;
  role: "system" | "user" | "assistant";
  metadata: MessageMetadataOf<TContract> & {
    pai: PaiMessageFacts;
  };
  parts: PaiMessagePart<TContract>[];
};

That metadata shape describes canonical server messages. An optimistic PaiPendingInputMessage<TContract> instead carries the sparse metadata supplied to send; schema defaults and transforms appear only when the canonical message with the same ID replaces it. Narrow the union with the exported guard:

if (isOptimisticInputMessage(message)) {
  // Declared application metadata fields may still be absent.
} else {
  // Canonical metadata is the complete schema output.
}

Application metadata remains at the top level. metadata.pai is reserved for PAI provenance and lifecycle facts:

type PaiMessageFacts =
  | {
      producer: "send" | "trigger";
      runId: RunId;
      relation: "input";
      status: "committed";
      createdAt: string;
      updatedAt: string;
    }
  | {
      producer: "model";
      runId: RunId;
      relation: "output";
      status: "open" | "committed";
      createdAt: string;
      updatedAt: string;
      usage?: UsageSummary;
      diagnostics?: MessageDiagnosticsRecord;
    }
  | {
      producer: "lifecycle";
      runId: RunId;
      relation: "context" | "output";
      status: "committed";
      createdAt: string;
      updatedAt: string;
    }
  | {
      producer: "admin";
      status: "committed";
      createdAt: string;
      updatedAt: string;
    };

type PendingPaiFactsShape = {
  /** Client-only optimistic input before send admission settles. */
  producer: "send";
  status: "pending";
  createdAt: string;
  updatedAt: string;
  runId?: never;
  relation?: never;
};

The actual exported union correlates producer, role, relation, and status more narrowly than this abbreviated overview. A client-only pending input has no run ID yet. The canonical server message with the same message ID reconciles it in place after admission.

PaiMessagePart

PaiMessagePart is the exported union of ordinary AI SDK UI-message parts, contract-typed application data parts, PAI-enriched static and dynamic tool parts, PaiReasoningPart, and PaiAttachmentPart. Standard AI SDK discriminants remain native: text, reasoning, source-url, source-document, file, dynamic-tool, tool-${name}, and application data-${name} parts. Use the exported guards where a pattern needs type narrowing:

import {
  isPaiAttachmentPart,
  isPaiDataPart,
  isPaiToolPart,
} from "@pai/client";

for (const message of state.messages) {
  for (const part of message.parts) {
    if (part.type === "text") renderText(part.text);
    else if (isPaiToolPart(part)) renderTool(message, part);
    else if (isPaiAttachmentPart(part)) renderAttachment(part);
    else if (isPaiDataPart(part)) renderApplicationData(part);
  }
}

PAI consumes its reserved data parts during projection. Their public effects are joined onto the native owner: ToolData and suspension episodes enrich a tool part, a generated summary enriches a reasoning part, and a managed file reference becomes PaiAttachmentPart.

Tool parts

Static tool parts use type: "tool-${name}"; dynamic provider or request-time tools use type: "dynamic-tool" plus toolName. Both keep the AI SDK's native state-discriminated fields:

switch (part.state) {
  case "input-streaming":
    renderPartialInput(part.input);
    break;
  case "input-available":
    renderInput(part.input);
    break;
  case "approval-requested":
  case "approval-responded":
    renderApproval(part.approval);
    break;
  case "output-available":
    renderOutput(part.output);
    break;
  case "output-error":
    renderError(part.errorText);
    break;
  case "output-denied":
    renderDenied(part.approval.reason);
    break;
}

PAI does not serialize custom running, waiting, or cancelled states into the native tool union. React derives those presentation states from the native part plus run and action sidecars.

For a contract-typed static tool named TName, the projected PAI fields are equivalent to:

type PaiStaticToolProjection<
  TContract extends AgentContract,
  TName extends ToolName<TContract> & string,
> = {
  data: PaiToolDataView<TContract, TName>;
  suspensions: readonly PaiSuspensionView<TContract, TName>[];
  pendingSuspension: Extract<
    PaiSuspensionView<TContract, TName>,
    { state: "pending" }
  > | null;
};

For a static tool, data, suspension input, and submitted resume payloads are typed from the agent contract. Dynamic tools expose the same fields with JSON-safe, contract-erased values. The native toolCallId is the invocation identity; PAI does not invent another universal tool-part identifier.

Reasoning and attachments

type PaiReasoningPart = ReasoningUIPart & {
  summary?: string;
};

type PaiAttachmentPart = {
  type: "attachment";
  id: string;
  fileId: string;
  mediaType: string;
  filename?: string;
  byteSize?: number;
  metadata?: JsonObject;
};

Reasoning and other standard content retain native provider metadata. An attachment is a provider-neutral snapshot of a file reference managed by PAI; request a view or download URL separately when rendering it.

RunState

type RunStateBase<TContract extends AgentContract = AgentContract> = {
  threadId: ThreadId;
  runId: RunId;
  /** Absent only when queued input was cancelled before admission. */
  inputMessageIds?: [MessageId, ...MessageId[]];
  initiator: "send" | "trigger" | "regenerate";
  admission:
    | { mode: "direct" }
    | { mode: "queue" | "steer"; queueItemId: QueueItemId };
  usage: UsageSummary;
  metadata: RunMetadataOf<TContract>;
  createdAt: string;
};

type RunLifecycleStatus =
  | "queued"
  | "running"
  | "waiting"
  | "completed"
  | "failed"
  | "cancelled";

The exported RunState is a status-discriminated union. running and waiting include startedAt; terminal states include completedAt; completed includes finishReason; failed includes a persisted error. Regenerated runs are always direct, while queue and steer admission apply only to send or trigger runs.

RunState is client-safe. metadata is typed from the agent's public run schema; private run metadata remains available only to lifecycle and trusted runtime code.

Thread

type Thread<TContract extends AgentContract = AgentContract> = {
  readonly threadId: ThreadId;
  readonly commands: ThreadCommands<TContract>;
  subscribeData(
    listener: (part: DataUIPart<TContract["uiData"]>) => void,
  ): () => void;
  run(runId: RunId): RunHandle<TContract>;
  refresh(): Promise<ThreadState<TContract>>;
  getState(): ThreadState<TContract>;
  rename(title: string): Promise<ThreadState<TContract>>;
  send(
    input: ClientSendInput<TContract>,
    options?: SendOptions<TContract>,
  ): Promise<RunHandle<TContract>>;
  regenerate(
    input: ClientThreadRegenerateInput<TContract>,
    options?: ThreadRegenerateOptions<TContract>,
  ): Promise<RunHandle<TContract>>;
  retry(
    options?: ThreadRegenerateOptions<TContract>,
  ): Promise<RunHandle<TContract>>;
  stop(input?: ThreadStopOptions): Promise<ActionResult<TContract>>;
  writeToolData(
    input: Omit<
      WriteToolDataInput<TContract>,
      "threadId" | "clientData" | "clientTools"
    >,
  ): Promise<ActionResult<TContract>>;
  recallQueuedItem(
    input: Omit<RecallQueuedItemInput, "threadId">,
  ): Promise<ActionResult<TContract>>;
  getPendingAction(
    message: PaiMessage<TContract>,
    part: PaiStaticToolPart<TContract> | PaiDynamicToolPart,
  ): PendingActionView<TContract> | null;
  watch(options?: {
    signal?: AbortSignal;
    intervalMs?: number;
  }): AsyncIterable<ThreadState<TContract>>;
};

refresh() fetches an authoritative snapshot. getState() returns the latest locally reduced public state. watch() yields that same shape and never exposes raw transport events.

subscribeData(listener) observes future application-authored native data-${name} parts on this thread handle and returns an unsubscribe function. Like ThreadHandleOptions.onData, it excludes reserved data-pai-* parts and uses the agent's closed UI-data registry for payload typing. Live reconnect and generation replay do not redeliver a data part already observed by the same thread handle; refreshing a retained snapshot does not invoke data listeners.

run(runId) creates an exact run handle without I/O. It does not claim that the run exists; run.getState() returns null for an unknown identity.

Sending And Regeneration

type ClientSendInput<
  TContract extends AgentContract = AgentContract,
> =
  | string
  | UserMessageInput<TContract>
  | {
      text?: string;
      attachments?: SendAttachmentInput[];
      metadata?: Exclude<UserMessageInput<TContract>["metadata"], undefined>;
      parts?: never;
    };

/** Illustrative private helper used by the signature below. */
type IsUnion<TValue, TWhole = TValue> = TValue extends TWhole
  ? [TWhole] extends [TValue]
    ? false
    : true
  : never;

type UserMessageInput<
  TContract extends AgentContract = AgentContract,
> = {
  parts: UserMessagePartInput[];
  metadata?: IsUnion<TContract> extends true
    ? never
    : MessageMetadataInput<MessageMetadataOf<TContract>>;
};

type UserMessagePartInput =
  | { type: "text"; text: string }
  | {
      type: "file";
      url: string;
      mediaType: string;
      filename?: string;
      providerMetadata?: ProviderMetadataRecord;
    }
  | { type: "attachment"; fileId: string };

thread.send("hello") is shorthand for one native text input. The client adds an optimistic pending message immediately and reconciles it using the final message ID. With a concrete agent contract, both structured and shorthand metadata inputs accept only declared fields and their input values. Declared fields are optional because server defaults or transforms may complete them. An erased AgentContract client remains open to arbitrary strict-JSON metadata. When a client is bound to an unresolved union of agent contracts, text and metadata-free structured inputs remain available, but any metadata requires narrowing the selected contract first. This conservative rule also covers keys shared by every branch because separate runtime agent and input values cannot preserve that correlation.

type SendOptions<TContract extends AgentContract = AgentContract> = {
  queue?: SendQueueOptions;
  clientData?: JsonCompatible<TContract["clientData"]>;
  clientTools?: ClientToolDefinitionSnapshot[];
  /** Final user-message identity; generated by the client when omitted. */
  messageId?: MessageId;
};

type ClientThreadRegenerateInput<
  TContract extends AgentContract = AgentContract,
> = {
  messageId: MessageId;
  replacement?: ClientSendInput<TContract>;
};

Regeneration rewinds from an admitted user message. run.regenerate() is the safer run-centered form because it preserves the complete ordered input set for a merged queued turn.

PendingActionView

Pending action commands are controller sidecars. They are not stored on the native tool part and are not a public collection on ThreadState.

const state = await run.waitUntilBlocked();

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 });
  }
}

getPendingAction() matches the exact message identity, tool-call identity, and current suspension identity. It returns null for a stale message or part.

type PendingActionView<TContract extends AgentContract = AgentContract> = {
  name: string;
  input: unknown;
  origin: {
    kind: "tool";
    threadId: ThreadId;
    toolName:
      | ToolName<TContract>
      | (string & Record<never, never>);
    suspendName?: string;
    messageId: MessageId;
    toolCallId: ToolCallId;
    runId: RunId;
  };
  ref: PendingActionRef<TContract>;
  submit(resume: unknown): Promise<ActionResult<TContract>>;
  cancel(input?: { reason?: string }): Promise<ActionResult<TContract>>;
  fail(error: unknown): Promise<ActionResult<TContract>>;
};

type PendingActionRef<TContract extends AgentContract = AgentContract> =
  PendingActionView<TContract>["origin"] & {
    /** Suspension data identity, or toolCallId for client/manual output. */
    actionId: string;
    name: string;
  };

The low-level client keeps action payloads as unknown at this transport-safe boundary. @pai/react narrows renderer action input and submit() from the tool contract.

For an external-output client tool, actionId equals toolCallId and the action name is ${toolName}.output. A named suspension uses its durable suspension data identity.

cancel() means the action cannot receive a valid domain response. If denial is valid domain data, submit a payload such as { approved: false } instead.

ActionResult

type ActionResult<TContract extends AgentContract = AgentContract> = {
  run?: RunHandleLike<TContract>;
  state: ThreadState<TContract>;
};

The optional run is present when the command created or resumed executable work. The state is the latest projected state after the mutation settles.

Stopping

type ThreadStopContinuation = "none" | "steer" | "all";

type ThreadStopOptions = {
  reason?: string;
  /** Defaults to "none". */
  continueWith?: ThreadStopContinuation;
};

type RunStopOptions = {
  reason?: string;
};

thread.stop() cancels active work and applies the selected policy to queued work. run.stop() targets only that exact running or waiting run and leaves sibling queued work unchanged. Cancellation is cooperative.

Queue

type ThreadQueueView<TContract extends AgentContract = AgentContract> = {
  items: QueuedItemView<TContract>[];
  canRecall: boolean;
};

type QueuedItemView<TContract extends AgentContract = AgentContract> = {
  queueItemId: QueueItemId;
  runId: RunId;
  mode: "queue" | "steer";
  messages: [PaiQueuedMessage<TContract>, ...PaiQueuedMessage<TContract>[]];
  createdAt: string;
  recall(): Promise<ActionResult<TContract>>;
};

Queued messages use the same projected native message shape as admitted input, but they do not enter state.messages until admission. Calling recall() removes the queued item and cancels its queued run without creating transcript history.

await thread.send("Handle this later", {
  queue: { mode: "queue", merge: "none" },
});

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

Omitting queue keeps the default busy-thread behavior: reject with ThreadBusyError.

UsageSummary

UsageSummary is shared by thread, run, and model-message step usage:

type UsageSummary = {
  inputTokens?: number;
  outputTokens?: number;
  totalTokens?: number;
  inputTokenDetails: {
    noCacheTokens?: number;
    cacheReadTokens?: number;
    cacheWriteTokens?: number;
  };
  outputTokenDetails: {
    textTokens?: number;
    reasoningTokens?: number;
  };
};

Step usage is available on model-produced message.metadata.pai.usage. Run usage is in state.runs; state.usage is the thread aggregate. Usage counts provider calls, not unique transcript tokens.

The package exports addUsage, emptyUsage, and hasUsage for safe aggregation and presence checks.

Files And Attachments

type FileUploadInput = {
  body: Uint8Array | Blob | ArrayBuffer | ReadableStream<Uint8Array>;
  mediaType: string;
  filename?: string;
  threadId?: ThreadId;
  byteSize?: number;
  metadata?: Record<string, unknown>;
};

type UploadedFile = {
  fileId: string;
  mediaType: string;
  filename?: string;
  byteSize?: number;
  metadata?: Record<string, unknown>;
};

type ClientCreateFileUrlInput = {
  fileId: string;
  intent?: "view" | "download";
  ttlSeconds?: number;
};

Upload first, then send the returned file reference as an attachment. Direct client.files.upload() is thread-agnostic unless threadId is supplied. Framework composers can fill that association automatically.

Attachment bytes remain in the configured file provider. The transcript keeps the immutable provider-neutral PaiAttachmentPart, and the runtime expands it into provider input only when assembling model context.

Runtime/Admin Extensions

Trusted runtime clients add server-only message, metadata, run, command, and trigger operations. Their message API returns LifecycleMessage, the same native-first canonical message plus visibility and private metadata:

type RuntimeThreadMessageClient<
  TContract extends AgentContract = AgentContract,
> = {
  getMany(messageIds: MessageId[]): Promise<LifecycleMessage<TContract>[]>;
  append(
    input: AppendAdminMessageInput<TContract>,
  ): Promise<LifecycleMessage<TContract>>;
  updateVisibility(
    messageIds: MessageId[],
    input: { visibility: LifecycleMessageVisibility },
  ): Promise<void>;
};

See the runtime reference for the complete trusted surface. Ordinary browser clients cannot append arbitrary messages, update visibility, or write private metadata.

Client Tool Snapshots

type ClientToolDefinitionSnapshot = {
  name: string;
  description: string;
  inputSchema: JsonValue;
  outputSchema: JsonValue;
  dataSchemas?: Record<string, JsonValue>;
};

Low-level clients can pass serialized frontend tool definitions through thread request context or send options. Executable functions and renderers never cross this boundary. React clients normally use provider/session clientTools or component-scoped useClientTool() registrations.

RunHandle

type RunHandle<TContract extends AgentContract = AgentContract> = {
  readonly runId: RunId;
  getState(): Promise<RunState<TContract> | null>;
  watch(options?: {
    stopOn?: "idle" | "blocked";
    signal?: AbortSignal;
    intervalMs?: number;
  }): AsyncIterable<ThreadState<TContract>>;
  waitUntilStatus(status: RunLifecycleStatus): Promise<RunState<TContract>>;
  waitUntil(status: RunLifecycleStatus): Promise<ThreadState<TContract>>;
  stop(input?: RunStopOptions): Promise<ActionResult<TContract>>;
  regenerate(
    options?: ThreadRegenerateOptions<TContract>,
  ): Promise<RunHandle<TContract>>;
  waitUntilBlocked(): Promise<ThreadState<TContract>>;
  waitUntilIdle(): Promise<ThreadState<TContract>>;
};

waitUntilStatus() returns the run record. The other wait helpers return the latest owning ThreadState, which is usually what rendering and action code needs.

run.watch() yields the same state as thread.watch() and stops at the chosen boundary. idle means completed, failed, or cancelled; blocked also includes waiting.

waitUntilBlocked() follows queued and running work until it waits or becomes terminal. waitUntilIdle() continues through waiting until the run becomes terminal.

On this page