PAIPAI

@pai/hono

Hono adapter for mounting a PAI app receiver.

@pai/hono mounts every agent in a createPai({ agents }) registry under a single Hono route.

pnpm add @pai/hono hono

createPaiHonoReceiver

function createPaiHonoReceiver<
  TPai extends PaiLike,
  THostContext = undefined,
  TEnv extends Env = Env,
>(
  input: CreatePaiHonoReceiverOptions<TPai, THostContext, TEnv>,
): PaiHonoReceiver<TEnv>;
import { createPai, type InferPaiContract } from "@pai/core";
import { createPaiHonoReceiver } from "@pai/hono";
import { Hono } from "hono";
import { assistantAgent } from "./assistant-agent";

export const pai = createPai({
  agents: {
    main: assistantAgent,
  },
  scopeKey: (identity) => identity.workspaceId,
});
export type AssistantPai = InferPaiContract<typeof pai>;

const app = new Hono();

app.route(
  "/api/pai",
  createPaiHonoReceiver({
    pai,
    resolveIdentity: async ({ request }) => ({
      identity: {
        userId: request.headers.get("x-user-id") ?? "anonymous",
        workspaceId: request.headers.get("x-workspace-id") ?? "default",
      },
    }),
  }),
);

If the receiver is mounted at /api/pai, the main agent is served under /api/pai/main. App clients should point at the app route:

const paiClient = createPaiHttpClient<AssistantPai>({
  url: "/api/pai",
});

const assistant = paiClient.agent("main");

Options

type CreatePaiHonoReceiverOptions<
  TPai extends PaiLike,
  THostContext = undefined,
  TEnv extends Env = Env,
> = {
  pai: TPai;
  resolveIdentity:
    | ResolveHttpAgentRequest<any, THostContext>
    | {
        [agentId: string]: ResolveHttpAgentRequest<any, THostContext>;
      };
  access?: Partial<Record<string, "full" | "read" | "none">>;
  getContext?: (context: Context<TEnv>) => THostContext | Promise<THostContext>;
  httpTelemetry?: HttpAgentTelemetryOptions;
  requestLimits?: HttpAgentRequestLimits;
  deriveFileUrlFallback?: boolean;
  mapHttpError?: HttpAgentErrorMapper;
  onHttpError?: false | HttpAgentErrorHandler;
};

Use one resolveIdentity function when all agents share the same auth and scope policy. Use the object form when each agent needs a different resolver.

createPaiHonoReceiver({
  pai,
  resolveIdentity: {
    support: resolveSupportIdentity,
    reviewer: resolveReviewerIdentity,
  },
  access: {
    reviewer: "read",
  },
});

access: "read" allows manifest, thread list, thread state, watch, run read, and file read routes while rejecting mutating operations. access: "none" skips mounting that agent.

mapHttpError converts recognized receiver exceptions into safe PaiErrorDetails. onHttpError receives the original trusted exception plus the same safe error and diagnostic reference sent to the client. Omit the reporter for PAI's default server console reporting, replace it with your logger, or pass false to suppress reporting.

See Error Handling for mapping, correlation, and redaction.

Lifecycle

createPaiHonoReceiver() returns a Hono app with close(). Call it from your server cleanup path when the receiver owns runtimes, storage, realtime, files, or telemetry resources through the createPai app.

const routes = createPaiHonoReceiver({ pai, resolveIdentity });

process.on("SIGTERM", () => {
  void routes.close();
});

On this page