PAIPAI

@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 express

createPaiExpressReceiver

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.

On this page