TypeScript SDK
Drive Ellipsis agent sessions from TypeScript. Install @ellipsis-dev/sdk; every /v1 operation typed from the API spec, no runtime dependencies.
The TypeScript SDK is a typed client for the Ellipsis API. Every /v1 operation is generated from the same OpenAPI spec the API reference is built from, so the two cannot disagree. On top of the generated methods it adds a session handle, live streaming, transparent pagination, and typed errors.
Install and authenticate
npm install @ellipsis-dev/sdkThe package is ESM only, ships its own type declarations, and has no runtime dependencies: it uses the runtime's global fetch, so it runs on current Node, Bun, Deno, and in browsers. Three subpath exports: @ellipsis-dev/sdk (the REST client, types, and session handle), @ellipsis-dev/sdk/stream (the WebSocket stream client), and @ellipsis-dev/sdk/store (a transcript store that turns frames into renderable state).
Create an API key on the API keys page of the dashboard and keep it in an environment variable. apiKey is the client's only required option; a CLI user token from agent login works in the same option. baseUrl defaults to https://api.ellipsis.dev, timeoutMs to 60 seconds per request, and maxRetries to 2; fetch is injectable for tests. Never ship an API key to a browser: call your own backend and let it hold the credential.
import { Ellipsis } from '@ellipsis-dev/sdk';
const client = new Ellipsis({ apiKey: process.env.ELLIPSIS_API_TOKEN! });Start a session and wait
sessions.run() takes exactly the options of sessions.start(): prompt, environment (a saved environment's name or env_ id, or an inline object), claude or codex, permissions, skills, output, budget in dollars, interactive, force_rebuild, and metadata. Anything omitted falls back to your organization settings, so prompt alone is a complete request. It resolves to a SessionHandle as soon as the session exists; the agent works in a cloud sandbox while your code decides what to do.
const handle = await client.sessions.run({
prompt: 'Fix the flaky test in ci/',
environment: 'cloud_agent_environment',
budget: 5.0,
});
await handle.send('Also add a regression test', {
idempotencyKey: 'regression-ask',
});
const session = await handle.wait({ timeoutMs: 900_000 });
console.log(session.status, session.exit_status);wait({ timeoutMs, pollIntervalMs }) polls every 3 seconds until the session settles, and rejects after timeoutMs, leaving the session running. send(message, { idempotencyKey }) posts into the session's inbox: delivered at the next turn boundary, it wakes a parked session, and the same key delivers once. stop() stops the agent now; history stays readable. refresh() re-fetches handle.session, the newest snapshot. The record log is client.sessions.records(handle.id). sessions.handle(sessionId) builds a handle over a session another process started.
Settled means status is completed, error, cancelled, or stopped, or a durable conversation parked with session_state of idle or closed. Parking matters: a durable conversation ends each turn with a terminal status while the conversation stays alive, so waiting on status alone would resolve after the first turn. The same rule is exported as isSettled(session).
Invoke an automation
An automation is a saved agent definition. automations.run() runs it exactly as defined and resolves to a SessionResponse; the options carry only the typed input its schema demands, an optional budget that may only lower the automation's own, and metadata. Wrap the result in a handle to wait on it. automations.list, get, create, update, delete, link, and unlink manage the definitions; templates.list and templates.get return the built-in starters.
const { session } = await client.automations.run('linear-issue-implementer', {
input: { issue: 'WEB-142' },
});
const handle = await client.sessions.handle(session.id);Stream a session live
streamSession from @ellipsis-dev/sdk/stream owns frame parsing, reconnect, resume, and heartbeat liveness; you supply openSocket, because opening a socket differs between a browser, a terminal, and a server. It receives the session id and a prebuilt handshake query (append it to the URL verbatim: it carries the protocol version and the resume cursor) and returns four listeners plus close. On a server, authenticate with a bearer header:
import WebSocket from 'ws';
import { streamSession, type OpenSocket } from '@ellipsis-dev/sdk/stream';
const token = process.env.ELLIPSIS_API_TOKEN!;
const openSocket: OpenSocket = ({ sessionId, query }) => {
const path = `/v1/sessions/${encodeURIComponent(sessionId)}/stream`;
const ws = new WebSocket(`wss://api.ellipsis.dev${path}?${query}`, {
headers: { authorization: `Bearer ${token}` },
});
return {
onOpen: (cb) => ws.on('open', cb),
onMessage: (cb) => ws.on('message', (raw) => cb(raw.toString())),
onClose: (cb) => ws.on('close', (code: number) => cb(code)),
onError: (cb) => ws.on('error', (err: Error) => cb(err)),
close: () => ws.close(),
};
};
const outcome = await streamSession({
sessionId: handle.id,
openSocket,
onFrame: (frame) => console.log(frame.type),
});
console.log(outcome.type);onFrame runs for every frame. streamSession resolves when the session finishes, with an outcome whose type is done (with the final status and exitStatus), error (with a message), or aborted if you cancelled through the optional signal. Seven frame types arrive, typed as the StreamFrame union. snapshot opens with the session and the earliest available record sequence. records_append carries new records in feed_seq order, the log of what the agent did. session resends the whole session whenever it changes. delta is partial text as the model produces it. heartbeat is liveness, roughly every 20 seconds. done ends the conversation. error reports a server-side failure with a message. The union also admits an unknown frame shape, so a new frame type does not break your build; ignore what you do not recognize.
A dropped socket reconnects with capped backoff, up to maxReconnects consecutive failures (5 by default; any delivered frame resets the count). Resume is cursored on records_append alone: the client asks for everything after the highest feed_seq it saw, so no record is lost or repeated, and snapshot and session frames are resent whole. StreamUnavailableError means streaming cannot be used here (endpoint missing, protocol unsupported, reconnects exhausted): poll with handle.wait() instead. StreamAuthError means the credential was rejected; polling would fail the same way. To render a transcript, pass onFrame: store.ingest with a SessionTranscriptStore from @ellipsis-dev/sdk/store; its subscribe and getSnapshot fit React's useSyncExternalStore.
List and paginate
A list method resolves to a Page. Iterating it with for await walks every page, one request per page, holding only the current page in memory: for await (const session of await client.sessions.list({ days: 7 })) reads a week of history. sessions.list, sessions.records, reviews.list, alerts.list, and files.list paginate; each accepts cursor and limit, and limit bounds the page, not the total. For one page at a time read page.items, page.hasMore, and page.nextCursor (pass it back as cursor); page.response is the raw envelope.
Errors
Every failure throws an EllipsisError. TransportError means the request never got a response (DNS, TLS, timeout). APIError is any non-2xx answer, carrying status, code, message, requestId, and body; its subclasses are chosen by status: AuthenticationError 401, ForbiddenError 403, NotFoundError 404, ConflictError 409, UnprocessableError 422, RateLimitError 429, ServerError 5xx. Switch on code, never on message; codes are an open vocabulary, so treat an unrecognized one by its status.
import { APIError } from '@ellipsis-dev/sdk';
try {
await handle.send('keep going');
} catch (error) {
if (!(error instanceof APIError && error.code === 'session_finished')) {
throw error;
}
}Transport failures and 429, 502, 503, and 504 are retried maxRetries times with exponential backoff and jitter (a 429 honors Retry-After) before anything reaches you. A RateLimitError in your code means the retries are already spent. No other status is retried.
Types
Request and response types are generated from the spec and exported from the package root: import type { Session } from '@ellipsis-dev/sdk'. Field names are the wire names, so a response matches the JSON exactly: session_state, not sessionState; only the SDK's own surface (idempotencyKey, timeoutMs) is camelCase. Responses are envelopes: sessions.get() resolves to a SessionResponse whose session is the Session, and the handle unwraps it for you. A field the API may omit is optional, and a nullable one includes null. Open vocabularies such as a record's source or an error code are typed string; closed ones keep their union. Timestamps are ISO 8601 strings. Money is a number in dollars: budget: 5.0 is five dollars.
Example: run in CI and fail the job on failure
import { Ellipsis } from '@ellipsis-dev/sdk';
const client = new Ellipsis({ apiKey: process.env.ELLIPSIS_API_TOKEN! });
const [owner, name] = process.env.GITHUB_REPOSITORY!.split('/');
const handle = await client.sessions.run({
prompt: 'Fix the flaky test in ci/',
environment: { repositories: [{ owner, name }] },
budget: 5.0,
});
const session = await handle.wait({ timeoutMs: 1_800_000 });
console.log(`${handle.id} ${session.status} (${session.exit_status})`);
if (session.status !== 'completed') process.exit(1);wait() rejects after 30 minutes, which fails the job and leaves the session running. Any status other than completed exits non-zero.
Example: fan out across repositories concurrently
import { Ellipsis } from '@ellipsis-dev/sdk';
const client = new Ellipsis({ apiKey: process.env.ELLIPSIS_API_TOKEN! });
const results = await Promise.all(
['your-org/web-repo', 'your-org/api-repo'].map(async (repository) => {
const [owner, name] = repository.split('/');
const handle = await client.sessions.run({
prompt: 'Fix the flaky test in ci/',
environment: { repositories: [{ owner, name }] },
});
const session = await handle.wait();
return `${repository.padEnd(20)} ${session.status}`;
})
);
console.log(results.join('\n'));