Skip to content
Theme:

TypeScript SDK

The TypeScript SDK allows you to programatically define agents, start sessions, send prompts, stream updates, and attach MCP tools.

Terminal window
npm install @aether-agent/sdk

The package depends on @aether-agent/cli, which bundles the aether binary for your platform. You do not need to install the CLI separately.

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}`);
}
}

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.

OptionDescription
cwdWorking directory for the spawned aether acp process. Defaults to process.cwd().
agentAgent name from .aether/settings.json.
modelDirect model id, such as anthropic:claude-sonnet-4-5.
reasoningEffort"minimal", "low", "medium", "high", "xhigh", or "max" when using a direct model.
settingsInline Aether settings object using the .aether/settings.json shape. SDK-hosted and external MCP servers live here under mcps.
settingsFilePath to an alternate settings JSON file.
binaryPathOverride the bundled CLI binary with an absolute path or command available on PATH.
envEnvironment variables for the spawned aether acp process.
logDirCustom directory for ACP logs.
providersProvider connection overrides, such as custom Bedrock endpoints or auth behavior.
traceContextA remote W3C traceparent with optional tracestate, or a standalone traceId for root spans without a parent. See Telemetry for validation and sampling semantics.
abortSignalCancel the active session and tear down the subprocess.
onPermissionRequestCustom policy for ACP permission requests. Defaults to autoApprovePermissions.
onElicitationHandler for native ACP elicitation/create requests (form or URL mode).

settings and settingsFile are mutually exclusive.

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.

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);
}
}

AetherHeadlessOptions mirrors the headless CLI flags plus SDK process controls:

OptionDescription
promptRequired. Prompt text to run.
agentNamed agent from .aether/settings.json. Mutually exclusive with model.
modelDirect model id, such as anthropic:claude-sonnet-4-5. Mutually exclusive with agent.
cwdWorking directory. Defaults to process.cwd().
settingsInline Aether settings object. Mutually exclusive with settingsFile.
settingsFilePath to an alternate settings JSON file.
systemPromptAdditional system prompt text.
eventsEvent kinds to emit; omit to include all, including session_usage and context_usage. See event filtering.
verboseVerbose diagnostic logging to stderr.
providersProvider connection overrides, such as custom Bedrock endpoints or auth behavior.
traceContextA remote W3C traceparent with optional tracestate, or a standalone traceId. See Telemetry.
binaryPathOverride the bundled CLI binary with an absolute path or command available on PATH.
envEnvironment variables for the spawned aether headless process.
abortSignalCancel the run and tear down the subprocess.

Unlike AetherSession.start(), headless runs are non-interactive, so there are no permission or elicitation handlers.

session.prompt() yields AetherMessage objects. Each has a type discriminator:

TypeDescription
session_updateAn ACP session update (agent text, tool calls, tool results, progress, etc.). Access .update.
usagePer-call token usage and estimated cost, plus cumulative session totals. Access .usage.
elicitation_completeA URL-mode elicitation finished. Access .elicitationId.
resultThe prompt finished. .stopReason describes why (e.g. end_turn, cancelled).
errorAn 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.

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.

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.

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],
},
],
},
});

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 ..." },
},
},
},
],
},
});

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 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.