@pai/express
Express adapter for mounting PAI app and agent HTTP receivers.
@pai/express adapts a PAI app or agent endpoint to Express.
pnpm add @pai/express expresscreatePaiExpressReceiver
function createPaiExpressReceiver<
TPai extends {
agents: Record<string, AgentRuntime>;
agent(id: string): AgentRuntime;
close?(): Promise<void>;
},
THostContext = undefined,
>(
input: CreatePaiExpressReceiverOptions<TPai, THostContext>,
): PaiExpressReceiver;import express from "express";
import { createPai } from "@pai/core";
import { createPaiExpressReceiver } from "@pai/express";
import { assistantAgent } from "./assistant-agent";
import { reviewerAgent } from "./reviewer-agent";
const pai = createPai({
agents: {
assistant: assistantAgent,
reviewer: reviewerAgent,
},
scopeKey: (identity) => identity.workspaceId,
});
const app = express();
app.use(
"/api/pai",
createPaiExpressReceiver({
pai,
getContext: (request) => ({
userId: request.get("x-user-id") ?? "anonymous",
workspaceId: request.get("x-workspace-id") ?? "default",
}),
resolveIdentity: {
assistant: async ({ context }) => ({
identity: { id: context.userId, workspaceId: context.workspaceId },
}),
reviewer: async ({ context }) => ({
identity: { id: context.userId, workspaceId: context.workspaceId },
}),
},
}),
);Each registered agent is mounted below the app route at /:agentId, so the
example above serves /api/pai/assistant/manifest and
/api/pai/reviewer/manifest. Use access to hide agents or expose an agent as
read-only:
createPaiExpressReceiver({
pai,
resolveIdentity,
access: {
assistant: "full",
reviewer: "read",
},
});CreatePaiExpressReceiverOptions also accepts:
{
mapHttpError?: HttpAgentErrorMapper;
onHttpError?: false | HttpAgentErrorHandler;
}mapHttpError supplies safe client details for recognized receiver failures.
onHttpError receives the original trusted exception plus the matching safe
error and diagnostic reference. Omit the reporter for PAI's default server
console reporting, replace it with your logger, or pass false to suppress
reporting.
The returned router has close(). Call it from your server shutdown path to
close mounted receivers and the underlying createPai app.
createExpressAgentHandler
function createExpressAgentHandler<
TContract extends AgentContract,
THostContext = undefined,
>(
input: CreateExpressAgentHandlerOptions<TContract, THostContext>,
): Router;
function createExpressAgentHandler<THostContext>(
receiver: HttpAgentReceiver<THostContext>,
options?: ExpressAgentHandlerAdapterOptions<THostContext>,
): Router;import express from "express";
import { createExpressAgentHandler } from "@pai/express";
import { createAgentRuntime } from "@pai/core";
import { assistantAgent } from "./assistant-agent";
import { storage } from "./storage";
const runtime = createAgentRuntime({
agent: assistantAgent,
storage,
scopeKey: (identity) => identity.workspaceId,
});
const app = express();
app.use(
"/api/agent",
createExpressAgentHandler({
runtime,
getContext: (request) => ({
userId: request.get("x-user-id") ?? "anonymous",
workspaceId: request.get("x-workspace-id") ?? "default",
}),
resolveIdentity: async ({ context }) => ({
identity: { id: context.userId, workspaceId: context.workspaceId },
}),
}),
);Pass an existing HttpAgentReceiver when another package already owns receiver
creation, or when you need to mount one agent explicitly:
app.use("/api/agent", createExpressAgentHandler(receiver));If the receiver expects host context, provide the same adapter hook with the receiver form:
app.use(
"/api/agent",
createExpressAgentHandler(receiver, {
getContext: (request) => ({
userId: request.get("x-user-id") ?? "anonymous",
workspaceId: request.get("x-workspace-id") ?? "default",
}),
}),
);The adapter converts Express requests and responses. Agent execution, storage,
access checks, snapshots, and watch streams remain in @pai/core and the
framework-neutral HTTP receiver.
See Error Handling for mapping, correlation, and redaction.