PAIPAI

Getting Started

Define an agent, serve it over HTTP, and send a message.

This guide creates the smallest useful PAI app inside an existing TypeScript project.

Install

pnpm add @pai/core @pai/client-http @pai/hono hono @hono/node-server ai @ai-sdk/openai zod
pnpm add -D tsx

Set your model provider credentials before running the server:

export OPENAI_API_KEY=...

React apps also install:

pnpm add @pai/react

Define The Agent

// server/assistant-agent.ts
import { openai } from "@ai-sdk/openai";
import { defineAgent } from "@pai/core";
import { z } from "zod";

export const assistantAgent = defineAgent({
  name: "report",
  model: openai("gpt-4.1-mini"),
  identity: z.object({ workspaceId: z.string() }),
  instructions: "Draft concise business reports.",
});

To use a personal subscription instead of a platform API key, install @pai/openai-codex for an eligible ChatGPT subscription or @pai/xai-grok for a Grok subscription, run that package's login (pnpm exec pai-openai-codex login or pnpm exec pai-xai-grok login), and create the model through an explicit auth manager. These experimental options are for local, single-user Node agents; see the @pai/openai-codex and @pai/xai-grok references for their security and support boundaries.

Start A Node HTTP Server

This example uses Hono on Node. Your React SPA can run separately, such as from Vite on http://localhost:5173, and call the agent server on http://localhost:3001.

// server/http-server.ts
import { serve } from "@hono/node-server";
import { createPai, type InferPaiContract } from "@pai/core";
import { createPaiHonoReceiver } from "@pai/hono";
import { Hono } from "hono";
import { cors } from "hono/cors";
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.use(
  "/api/*",
  cors({
    origin: "http://localhost:5173",
  }),
);

app.route(
  "/api/pai",
  createPaiHonoReceiver({
    pai,
    resolveIdentity: () => ({
      identity: { id: "dev-user", workspaceId: "dev" },
    }),
  }),
);

serve({
  fetch: app.fetch,
  port: 3001,
});

createPai({ agents }) creates the app runtime registry, and createPaiHonoReceiver({ pai, resolveIdentity }) mounts it over HTTP. The default storage is in-memory unless you provide a durable store. Keep the explicit resolveIdentity function even in development so production auth and scope decisions have one place to live.

Add scripts:

{
  "scripts": {
    "dev:server": "tsx server/http-server.ts"
  }
}

Start the server with:

pnpm dev:server

For Express and custom frameworks, see Framework Adapters.

Create A Client

// web/src/pai-client.ts
import { createPaiHttpClient } from "@pai/client-http";
import type { AssistantPai } from "../../server/http-server";

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

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

Send A Message

const thread = assistant.thread(assistant.newThreadId());
const run = await thread.send("Draft a one paragraph report for the team.");

const state = await run.waitUntilIdle();

console.log(state.messages);

Use The Client From React

React apps use the same typed client through @pai/react.

// web/src/pai-react.ts
import { createPaiReact } from "@pai/react";
import type { AssistantPai } from "../../server/http-server";

export const Pai = createPaiReact<AssistantPai>();
export const AssistantAI = Pai.agent("main");
// web/src/App.tsx
import { useState } from "react";
import { assistant, paiClient } from "./pai-client";
import { AssistantAI, Pai } from "./pai-react";

export function App() {
  const [threadId] = useState(() => assistant.newThreadId());

  return (
    <Pai.Provider client={paiClient}>
      <AssistantAI.ThreadProvider threadId={threadId}>
        <AssistantChat />
      </AssistantAI.ThreadProvider>
    </Pai.Provider>
  );
}

function AssistantChat() {
  const chat = AssistantAI.useChat();

  return (
    <section>
      <ol>
        {chat.messages.map((message) => (
          <li key={message.id}>
            {message.parts.map((part, index) =>
              part.type === "text" ? (
                <span key={index}>{part.text}</span>
              ) : null,
            )}
          </li>
        ))}
      </ol>

      <ComposerRow />
    </section>
  );
}

// The provider-scoped draft has its own subscription, so typing re-renders this
// row rather than the conversation above it.
function ComposerRow() {
  const composer = AssistantAI.useComposer();

  return (
    <form onSubmit={composer.submit}>
      <textarea
        value={composer.text}
        onChange={(event) => composer.setText(event.currentTarget.value)}
      />

      <button type="submit" disabled={!composer.canSubmit}>
        Send
      </button>
    </form>
  );
}

For thread persistence, message rendering, and richer UI state, continue to React.

On this page