@ensoul-ai/sdk
v0.3.0
Published
Official TypeScript SDK for the Ensoul API. Build AI NPCs and personas with memory and personality that evolve through real conversation, scale to thousands, and run simulations where they change over time.
Downloads
243
Maintainers
Readme
Ensoul TypeScript SDK
Official TypeScript SDK for the Ensoul API. Build AI NPCs and personas with memory and personality that evolve through real conversation. Scale to thousands of personas, and run simulations where they grow and change over time.
Installation
npm install @ensoul-ai/sdkQuick Start
A persona needs no world. This one stands alone, and talks to one of your own end users:
import { Ensoul } from "@ensoul-ai/sdk";
const client = new Ensoul({ apiKey: "YOUR_API_KEY" });
// A persona needs no world. This one stands alone.
const persona = await client.personas.create({ name: "Ada" });
// userId is your own id for the person talking.
// Each end user gets their own conversations with the persona.
const reply = await client.chat.send(
persona.id,
"Hi! Who are you?",
{ userId: "YOUR_END_USER_ID" },
);
console.log(reply.response);
// End the conversation when the person is done.
await client.chat.endConversation(
persona.id,
reply.conversation_id,
{ userId: "YOUR_END_USER_ID" },
);Pass world: "my_world" to create to place a persona in a world instead.
Streaming
Chat and aggregate endpoints support server-sent events. The stream is an async iterable.
Each event's data is a JSON object. The delta text lives in the chunk field:
| Field | Type | Notes |
|-------|------|-------|
| chunk | string | The text delta. Append these to build the full reply. |
| conversationId | string | Stable across the stream. Pass it back to continue the conversation. |
| chunkIndex | number | 0-based position of this chunk in the stream. |
| isFinal | boolean | true on the last event. Its chunk is empty (""). |
| tokenUsage | object \| undefined | Present only on the final event. |
Use parseChatEvent to turn each raw SSEEvent into a typed ChatStreamEvent
(camelCase fields), then write chunk as it arrives:
import { parseChatEvent } from "@ensoul-ai/sdk";
const stream = await client.chat.stream("persona_abc123", "Tell me a story.");
for await (const event of stream.events()) {
const parsed = parseChatEvent(event);
process.stdout.write(parsed.chunk); // print text as it streams
if (parsed.isFinal) {
process.stdout.write("\n");
if (parsed.tokenUsage) console.log("tokens:", parsed.tokenUsage);
}
}If you prefer to read the raw payload yourself, the JSON uses snake_case keys
(chunk, conversation_id, chunk_index, is_final, token_usage):
for await (const event of stream.events()) {
const data = JSON.parse(event.data);
process.stdout.write(data.chunk);
}Pagination
List endpoints return a Page<T> object that implements Symbol.asyncIterator, so you can
iterate over all records without manual cursor management.
// Fetch a page
const page = await client.personas.list({ perPage: 50 });
console.log(page.items, page.total);
// Auto-paginate through all results
for await (const persona of page.autoPagingIter()) {
console.log(persona.id, persona.name);
}Error Handling
All errors extend EnsoulError. Import the specific subclasses you need.
import { Ensoul, AuthenticationError, RateLimitError, NotFoundError } from "@ensoul-ai/sdk";
try {
const persona = await client.personas.get("missing_id");
} catch (err) {
if (err instanceof NotFoundError) {
console.error("Persona not found:", err.message);
} else if (err instanceof RateLimitError) {
console.error("Rate limited. Retry after:", err.retryAfter);
} else if (err instanceof AuthenticationError) {
console.error("Invalid or missing API key.");
} else {
throw err;
}
}Error hierarchy:
EnsoulError
APIError
AuthenticationError (401)
PaymentRequiredError (402)
PersonaCallsLimitError (402, resource "persona_calls")
AuthorizationError (403)
NotFoundError (404)
ValidationError (422)
EndUserRequiredError (400, "user_id_required")
ConflictError (409)
TurnInFlightError (409, "turn_in_flight")
ConversationEndedError (409, "conversation_ended")
EndUserForgottenError (409, "end_user_forgotten")
RateLimitError (429)
ServerError (5xx)Each new subclass carries the fields its error body adds (ConversationEndedError.conversationId, PersonaCallsLimitError.personaId, and so on) and still matches a catch written against its parent, so instanceof ConflictError keeps catching TurnInFlightError. See MIGRATION.md.
Configuration
The client reads two environment variables as defaults:
| Variable | Purpose |
|----------|---------|
| ENSOUL_API_KEY | API key (avoids passing apiKey in code) |
| ENSOUL_BASE_URL | API base URL (default: https://api.ensoul-ai.com) |
Demo API: the current hosted demo is available at:
export ENSOUL_BASE_URL="https://api.demo.ensoul-ai.com"
export ENSOUL_API_KEY="your-api-key"With these set, new Ensoul() connects to the demo with no constructor options.
You can also pass the base URL explicitly:
const client = new Ensoul({ apiKey: "ens_...", baseUrl: "https://api.demo.ensoul-ai.com" });Authentication
API key:
const client = new Ensoul({ apiKey: "ens_live_..." });
// or rely on process.env.ENSOUL_API_KEY
const client = new Ensoul();Bearer token:
const client = new Ensoul({ bearerToken: "eyJ..." });OAuth2 token exchange (password flow, form-encoded):
const token = await client.auth.token("[email protected]", "your-password");
const authedClient = new Ensoul({ bearerToken: token.access_token });Resources
| Namespace | Description |
|-----------|-------------|
| client.personas | CRUD, list (paginated), batch create, personality vectors |
| client.chat | Send messages, streaming SSE, conversation history, explicit end |
| client.endUsers | Forget one of your end users: erase their conversations and memories |
| client.domains | World configuration management |
| client.simulations | Time-based evolution simulations |
| client.aggregate | Aggregate queries with streaming |
| client.memory | Memory management per persona |
| client.sessions | Hierarchical session orchestration |
| client.frameworks | Framework management |
| client.auth | OAuth2 token exchange |
| client.health | Health checks |
| client.info | Server configuration and metadata, and your account info and key binding (info.me()) |
Requirements
- Node.js 18+ (native
fetch, no runtime dependencies) - TypeScript 5.7+ (for full type inference)
