@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
| Area | Representative exports |
|---|---|
| Identifiers | ThreadId, RunId, MessageId, QueueItemId, ThreadVersion |
| JSON | JsonValue, JsonObject, JsonCompatible, assertJsonValue, normalizeJsonValue |
| Agent contracts | AgentContract, ToolContract, ToolSpec, CommandContract, AgentManifest |
| Contract inference | ToolName, ToolInput, ToolOutput, ToolData, CommandInput, metadata helpers |
| Public messages | PaiMessage, PaiMessagePart, PaiCanonicalMessage, PaiPendingInputMessage |
| Message predicates | isPaiToolPart, isPaiDataPart, isPaiAttachmentPart, isOptimisticInputMessage |
| Tool projections | PaiStaticToolPart, PaiDynamicToolPart, PaiToolDataView, PaiSuspensionView |
| Public state | ThreadState, ThreadStateDTO, PaiThreadHead, RunState, QueuedItemDTO |
| Action sidecars | PendingActionRecord, PendingActionRef, PendingActionView, ActionResult |
| Commands | SendInput, SendQueueOptions, ThreadStopOptions, RunStopOptions |
| Errors | PaiErrorDetails, PersistedErrorRecord, PAI error classes and DTO helpers |
| Usage | UsageSummary, 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.datafor ToolData;part.suspensionsandpart.pendingSuspensionfor 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.