Spectral SDK
The @aexol/spectral/sdk subpath exposes the same agent engine that powers the CLI as a programmatic SDK for Node.js. You can embed the full agent loop in your own process: create a session, choose which tools are active, register custom tools, and opt in to native extensions — all from code.
Unlike the CLI, native (built-in) extensions are off by default on the SDK path. Everything is opt-in, so your embedding process only gets the capabilities you explicitly enable.
Installation
The SDK ships with the Spectral CLI package:
npm install @aexol/spectral
Requires Node.js 20 or newer. Import from the /sdk subpath:
import { createAgentSession, defineTool } from "@aexol/spectral/sdk";
Quick Start
Create a session
createAgentSession sets up the full agent loop: model registry, auth storage, settings, and tools.
import { createAgentSession } from "@aexol/spectral/sdk";
const { session, extensionsResult, modelFallbackMessage } = await createAgentSession({
cwd: process.cwd(),
});
Subscribe to events
Results are delivered through events, not return values. message_update streams assistant text; agent_end marks the end of a run.
const unsubscribe = session.subscribe((event) => {
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
process.stdout.write(event.assistantMessageEvent.delta);
}
if (event.type === "agent_end") {
if (event.error) console.error(`Run failed: ${event.error}`);
console.log(`Done — ${event.messages.length} messages in the transcript`);
}
});
Run a prompt
prompt() awaits the full agent run — streaming text arrives through your listener, and the run ends with agent_end, after which prompt() resolves.
await session.prompt("What is in this repo?");
unsubscribe();
Session Options
createAgentSession(options) accepts these options (all optional):
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | process.cwd() | Working directory for project-local discovery |
agentDir | string | ~/.spectral/agent | Global config directory |
authStorage | AuthStorage | agentDir/auth.json | Credential storage |
modelRegistry | ModelRegistry | agentDir/models.json | Model registry |
model | Model | From settings, else first available | Model to use |
thinkingLevel | ThinkingLevel | From settings, else 'medium' | Reasoning depth (clamped to model capabilities) |
scopedModels | { model, thinkingLevel? }[] | — | Models available for cycling |
noTools | "all" | "builtin" | — | Start with no tools, or disable only the built-ins |
tools | string[] | Default built-in set | Allowlist of active tool names |
customTools | ToolDefinition[] | — | Custom tools to register |
resourceLoader | ResourceLoader | DefaultResourceLoader | Full override of extension loading |
nativeExtensions | NativeExtensionsOption | "none" | Native extension opt-in (ignored when resourceLoader is provided) |
extensions | string[] | — | Explicit extension entry paths (ignored when resourceLoader is provided) |
sessionManager | SessionManager | SessionManager.create(cwd) | Session persistence |
settingsManager | SettingsManager | SettingsManager.create(cwd, agentDir) | Settings source |
sessionId | string | — | Stable caller-owned identity for provider cache affinity |
sessionStartEvent | SessionStartEvent | — | Session start metadata for the extension runtime |
devProcessRegistry | BashJobRegistry | — | Machine-level registry for background bash jobs |
Overriding Tools
By default a session starts with the built-in coding tools: read, bash, bash_status, bash_kill, and apply_patch.
Allowlist
Pass tools to restrict the session to specific tool names. Only listed tools are enabled:
const { session } = await createAgentSession({
tools: ["read", "bash"],
});
Suppression modes
When you don't pass an allowlist, noTools changes the default:
// Start with no tools enabled at all
const empty = await createAgentSession({ noTools: "all" });
// Disable built-ins but keep extension/custom tools
const extOnly = await createAgentSession({ noTools: "builtin" });
Custom tools
Register your own tools with defineTool and activate them through the allowlist:
import { createAgentSession, defineTool } from "@aexol/spectral/sdk";
const lookupUser = defineTool({
name: "lookup_user",
label: "Lookup User",
description: "Finds a user by id in the local database.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User id" },
},
required: ["id"],
additionalProperties: false,
},
async execute(_toolCallId, params) {
const user = await findUser(params.id); // your application code
return {
content: [{ type: "text", text: JSON.stringify(user) }],
details: {},
};
},
});
const { session } = await createAgentSession({
tools: ["lookup_user"],
customTools: [lookupUser],
});
parameters is a JSON Schema (TypeBox-compatible) object. execute returns an AgentToolResult — a content array (typically one text block) plus optional details.
Tool factories
For tools that operate outside the session cwd, use the factories:
import { createAgentSession, createCodingTools, createReadOnlyTools } from "@aexol/spectral/sdk";
const { session } = await createAgentSession({
customTools: [
...createCodingTools("/other/project"), // full read + bash set
...createReadOnlyTools("/other/project"), // read-only
],
tools: ["read", "bash", "bash_status", "bash_kill", "apply_patch"],
});
createCodingTools(cwd) returns the read + bash toolset; createReadOnlyTools(cwd) returns just the read tool. Individual factories (createReadTool, createBashTool) are also exported.
Overriding Extensions
Native extensions are off by default
This is the key difference from the CLI: on the SDK path, native extensions are disabled unless you opt in:
import { createAgentSession } from "@aexol/spectral/sdk";
// Load every file-based native extension
const everything = await createAgentSession({ nativeExtensions: "all" });
// Load none (the default — same as omitting the option)
const bare = await createAgentSession({ nativeExtensions: "none" });
// Explicit selection by id — defaultEnabled flags are ignored
const selected = await createAgentSession({
nativeExtensions: { enabled: ["browser", "seo"], disabled: [] },
});
Explicit extension paths
Load your own extension files with extensions. These paths are always loaded by the default resource loader:
const { session, extensionsResult } = await createAgentSession({
extensions: ["./extensions/my-ext.ts"],
});
An extension file exports a default activate function that registers tools via ext.registerTool(...) — see Extensions for the full authoring API.
Graceful degradation
A missing extension path does not throw. The failure is recorded in extensionsResult.errors and the session is still created:
const { session, extensionsResult } = await createAgentSession({
extensions: ["./does-not-exist.ts"],
});
if (extensionsResult.errors.length > 0) {
for (const error of extensionsResult.errors) {
console.warn(`Failed to load ${error.path}: ${error.error}`);
}
}
// session is still usable
Precedence
When a resourceLoader is provided, nativeExtensions and extensions are ignored entirely — the loader wins.
Native Extensions Reference
Pass ids to nativeExtensions: { enabled, disabled } or use "all" to enable every file-based extension:
| Id | What it provides |
|---|---|
memory | Observational memory — persists observations and reflections across sessions (core, empty entryPath); it is not loaded on the SDK path via nativeExtensions — listing it in enabled is a no-op and "all" does not enable it either; memory only runs in the CLI runtime |
browser | Chromium automation via Playwright — navigate, click, type, screenshot, inspect network (23 tools) |
studio-mcp | Remote Aexol Studio tools — From tasks, refinement, project files, cloud documents, project binding |
wizard | Wizard artifact, skill, and project guidance integration (off by default) |
image-generation | Image generation through OpenRouter's Images API, with image-to-image support |
spectral-vision-fallback | Image description via configured vision models when direct analysis is unavailable |
seo | Offline SEO audit toolset — on-page parsing, technical checks, schema, sitemaps, drift monitoring |
web | Fetch and read web pages as text, markdown, or HTML |
pdf | Generate polished PDF documents from editable HTML sources |
desktop-screenshot | Capture the desktop screen for AI vision analysis |
desktop-control | Computer use — mouse, keyboard, window focus with a safety layer (off by default, injects real input) |
code-order | Analyze folder structure and file sizes, generate a cleanup report |
ports | List listening TCP ports, detect collisions, find free ports |
session-fanout | Spawn parallel worker sessions for batches of independent tasks |
The full manifests are available programmatically via NATIVE_EXTENSIONS and getNativeExtension(id).
Events & Control
Event subscription
session.subscribe(listener) returns an unsubscribe function. Events include:
| Event | Description |
|---|---|
agent_start / agent_end | Run lifecycle; agent_end carries messages, optional error, and willRetry |
message_start / message_update / message_end | Assistant message streaming; message_update carries an assistantMessageEvent (e.g. text_delta) |
tool_execution_start / tool_execution_end | Tool call lifecycle with toolCallId and toolName |
turn_start / turn_end | Turn boundaries |
queue_update | Queued steering and follow-up messages |
compaction_start / compaction_end / compaction_delta | Context compaction progress |
auto_retry_start / auto_retry_end | Automatic retry attempts |
Mid-run control
session.steer("Also check the test files"); // redirect the current run
await session.followUp("Now summarize what you found"); // queue after the run
await session.abort(); // cancel the current run
Runtime adjustments
session.setActiveToolsByName(["read"]); // change active tools mid-session
session.setModel(model); // switch model
session.setThinkingLevel("high"); // change reasoning depth
await session.compact(); // manually compact context
await session.reload(); // reload extensions and resources
Reading state
session.state; // current agent state
session.messages; // transcript so far
session.sessionId; // stable session identity
session.isStreaming; // whether a run is in progress
Advanced — Full Resource Control
For complete control over extension discovery and loading, construct your own DefaultResourceLoader and pass it as resourceLoader. It overrides both nativeExtensions and extensions:
import { createAgentSession, DefaultResourceLoader, SessionManager } from "@aexol/spectral/sdk";
const loader = new DefaultResourceLoader({
cwd: process.cwd(),
agentDir: "~/.spectral/agent",
nativeExtensions: "none", // or "all" / { enabled, disabled }
extensionFactories: [], // in-code extension factories
extensionsOverride: (base) => base, // transform the loaded set
noExtensions: false, // true disables all extensions
additionalExtensionPaths: ["./extensions/extra.ts"],
});
await loader.reload();
const { session, extensionsResult } = await createAgentSession({
resourceLoader: loader,
sessionManager: SessionManager.inMemory(),
});
// extensionsResult is the loader's own result:
console.log(extensionsResult === loader.getExtensions()); // true
loader.getExtensions() returns the current LoadExtensionsResult; loader.reload() re-runs discovery.
Authentication
By default the SDK reads credentials from ~/.spectral/agent/auth.json — the same store the CLI uses. If you are already authenticated via aexol login, the SDK picks those credentials up automatically.
For direct provider API keys, set the standard environment variables before creating the session:
export OPENROUTER_API_KEY=sk-or-... # OpenRouter models
export OPENAI_API_KEY=sk-... # OpenAI models
Without a usable model or API key, prompt() throws — check modelFallbackMessage from createAgentSession for guidance when no model is available.
Next Steps
- Spectral Agent — The same agent engine as an always-on CLI
- Extensions — Authoring extension files for
extensionspaths - CLI & Tooling — Parse, validate, and generate docs from Aexol specs