TypeScript SDK
The TypeScript SDK allows you to programatically define agents, start sessions, send prompts, stream updates, and attach MCP tools.
Install
Section titled “Install”npm install @aether-agent/sdkThe package depends on @aether-agent/cli, which bundles the aether binary for your platform. You do not need to install the CLI separately.
Run an agent
Section titled “Run an agent”import { AetherSession } from "@aether-agent/sdk";
await using session = await AetherSession.start({ cwd: process.cwd(), agent: "planner",});
for await (const message of session.prompt("Find TODOs in this repo")) { if (message.type === "session_update") { console.log(message.update); }
if (message.type === "result") { console.log(`Agent stopped: ${message.stopReason}`); }}Selecting an agent or model
Section titled “Selecting an agent or model”AetherSession.start() accepts either an Aether agent name from .aether/settings.json or a direct model id.
await AetherSession.start({ cwd: "/path/to/repo", agent: "codebase-explorer",});
await AetherSession.start({ cwd: "/path/to/repo", model: "anthropic:claude-sonnet-4-5", reasoningEffort: "high",});agent and model are mutually exclusive. reasoningEffort requires model.
Session options
Section titled “Session options”| Option | Description |
|---|---|
cwd | Working directory for the spawned aether acp process. Defaults to process.cwd(). |
agent | Agent name from .aether/settings.json. |
model | Direct model id, such as anthropic:claude-sonnet-4-5. |
reasoningEffort | "minimal", "low", "medium", "high", "xhigh", or "max" when using a direct model. |
settings | Inline Aether settings object using the .aether/settings.json shape. SDK-hosted and external MCP servers live here under mcps. |
settingsFile | Path to an alternate settings JSON file. |
binaryPath | Override the bundled CLI binary with an absolute path or command available on PATH. |
env | Environment variables for the spawned aether acp process. |
logDir | Custom directory for ACP logs. |
providers | Provider connection overrides, such as custom Bedrock endpoints or auth behavior. |
traceContext | A remote W3C traceparent with optional tracestate, or a standalone traceId for root spans without a parent. See Telemetry for validation and sampling semantics. |
abortSignal | Cancel the active session and tear down the subprocess. |
onPermissionRequest | Custom policy for ACP permission requests. Defaults to autoApprovePermissions. |
onElicitation | Handler for native ACP elicitation/create requests (form or URL mode). |
settings and settingsFile are mutually exclusive.
Multi-turn sessions
Section titled “Multi-turn sessions”A session is stateful. Send follow-up prompts on the same session to continue the conversation.
await using session = await AetherSession.start({ cwd: process.cwd() });
for await (const message of session.prompt("Explain the architecture")) { console.log(message);}
for await (const message of session.prompt("Now list likely refactors")) { console.log(message);}Only one prompt can be in progress per session.
Run a single headless prompt
Section titled “Run a single headless prompt”runHeadless() runs one non-interactive prompt and returns an async iterator of generated AgentEvent objects.
import { runHeadless } from "@aether-agent/sdk";
for await (const event of runHeadless({ cwd: process.cwd(), prompt: "Summarize the architecture",})) { if (event.category === "message" && event.event.type === "text") { console.log(event.event.chunk); } else if (event.category === "session_usage") { console.log("Estimated cost (USD):", event.event.totals.estimated_usd); }}Options
Section titled “Options”AetherHeadlessOptions mirrors the headless CLI flags plus SDK process controls:
| Option | Description |
|---|---|
prompt | Required. Prompt text to run. |
agent | Named agent from .aether/settings.json. Mutually exclusive with model. |
model | Direct model id, such as anthropic:claude-sonnet-4-5. Mutually exclusive with agent. |
cwd | Working directory. Defaults to process.cwd(). |
settings | Inline Aether settings object. Mutually exclusive with settingsFile. |
settingsFile | Path to an alternate settings JSON file. |
systemPrompt | Additional system prompt text. |
events | Event kinds to emit; omit to include all, including session_usage and context_usage. See event filtering. |
verbose | Verbose diagnostic logging to stderr. |
providers | Provider connection overrides, such as custom Bedrock endpoints or auth behavior. |
traceContext | A remote W3C traceparent with optional tracestate, or a standalone traceId. See Telemetry. |
binaryPath | Override the bundled CLI binary with an absolute path or command available on PATH. |
env | Environment variables for the spawned aether headless process. |
abortSignal | Cancel the run and tear down the subprocess. |
Unlike AetherSession.start(), headless runs are non-interactive, so there are no permission or elicitation handlers.
Messages
Section titled “Messages”session.prompt() yields AetherMessage objects. Each has a type discriminator:
| Type | Description |
|---|---|
session_update | An ACP session update (agent text, tool calls, tool results, progress, etc.). Access .update. |
usage | Per-call token usage and estimated cost, plus cumulative session totals. Access .usage. |
elicitation_complete | A URL-mode elicitation finished. Access .elicitationId. |
result | The prompt finished. .stopReason describes why (e.g. end_turn, cancelled). |
error | An unrecoverable error occurred. Access .error. |
A result message is always emitted last (unless the stream errors), so it is a reliable place to stop consuming the iterator.
Token usage and estimated cost
Section titled “Token usage and estimated cost”Aether exposes accounting as a domain-level usage message rather than an ACP
extension notification:
for await (const message of session.prompt("Implement the feature")) { if (message.type === "usage") { console.log(message.usage.tokens); console.log(message.usage.estimated_cost?.total_usd); console.log(message.usage.totals.estimated_usd); }}tokens and estimated_cost describe the provider call that produced the
update. totals contains cumulative usage after that call. Costs are
catalog-based USD estimates; totals.estimated_usd excludes calls whose model
pricing is unknown, and totals.unpriced_calls reports how many were excluded.
Cumulative cost breakdowns are available via totals.estimated_input_usd,
totals.estimated_output_usd, totals.estimated_cache_read_usd, and
totals.estimated_cache_creation_usd.
Do not sum totals fields across snapshots or group/filter by source.parent_agent_id. The root agent’s stream includes sub-agent and
compaction in its totals, even when source identifies a child.
TypeScript tools
Section titled “TypeScript tools”Use tool() to define a TypeScript function and mcp() to host one or more of
them as an MCP server. Tool handlers run in your Node.js process, so closures and
in-memory state work normally.
import { AetherSession, mcp, tool } from "@aether-agent/sdk";import { z } from "zod";
let submitted: { answer: string } | null = null;
const submitAnswer = tool({ name: "submit_answer", description: "Submit the final answer", inputSchema: { answer: z.string() }, handler: async ({ answer }) => { submitted = { answer }; return { content: [{ type: "text", text: "Submitted." }] }; },});
await using custom = await mcp({ name: "custom", tools: [submitAnswer] });
await using session = await AetherSession.start({ cwd: process.cwd(), settings: { agents: [], mcps: [custom.spec], },});
for await (const _message of session.prompt( "Call custom__submit_answer with the final answer.",)) { // Consume streamed agent updates until the prompt completes.}
console.log(submitted);The handle implements Symbol.asyncDispose, so await using stops the server on
scope exit.
Per-agent tools
Section titled “Per-agent tools”A spec on the top-level mcps is available to every agent. Put it on a single
agent’s mcps instead to scope those tools to that agent.
await using planner = await mcp({ name: "planner-tools", tools: [plan] });await using reviewer = await mcp({ name: "reviewer-tools", tools: [review] });await using session = await AetherSession.start({ settings: { agents: [ { name: "planner", description: "Planner", model: "anthropic:claude-sonnet-4-5", userInvocable: true, mcps: [planner.spec], }, { name: "reviewer", description: "Reviewer", model: "anthropic:claude-sonnet-4-5", userInvocable: true, mcps: [reviewer.spec], }, ], },});External MCP servers
Section titled “External MCP servers”Attach external MCP servers by adding an inline source to settings.mcps.
await using session = await AetherSession.start({ cwd: process.cwd(), settings: { agents: [], mcps: [ { type: "inline", servers: { filesystem: { type: "stdio", command: "uvx", args: ["mcp-server-filesystem", process.cwd()], }, remote: { type: "http", url: "https://mcp.example.com/mcp", headers: { Authorization: "Bearer ..." }, }, }, }, ], },});Permissions and elicitation
Section titled “Permissions and elicitation”By default, the SDK uses autoApprovePermissions, which selects the first allow_* permission option. That is convenient for trusted development contexts. For production or untrusted prompts, provide an explicit permission policy.
import { AetherSession } from "@aether-agent/sdk";
await AetherSession.start({ onPermissionRequest: async (request) => { const safeOption = request.options.find( (option) => option.kind === "allow_once", ); return safeOption ? { outcome: { outcome: "selected", optionId: safeOption.optionId } } : { outcome: { outcome: "cancelled" } }; }, onElicitation: async (request) => { // Native ACP request: `mode` is "form" or "url". console.log(request.mode, request.message); return { action: "cancel" }; },});The hook receives native ACP form or URL elicitation requests and returns an ACP accept, decline, or cancel response. Without the hook, the SDK does not advertise elicitation support.
Provider overrides
Section titled “Provider overrides”Provider overrides can route a provider to a custom endpoint or change auth behavior.
await AetherSession.start({ model: "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0", providers: { bedrock: { url: "http://127.0.0.1:8787", auth: "none", }, },});Set auth: "none" only when a trusted proxy injects or signs provider authentication.