Aexol Language

DocsAexol LanguageSpectral SDK

Drive the full spectral agent loop from Node.js code — create sessions, override tools, register custom tools, and opt in to native extensions.

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):

OptionTypeDefaultDescription
cwdstringprocess.cwd()Working directory for project-local discovery
agentDirstring~/.spectral/agentGlobal config directory
authStorageAuthStorageagentDir/auth.jsonCredential storage
modelRegistryModelRegistryagentDir/models.jsonModel registry
modelModelFrom settings, else first availableModel to use
thinkingLevelThinkingLevelFrom 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
toolsstring[]Default built-in setAllowlist of active tool names
customToolsToolDefinition[]—Custom tools to register
resourceLoaderResourceLoaderDefaultResourceLoaderFull override of extension loading
nativeExtensionsNativeExtensionsOption"none"Native extension opt-in (ignored when resourceLoader is provided)
extensionsstring[]—Explicit extension entry paths (ignored when resourceLoader is provided)
sessionManagerSessionManagerSessionManager.create(cwd)Session persistence
settingsManagerSettingsManagerSettingsManager.create(cwd, agentDir)Settings source
sessionIdstring—Stable caller-owned identity for provider cache affinity
sessionStartEventSessionStartEvent—Session start metadata for the extension runtime
devProcessRegistryBashJobRegistry—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:

IdWhat it provides
memoryObservational 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
browserChromium automation via Playwright — navigate, click, type, screenshot, inspect network (23 tools)
studio-mcpRemote Aexol Studio tools — From tasks, refinement, project files, cloud documents, project binding
wizardWizard artifact, skill, and project guidance integration (off by default)
image-generationImage generation through OpenRouter's Images API, with image-to-image support
spectral-vision-fallbackImage description via configured vision models when direct analysis is unavailable
seoOffline SEO audit toolset — on-page parsing, technical checks, schema, sitemaps, drift monitoring
webFetch and read web pages as text, markdown, or HTML
pdfGenerate polished PDF documents from editable HTML sources
desktop-screenshotCapture the desktop screen for AI vision analysis
desktop-controlComputer use — mouse, keyboard, window focus with a safety layer (off by default, injects real input)
code-orderAnalyze folder structure and file sizes, generate a cleanup report
portsList listening TCP ports, detect collisions, find free ports
session-fanoutSpawn 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:

EventDescription
agent_start / agent_endRun lifecycle; agent_end carries messages, optional error, and willRetry
message_start / message_update / message_endAssistant message streaming; message_update carries an assistantMessageEvent (e.g. text_delta)
tool_execution_start / tool_execution_endTool call lifecycle with toolCallId and toolName
turn_start / turn_endTurn boundaries
queue_updateQueued steering and follow-up messages
compaction_start / compaction_end / compaction_deltaContext compaction progress
auto_retry_start / auto_retry_endAutomatic 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 extensions paths
  • CLI & Tooling — Parse, validate, and generate docs from Aexol specs