@aether-agent/sdk
v0.3.6
Published
TypeScript SDK for the Aether agent CLI
Readme
@aether-agent/sdk
TypeScript SDK for the Aether agent. It spawns
aether acp under the hood and exposes one explicit stateful API:
AetherSession— start an ACP session, send prompts, then close itmcp()— host closure-backed TypeScript tools as an MCP server and pass it to any agent via settings
Install
pnpm add @aether-agent/sdk
# or: npm install @aether-agent/sdkThe SDK depends on @aether-agent/cli, which bundles the aether binary for
your platform, so no separate install is required. Pass binaryPath to
AetherSession.start() if you want to point at a system or custom-built
aether instead (an absolute path or any name resolvable on PATH).
Basic session
AetherSession implements Symbol.asyncDispose, so the recommended pattern is
await using — the session closes and kills the subprocess automatically on
scope exit. SDK-hosted tool servers created with mcp() have their own
lifetime; see mcp().
import { AetherSession } from "@aether-agent/sdk";
await using session = await AetherSession.start({
cwd: "/path/to/repo",
agent: "planner",
});
for await (const message of session.prompt("Find TODOs in this repo")) {
if (message.type === "session_update") {
console.log(message.update);
}
}If your runtime predates explicit resource management, call session.close()
yourself in a finally block.
AetherSessionOptions lets you pick the initial agent or model:
| Option | Notes |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| agent | Mode name from .aether/settings.json (e.g. planner). |
| model | Direct model id (e.g. anthropic:claude-sonnet-4-5). |
| reasoningEffort | "low", "medium", "high", "xhigh". |
| settings | Inline Aether settings object using the .aether/settings.json shape. SDK-hosted tools live here under mcps. |
| settingsFile | Path to an alternate settings JSON file. |
| cwd | Working directory for the spawned aether acp process. |
| binaryPath | Override the bundled @aether-agent/cli binary (absolute path or name on PATH). |
| providers | Provider connection overrides, keyed by provider (for example { bedrock: { url: "http://127.0.0.1:8787", auth: "none" } }). |
| abortSignal | Cancel the active session and tear the subprocess down. |
agent and model are mutually exclusive. settings and settingsFile are
mutually exclusive. These are forwarded to the spawned aether acp process as
--settings-json and --settings-file, where the CLI resolves the initial
system prompt and tool filter before the session is constructed.
Provider connection overrides route a provider to a custom endpoint and can also
change auth behavior. Set auth: "none" only when a trusted proxy injects or
signs auth:
await AetherSession.start({
model: "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0",
providers: { bedrock: { url: "http://127.0.0.1:8787", auth: "none" } },
});For Bedrock inference profiles, keep model as the Bedrock foundation model ID
and pass the profile ARN as the Bedrock provider request target:
await AetherSession.start({
model: "bedrock:anthropic.claude-sonnet-4-5-20250929-v1:0",
providers: {
bedrock: {
inferenceProfileArn:
"arn:aws:bedrock:us-west-2:000000000000:application-inference-profile/000000000000",
},
},
});Multi-turn usage
await using session = await AetherSession.start({ cwd: process.cwd() });
for await (const m of session.prompt("First question")) console.log(m);
for await (const m of session.prompt("Follow-up")) console.log(m);SDK-hosted MCP tools with mcp()
mcp() creates a TypeScript MCP server. Tools run in the calling Node process, so closures, in-memory state, file handles, and database connections all work as you'd expect.
The returned handle implements Symbol.asyncDispose, so await using tears the server down on scope exit.
import { AetherSession, mcp, tool } from "@aether-agent/sdk";
import { z } from "zod";
function createSubmitTool() {
let submitted: { answer: string } | null = null;
return {
tool: tool({
name: "submit_answer",
description: "Submit the final answer",
inputSchema: { answer: z.string() },
handler: async ({ answer }) => {
submitted = { answer };
return { content: [{ type: "text", text: "Submitted." }] };
},
}),
getResult: () => submitted,
};
}
const submit = createSubmitTool();
await using custom = await mcp({ name: "custom", tools: [submit.tool] });
{
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.",
)) {
void _message;
}
}
console.log(submit.getResult());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],
},
],
},
});How mcp() is wired
Each mcp() call starts a small Streamable HTTP MCP server on
127.0.0.1:<random-port> and returns its address as an inline McpSourceSpec.
Adding that spec to settings.mcps (or an agent's mcps) tells the spawned
aether process to connect to it. Each server is protected by:
- A random bearer token (
Authorization: Bearer …) minted permcp()call. - DNS rebinding protection (host-header validation) provided by
createMcpExpressApp().
The server starts when you await mcp(...) and stops when the handle is
disposed — via await using scope exit or an explicit
await handle[Symbol.asyncDispose](). Disposal is idempotent.
Aether tool naming
Aether names MCP tools as server__tool internally. The name passed to mcp()
is the server prefix. If you register a tool named submit_answer under the
custom name, the agent sees it as custom__submit_answer. If your selected
agent has a restrictive tool allowlist in .aether/settings.json, include the
custom server pattern or leave the filter empty.
Permission and elicitation hooks
By default the SDK auto-accepts the first allow_* permission option — this is
the exported autoApprovePermissions handler, suitable for trusted/dev
contexts. For untrusted agents or production hosts, supply your own handler:
import { AetherSession, autoApprovePermissions } from "@aether-agent/sdk";
// Explicit auto-approve (same as the default).
await AetherSession.start({ onPermissionRequest: autoApprovePermissions });
// Custom policy.
await AetherSession.start({
onPermissionRequest: async (request) => {
return {
outcome: { outcome: "selected", optionId: request.options[0].optionId },
};
},
});onElicitation handles Aether's _aether/elicitation extension request.
See the [`@aether-agent/evals` README](../aether-evals/README.md) for usage.