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 tsxSet your model provider credentials before running the server:
export OPENAI_API_KEY=...React apps also install:
pnpm add @pai/reactDefine 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:serverFor 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.