@personaai/sdk is the official Node.js/TypeScript client for everything on this page — auth
header injection, typed resource methods for Agents/Skills/Knowledge/MCP/Providers/Threads/Files,
and a streaming chat client, all wrapping the same REST + AG-UI API described in the
API Reference. If you’re integrating from Node.js, start here instead of calling
fetch yourself. Every method on every resource client is a thin, typed 1:1 wrapper over a real
endpoint — there’s no hidden behavior, so this page doubles as a complete method-by-method
reference of the client surface.
Install
Public npm package coming soon.
@personaai/sdk isn’t published to the
npm registry yet — npm install @personaai/sdk won’t resolve until it is.
Until then, build it locally from source:npm install at the folder directly:
fetch/FormData/ReadableStream — no heavy HTTP client
dependency). The package ships both ESM and CommonJS builds plus .d.ts types, so import and
require both work.
Configuration
Every entry point takes the same options shape (PersonaClientOptions, re-exported as
HttpClientOptions):
Constructing a client is cheap and stateless beyond these options — there’s no connection pool or
warm-up step, so it’s fine to construct a fresh, per-request client scoped to whichever user is
making the request (see Configuration patterns per framework) rather than
trying to share a single instance across users.
Quickstart
Acting on behalf of one of your own end users
Construct a second client scoped to whoever is actually using your product right now — after your own auth has confirmed who that is. This maps directly onto the control-plane vs. runtime-plane distinction from the Integration Guide.Authentication & principal context
persona.whoami() resolves the principal context your credential (and optional
externalUserId) maps to — a side-effect-free way to sanity-check auth wiring before making real
calls:
externalUserId:
There is no third
ProjectAdmin variant reachable from the SDK — that context only comes from a
Clerk-authenticated human admin session (Developer Studio), a completely different auth model the
SDK deliberately doesn’t implement (see Out of scope).
Error handling
Every non-2xx response, or a{success:false} JSON envelope on a 2xx, throws a typed error instead
of resolving. All error classes are exported from the package root:
code mirrors the backend’s own machine-readable error code (e.g. VALIDATION_ERROR,
UNAUTHORIZED, PROJECT_NOT_ACTIVE, NOT_FOUND) — check the API Reference for
each endpoint’s documented error responses. message is the human-readable string from the same
envelope, safe to log or (after your own review) surface to a user.
Retries and rate limits
Every resource call goes through the sharedHttpClient, which automatically retries 429 Too Many Requests responses: it reads the response’s Retry-After header (seconds), waits that long, and
retries — up to maxRetries times (default 2) before giving up and throwing a PersonaApiError
with statusCode: 429. If Retry-After is absent it falls back to a 1-second wait. This is a fixed
per-attempt wait, not exponential backoff, matching the API’s own Retry-After contract rather than
guessing at a backoff curve.
400, 401, 403, 404, 500, etc.)
throws immediately on the first attempt, since
retrying those wouldn’t change the outcome.
Cancelling a request
Every method that accepts anoptions object with a signal field (currently chat.stream() /
chat.sendMessage()) supports a standard AbortSignal for cancellation:
Resources
Two real backend quirks show up across the typed responses below, documented rather than papered
over:
_idvs.id. Skill/Agent/Knowledge/Thread/Mcp responses use the raw MongoDB document shape (_id). Provider/File responses go through a clean formatted DTO (id). Check each type below for which one applies.- Bare arrays, not a pagination envelope. Every
list()/discover()-style method (skills.list(),agents.list(),knowledge.list(),mcps.list(),threads.list(),files.list()) returns a plain array, not{items, pagination}— despite acceptingpage/limitquery params. Fetch a page, check the returned array’s length againstlimitto infer whether there’s more.
Providers
Control-plane only — there is no list/discover endpoint for Providers, and noExternalUser
ownership concept (an end user can never own a Provider).
CreateProviderInput/UpdateProviderInput fields: label, baseURL, apiKey (create only,
required), defaultModel, isDefault?. apiKey is write-only — it’s never present on the
Provider object returned from any call.
Skills
DiscoverSkillsParams: page?, limit?, search?, scope?: 'mine' (restricts to the asserted
end user’s own Skills — only meaningful on a ProjectRuntimeContext client). Skill.isOwner is
present only on the result of get(), not list()/create()/update().
Agents
Notes on the
Agent shape:
visibility:'private' | 'unlisted' | 'public'.category:'productivity' | 'coding' | 'creative' | 'research' | 'roleplay' | 'other'.agents.get()populatesskills/mcps/knowledgeBasesas full objects;create()/update()/list()return them as bare id strings (or omit them). Don’t assume one shape works for both.systemPromptandproviderIdare stripped from the response entirely when the calling identity doesn’t own the Agent (e.g. browsing apublicAgent that belongs to someone else) — both fields are typed optional for exactly this reason.DiscoverAgentsParamsaddscategory?on top of the usualpage?/limit?/search?/scope?.
Knowledge
Limits: up to 10 files per
uploadDocuments() call, 20MB each, restricted to
PDF/TXT/MD/JSON/CSV. providerId is required on CreateKnowledgeBaseInput — unlike Persona’s
own (non-Developer) Knowledge routes, there’s no “use my default Provider” fallback for a Project or
ExternalUser caller. search()’s options.topK overrides the KnowledgeBase’s own configured
topK for that one call only.
MCP connectors
transport: 'http' | 'sse'. authType: 'none' | 'oauth' | 'apiKey'. authMode: 'owner' | 'user' (owner-mode shares one connection across everyone; user-mode gives each external user their
own). apiKey is required when authType: 'apiKey'; oauth (or useDynamicRegistration: true)
is required when authType: 'oauth'.
Creating an
authType: 'oauth' MCP synchronously probes the target URL’s
OAuth discovery endpoints — pointing it at a URL that doesn’t actually
implement OAuth discovery will fail the create() call itself, not just a
later connection test.OAuth connection flows (mcps.oauth)
For authType: 'oauth' MCPs, mcps.oauth drives the owner- and user-mode authorization flows:
Owner-mode authorizes the MCP itself, shared by every user of your Project — call
getOwnerAuthorizeUrl() from a control-plane client and redirect your admin through it once.
User-mode authorizes one specific end user’s own token — call getUserAuthorizeUrl() from a
runtime-plane client (externalUserId set) and redirect that user through it; returnTo is an
optional client-chosen URL to send them back to once the flow completes.
Threads
Every method requires the client to have been constructed withexternalUserId — a Thread’s
Subject is always a specific end user, so a bare Project credential is rejected with a 400 EXTERNAL_USER_REQUIRED PersonaValidationError.
Thread.agentId is populated as { _id, name, avatar?, slug } on list(), but a bare id string on
create()/get() — check which call produced the value before assuming a shape.
ThreadMessages.messages holds raw LangChain-style message objects (role/content/tool_calls vary
by message type) and is intentionally typed as unknown[] rather than guessing at a shape the SDK
doesn’t own — inspect it at runtime if you need to render history yourself, or prefer
chat.stream()’s typed AG-UI events for anything you’re building a live UI around.
Files
Every method requires the client to have been constructed withexternalUserId, same requirement
and rejection behavior as Threads — a File’s Subject is always a specific end user.
files.download() is the one method on the whole client that doesn’t return parsed JSON — it
returns the raw Response so you can call .arrayBuffer()/.blob(), or pipe .body (a
ReadableStream) straight onward, e.g. into an HTTP response in a Route Handler, without buffering
the whole file in memory.
Chat, streamed
chat requires the client to have been constructed with externalUserId — a chat run always
executes as a specific end user.
SendMessageOptions: messages: { role: 'user' | 'assistant', content: string }[], threadId?
(resumes a named Thread from threads.create() instead of the implicit deterministic
one-conversation-per-Agent-per-user thread), resume? (see below), signal? (an AbortSignal).
AG-UI event types
stream() yields the standard @ag-ui/core EventType union (re-exported from this package as
EventType so you don’t need a separate @ag-ui/core dependency just to compare event.type):
Human-in-the-loop: interrupts and resuming
If an Agent’s configuration requires confirming a sensitive tool call, or the run needs a clarifying answer from the user, the run pauses instead of finishing normally and emits aCUSTOM
event instead of RUN_FINISHED. sendMessage() detects this for you and sets result.interrupt:
Resume on the next
sendMessage()/stream() call for the same Thread, with messages: [] (no
new user message — you’re answering the interrupt, not starting a new turn) and resume set:
ChatResult also exposes events: AguiEvent[] — every raw event received during that call, in
order, for anything text/interrupt don’t already surface.
TypeScript types
Every public type is exported from the package root (import type { ... } from "@personaai/sdk")
— resource clients (ProvidersResource, SkillsResource, AgentsResource, KnowledgeResource,
McpsResource, McpOAuthResource, ThreadsResource, FilesResource, ChatClient), every
create/update/list-params/result interface referenced in the tables above, PrincipalContext and
its two variants, AguiEvent/EventType, and the error classes from
Error handling. There’s no separate @personaai/sdk/types entry point to
remember — one import covers the client, every resource, and every type.
Out of scope
Project/Members/Credentials management (creating Projects, inviting Members, minting/revoking credentials) is intentionally not part of this SDK — those routes are Clerk-session (human-admin) authenticated, a structurally different auth model than the machine-credential calls this SDK makes end to end. Manage them from Developer Studio instead.Framework recipes
Express
Construct the client once at module scope and use it in your route handlers — no special handling needed.NestJS
WrapPersonaClient in an @Injectable() provider so it plugs into Nest’s DI container like any
other third-party client — the SDK itself needs no Nest-specific support.
Next.js — read this one carefully
Next.js blurs server and client code in one codebase more than Express or Nest does, which makes it the one framework people actually leak the credential in by accident.fetch, it also works in Next.js’s Edge runtime, not just Node —
no extra configuration needed. For a live-streaming chat UI, a Route Handler can return a
ReadableStream directly, relaying the AG-UI event stream onward to your own browser client: