aod-sdk-typescript
Agent on Demand TypeScript SDK Skill
The @ravi-hq/aod-sdk package wraps every endpoint in docs/openapi.yaml with typed interfaces, a single async Client, and an AsyncIterable SSE event stream. Zero runtime dependencies — uses built-in fetch / ReadableStream / AbortController. Node 18+ and modern browsers. Package source lives at clients/typescript/ in this repo.
When This Skill Applies
Use this skill when:
- Writing TS/JS that calls the AoD API via
import { Client } from "@ravi-hq/aod-sdk" - Extending
clients/typescript/itself (new resources, new types, new stream helpers) - Debugging a thrown
AodHTTPError(orConflictError/ValidationError/RateLimitError)
For HTTP-level questions (route table, state machine, 409/422/429 semantics), defer to the agent-on-demand-api skill. For Python, use aod-sdk-python.
Install & Configure
npm install @ravi-hq/aod-sdk
# or, in-tree development:
cd clients/typescript && npm install && npm test
new Client({...}) resolves config in this order:
- Constructor options:
baseUrl,token - Env vars:
AOD_API_URL,AOD_API_TOKEN— Node only. In the browser they're undefined. baseUrldefault:http://localhost:8777.tokenis required — missing throws immediately (before any network call).
Client is not a Disposable — it holds no open connections between requests, so you don't need to close it. Streams are the exception (see below).
import { Client } from "@ravi-hq/aod-sdk";
const client = new Client({ baseUrl: "https://aod.example", token: "aod_..." });
// or just: new Client() — reads both from env in Node
Other constructor options:
| Option | Default | Notes |
|---|---|---|
fetch |
globalThis.fetch |
Inject for tests/proxies. Must match the standard fetch shape. |
timeoutMs |
30000 |
Per non-streaming request. Streams use the caller's AbortSignal instead. |
Resources Shape
Single async Client, three resource namespaces. Every non-stream method accepts an optional trailing { signal } for AbortSignal cancellation.
| Namespace | Methods |
|---|---|
client.agents |
list(), create(params), get(id), update(id, params), archive(id), versions(id) |
client.environments |
list(), create(params), get(id), update(id, params), archive(id), delete(id), versions(id) |
client.sessions |
list(), create(params), get(id), prompt(id, params), turns(id), terminate(id), delete(id), stream(id, options?) |
- Return types are interfaces exported from
@ravi-hq/aod-sdk:Agent,Environment,Session,SessionAck,SessionTurn,AgentVersion,EnvironmentVersion, plus param interfaces (AgentCreateParams,SessionCreateParams, etc.). Interfaces are structural — extra server fields don't break consumers; missing ones are typedundefined | null. - IDs are plain
strings (UUIDs on the wire). No helper type. sessions.create/sessions.prompt/sessions.terminatereturnSessionAck, notSession. Onlyid+statusare guaranteed;stream_url/environment_id/resources/current_turnare populated when the server provides them. Callclient.sessions.get(id)for the full record.- Request params are
stripUndefinedd before being serialized — omitted fields aren't sent, preserving server-side defaults.
Streaming
client.sessions.stream(sessionId, options?) returns a StreamHandle — an AsyncIterable<StreamEvent> with a close() method. Always wrap in try/finally so the underlying connection closes if you break early.
const stream = await client.sessions.stream(ack.id);
try {
for await (const event of stream) {
if (event.type === "output") process.stdout.write(event.extra.data as string);
if (event.type === "exit" || event.type === "error"
|| event.type === "terminated" || event.type === "stale") break;
}
} finally {
await stream.close();
}
StreamEvent shape: { type: StreamEventType; id: number | null; extra: Record<string, unknown> }. Everything except type and id lands in extra. Event schema is still evolving server-side; the SDK deliberately keeps the raw payload accessible instead of typing every field.
Event types: start, turn_start, output, stage, exit, error, terminated, stale. Terminal types are exit/error/terminated/stale.
Resume after disconnect with { since: lastEventId }. Cancel with an external AbortSignal:
const controller = new AbortController();
const stream = await client.sessions.stream(sid, { signal: controller.signal });
// ...later:
controller.abort(); // OR
await stream.close(); // either works — both abort the underlying connection
The stream's internal AbortController is linked to whatever signal you pass — aborting the external signal aborts the stream, and stream.close() aborts both.
Errors
All non-2xx responses throw a typed subclass of AodError:
| Status | Class | Common trigger |
|---|---|---|
| 401/3 | AuthError |
Missing/invalid token |
| 404 | NotFoundError |
Resource missing or not owned by the token's user |
| 409 | ConflictError |
Archive-already, terminal session, stale version, failed-resume |
| 422 | ValidationError |
Pydantic validation on the server — detail is a list |
| 429 | RateLimitError |
Concurrent-session quota. Has .limit and .active props (may be null) |
| 5xx | ServerError |
Sprites upstream error or unhandled exception |
All share .statusCode, .detail, .method, .url. detail is unknown in the TS types — it's a string for most codes and an array of error objects for 422. Narrow with instanceof rather than branching on statusCode, and on 422 check Array.isArray(err.detail) before reading.
import { ConflictError, RateLimitError } from "@ravi-hq/aod-sdk";
try {
await client.agents.update(agent.id, { version: agent.version, name: "renamed" });
} catch (e) {
if (e instanceof ConflictError) {
const latest = await client.agents.get(agent.id);
await client.agents.update(latest.id, { version: latest.version, name: "renamed" });
} else if (e instanceof RateLimitError) {
console.warn(`quota: ${e.active}/${e.limit}`);
} else {
throw e;
}
}
Optimistic Concurrency Idiom
Agents and environments require version on every update. Stale → ConflictError.
const agent = await client.agents.get(agentId);
await client.agents.update(agent.id, { version: agent.version, system: "..." });
Merge/replace semantics match the HTTP API (see agent-on-demand-api):
metadatais merged per-key; empty string deletes the key.env_varsis fully replaced — re-send every key you want to keep.
Node vs Browser
The SDK is isomorphic with a few caveats:
- Env var fallbacks are Node-only. In a browser pass
baseUrlandtokenexplicitly. - CORS: calling the AoD API from a browser on a different origin requires the server to send CORS headers. The SDK itself does nothing special.
- No
process.envleaks: config resolution guardstypeof process !== "undefined"so bundlers/Deno/browsers don't choke. - Dependencies: zero. If you see anything imported outside the
@ravi-hq/aod-sdktree, it's a bug.
Common Gotchas
- Missing token throws synchronously from
new Client({}). No network roundtrip to discover the config is broken. Sessionhas noprompt.promptlives onSessionTurn— fetch withclient.sessions.turns(sessionId).SessionAck.environment_idis absent onprompt/terminateacks. Only populated oncreate. Don't rely on it after a resume.for awaitof aStreamHandleleaks the connection if you don'tawait stream.close()infinally. Theclose()aborts the fetch; without it the reader can outlive your function.event.extra.data(foroutput) is typedunknown. You know from the server that it's astring, but the SDK types itunknownbecause the payload shape is evolving. Cast at the boundary.- There's no sync client. Node's
fetchis async — use top-levelawaitin modules or wrap in anasyncfunction. VERSIONexported from the package is a plainconst("0.1.1"at time of writing). Keep it in sync withpackage.jsonwhen releasing (release script enforces this).
End-to-End Example
import { Client } from "@ravi-hq/aod-sdk";
const client = new Client({ token: "aod_..." });
const env = await client.environments.create({
name: "demo",
packages: { pip: ["requests"] },
env_vars: { DEMO: "1" },
networking: { type: "limited", allowed_hosts: ["pypi.org"] },
});
const agent = await client.agents.create({
name: "demo",
model: "anthropic/claude-sonnet-4-6",
runtime: "claude",
system: "You are terse.",
environment_id: env.id,
});
const ack = await client.sessions.create({
agent_id: agent.id,
prompt: "summarize README.md",
resources: [{ type: "github_repository", url: "https://github.com/me/repo" }],
});
const stream = await client.sessions.stream(ack.id);
try {
for await (const event of stream) {
if (event.type === "output") process.stdout.write(event.extra.data as string);
if (["exit", "error", "terminated", "stale"].includes(event.type)) break;
}
} finally {
await stream.close();
}
const final = await client.sessions.get(ack.id);
console.log(`status=${final.status} exit_code=${final.exit_code}`);
Related Files
clients/typescript/src/client.ts—Client+ config resolutionclients/typescript/src/resources/—agents.ts,environments.ts,sessions.ts, barrelindex.tsclients/typescript/src/types.ts— interfaces (Agent,Session,StreamEvent, ...) andstreamEventFromPayloadclients/typescript/src/errors.ts— error classes +raiseForStatusclients/typescript/src/stream.ts—createStreamHandle+ SSE readerclients/typescript/src/index.ts— public exports barrelclients/typescript/README.md— user-facing docs- Sibling skill
agent-on-demand-api— HTTP semantics, status codes, state machine
More from ravi-hq/agent-on-demand
aod-sdk-python
Use when writing Python code that calls the Agent on Demand API via `aod-sdk` (package `aod` — `Client`/`AsyncClient`, `client.agents`/`environments`/`sessions`, `client.sessions.stream(...)`). Covers install, `AOD_API_URL`/`AOD_API_TOKEN` env fallbacks, typed pydantic models, sync-vs-async method parity, the `StreamEvent` SSE iterator (context manager + `.extra` dict), typed `AodHTTPError` subclasses (`ConflictError`/`ValidationError`/`RateLimitError.limit`/`active`), and the runtime-scoped `aod.pretty` formatters. Defers to the `agent-on-demand-api` skill for HTTP semantics, status codes, and state-machine edges.
1agent-on-demand-api
Use when driving the Agent on Demand REST API — creating agents, environments, or sessions; writing/maintaining `tests/e2e/`; adding new endpoints; or debugging 4xx responses. Covers auth, the route table, the `detail`-is-a-list quirk for 422, optimistic concurrency (`version`), agent-metadata-merge vs env_vars-full-replacement divergence, the session state machine (failed is terminal), session resources (GitHub repo clone), concurrent-session quota (429), SSE stream with stage events, and multi-turn session semantics. Canonical spec is `docs/openapi.yaml`.
1