PAIPAI

@pai/protocol

Shared public contracts and native-first message projections.

Package: @pai/protocol

@pai/protocol is PAI's browser-safe common language for agent contracts, public thread state, native-first messages, run and queue state, errors, usage, and portable command options.

It deliberately does not define one universal transport envelope. Client method contracts belong to @pai/client; HTTP routes and SSE encoding belong to @pai/client-http and @pai/receiver-http.

Public Export Groups

AreaRepresentative exports
IdentifiersThreadId, RunId, MessageId, QueueItemId, ThreadVersion
JSONJsonValue, JsonObject, JsonCompatible, assertJsonValue, normalizeJsonValue
Agent contractsAgentContract, ToolContract, ToolSpec, CommandContract, AgentManifest
Contract inferenceToolName, ToolInput, ToolOutput, ToolData, CommandInput, metadata helpers
Public messagesPaiMessage, PaiMessagePart, PaiCanonicalMessage, PaiPendingInputMessage
Message predicatesisPaiToolPart, isPaiDataPart, isPaiAttachmentPart, isOptimisticInputMessage
Tool projectionsPaiStaticToolPart, PaiDynamicToolPart, PaiToolDataView, PaiSuspensionView
Public stateThreadState, ThreadStateDTO, PaiThreadHead, RunState, QueuedItemDTO
Action sidecarsPendingActionRecord, PendingActionRef, PendingActionView, ActionResult
CommandsSendInput, SendQueueOptions, ThreadStopOptions, RunStopOptions
ErrorsPaiErrorDetails, PersistedErrorRecord, PAI error classes and DTO helpers
UsageUsageSummary, addUsage, replaceUsage, hasUsage

The package root is the source of truth; this table highlights the main groups.

Public Messages

PaiMessage<TContract> is the only ordinary transcript message type. It retains the AI SDK UIMessage structure while adding PAI-owned consumer enrichments.

const state: ThreadState<MyAgentContract> = await thread.refresh();

for (const message of state.messages) {
  if (isOptimisticInputMessage(message)) {
    console.log(message.id, "awaiting admission");
  } else {
    console.log(message.id, message.metadata.pai.runId);
  }

  for (const part of message.parts) {
    if (part.type === "text") console.log(part.text);
    if (isPaiToolPart(part)) console.log(part.toolCallId, part.state);
  }
}

Standard SDK parts are preserved. PAI projects its reserved data parts into:

  • PaiAttachmentPart;
  • PaiReasoningPart.summary;
  • part.data for ToolData;
  • part.suspensions and part.pendingSuspension for named waits.

Raw storage envelopes and reserved data-pai-* parts are not root exports and are not a second consumer API.

Message Metadata

PAI facts use the reserved message.metadata.pai namespace:

message.metadata.pai.producer;
message.metadata.pai.relation;
message.metadata.pai.runId;
message.metadata.pai.status;
message.metadata.pai.createdAt;
message.metadata.pai.updatedAt;

Application message metadata remains beside pai and keeps its inferred contract type. Applications cannot author or overwrite the reserved namespace.

An optimistic PaiPendingInputMessage<TContract> contains the sparse metadata accepted from the send input, so declared keys remain optional until admission. The canonical replacement contains the schema-parsed MessageMetadataOf<TContract>, including defaults and transforms. Use isOptimisticInputMessage(message) to narrow between those two states; checking message.metadata.pai.status does not make TypeScript narrow the enclosing message union.

SendInput<TContract> applies the same correlation rule at authoring time. A concrete contract accepts its sparse declared metadata, and an erased AgentContract accepts arbitrary strict JSON. If TContract is an unresolved union, text and metadata-free structured inputs remain valid, but any metadata requires narrowing first. This conservative rule also covers metadata keys shared by every branch because the separate agent and input values cannot preserve that runtime correlation.

Thread State

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

Runs and queue items remain normalized. A run-aware UI groups the same message objects by message.metadata.pai.runId; there is no ThreadFeed alias or parallel transcript tree.

Pending Actions

PendingActionRecord is serializable control data. @pai/client decorates it as PendingActionView with submit, cancel, and fail. Applications resolve the sidecar from an exact message/tool pair through thread.getPendingAction().

The origin/ref use native toolCallId plus message, run, and action identities; PAI does not invent a universal part id.

Send, Queue, And Stop Options

type SendQueueOptions = {
  mode: "reject" | "queue" | "steer";
  merge?: "none" | "last";
};

type ThreadStopOptions = {
  reason?: string;
  continueWith?: "none" | "steer" | "all";
};

The enclosing transport method inputs are exported by @pai/client.

Internal Native Contracts

PAI packages share exact storage, wire, SDK chunk, and transport contracts through @pai/protocol/internal. That subpath is framework infrastructure, not an application escape hatch. It includes strict stored message envelopes, contract-aware wire messages, private transport state, and native generation events.

Storage-provider authors consume stored message contracts through the documented @pai/storage/thread-store boundary instead of importing protocol internals.

JSON And Error Safety

Persisted/wire message values must be canonical strict JSON. Contract schemas are parsed and then checked as JSON fixed points before persistence. Static tool success with exact undefined is normalized to native null.

Client-safe errors include only deliberate fields such as code, message, optional retry guidance, and an opaque diagnostic reference. Causes, stacks, and provider-native exception payloads are never projected.

On this page