Atomic can help you use the SDK. Ask it to build an integration for your use case.
SDK
The SDK provides programmatic access to atomic’s agent capabilities. Use it to embed atomic in other applications, build custom interfaces, or integrate with automated workflows. Example use cases:- Build a custom UI (web, desktop, mobile)
- Integrate agent capabilities into existing applications
- Create automated pipelines with agent reasoning
- Build custom tools that spawn sub-agents
- Test agent behavior programmatically
Quick Start
ModelRuntime is the canonical asynchronous provider runtime when an integration wants provider-owned credentials, dynamic catalogs, and native providers in one object:
ModelRuntime.create() accepts custom authPath, modelsPath, credential storage, and runtime auth overrides, plus the model-catalog options allowModelNetwork, modelRefreshTimeoutMs, modelsStorePath, and modelsStore (see Model catalog persistence and refresh). ModelRegistry and AuthStorage remain available as Atomic’s synchronous compatibility facades. Use readStoredCredential(provider, authPath?) for a lightweight read of one stored provider credential.
Extensions supplied directly to SDK sessions can use the exported InlineExtension type. Extension APIs and event types include native registerProvider(Provider), registerEntryRenderer, entry_appended, before_provider_headers, and agent_settled.
The package root also exports buildContextEntries, sessionEntryToContextMessages, and CompactionEntry for converting durable session branches into model context. The equivalent active-session operation is sessionManager.buildContextEntries().
Installation
Install@bastani/atomic as a project dependency with npm, pnpm, or Bun:
With npm:
--ignore-scripts to the install command.
The SDK is included in the main package. No separate SDK package is needed.
Experimental remote sessions
@bastani/atomic/client is an experimental entrypoint for upstream remote protocol sessions. It exports RemoteSession plus transcript projection helpers. Pass it a connected PiClient from @earendil-works/pi-client, then use RemoteSession.open() or RemoteSession.create() to own one remote session.
RemoteSession and Atomic’s isolated interactive engine deliberately coexist; neither adapts the other. RemoteSession owns the pi-client/pi-protocol transport, its SessionLease, the leased SessionSnapshot, and the transcript projection used by an external protocol client. The isolated engine owns Atomic’s in-process host facade, child-process JSONL RPC engine, interactive rendering, custom UI, and engine recovery. The client entrypoint has no atomic client CLI command and does not start or control the local interactive engine.
RemoteSession.sessions is a durable catalog of SessionMetadata. That is enough for listing and selecting stored sessions, but not for Atomic consumers that need runtime phase, model, thinking level, attachment, or lock state. Those consumers need the SessionSnapshot from an acquired lease; RemoteSession.snapshot exposes the current leased snapshot.
This boundary is intentional. A bridge would join two different protocols and would risk routing isolated-engine teardown through the host facade’s unbounded cooperative abort. Keep the surfaces separate until a future upstream RemoteSession change supplies an engine-aware/server contract with teardown semantics that can preserve Atomic’s recovery guarantee. The API may change without notice while it remains experimental.
Experimental Harness factory
The package root also exportscreateCodingAgentHarness() for applications that provide a pi-agent-core ExecutionEnv. It creates a Harness with Atomic’s six coding tools: read, bash, edit, write, find, and search.
The factory routes the primary operations for the first five tools through the supplied execution environment, including directory-tree reads. URL reads use the session id for cache scope, fetch through the process network, and do not persist host-local artifacts because the factory has no local session directory. search is fully local; read and edit still use local path-variant probes and notebook projection, read also uses local archive, SQLite, and internal-resource selectors, write retains local generated-file, shebang, conflict, and resource helpers, and bash validates its cwd locally and uses Atomic’s local temp storage for overflow output.
The factory requires ExecutionEnv.renameFile() and does not add a fallback filesystem implementation.
Core Concepts
createAgentSession()
The main factory function for a singleAgentSession.
createAgentSession() uses a ResourceLoader to supply extensions, skills, prompt templates, themes, and context files. If you do not provide one, it uses DefaultResourceLoader with standard discovery.
AgentSession
The session manages agent lifecycle, message history, model state, compaction, and event streaming.compact() serializes older context to numbered lines, asks the session model for JSON deleted ranges, validates them, and mechanically reconstructs a durable verbatim transcript string. It appends a compaction entry with details.strategy: "verbatim-lines"; the recent tail remains ordinary messages. The model never authors replacement context text.
Session replacement APIs such as new-session, resume, fork, and import live on AgentSessionRuntime, not on AgentSession.
createAgentSessionRuntime() and AgentSessionRuntime
Use the runtime API when you need to replace the active session and rebuild cwd-bound runtime state. This is the same layer used by the built-in interactive, print, and RPC modes.createAgentSessionRuntime() takes a runtime factory plus the initial cwd/session target. The factory closes over process-global fixed inputs, recreates cwd-bound services for the effective cwd, resolves session options against those services, and returns a full runtime result.
AgentSessionRuntime owns replacement of the active runtime across:
newSession()switchSession()fork()- clone flows via
fork(entryId, { position: "at" }) importFromJsonl()
runtime.sessionchanges after those operations- event subscriptions are attached to a specific
AgentSession, so re-subscribe after replacement - if you use extensions, call
runtime.session.bindExtensions(...)again for the new session - creation returns diagnostics on
runtime.diagnostics - if runtime creation or replacement fails, the method throws and the caller decides how to handle it
Prompting and Message Queueing
PromptOptions controls prompt expansion, queueing behavior while streaming, and prompt preflight notifications:
preflightResult is called once per prompt() invocation:
truewhen the prompt was accepted, queued, or handled immediatelyfalsewhen prompt preflight rejected before acceptance
prompt() resolves. prompt() still resolves only after the full accepted run finishes, including retries. Failures after acceptance are reported through the normal event and message stream, not through preflightResult(false).
The prompt() method handles prompt templates, extension commands, and message sending:
- Extension commands (e.g.,
/mycommand): Execute immediately, even during streaming. They manage their own LLM interaction viapi.sendMessage(). - File-based prompt templates (from
.mdfiles): Expanded to their content before sending or queueing. - During streaming without
streamingBehavior: Throws an error. Usesteer()orfollowUp()directly, or specify the option. preflightResult(true): Means the prompt was accepted, queued, or handled immediately.preflightResult(false): Means preflight rejected before acceptance.
steer() and followUp() expand file-based prompt templates but error on extension commands (extension commands cannot be queued).
pauseQueuedMessages() is a synchronous admission gate. It moves existing raw steering/follow-up entries into a hold before an abort boundary and keeps later context-bearing arrivals—including trigger-turn custom messages, batches, interrupts, sendUserMessage(), and ordinary prompt() calls—queued without starting a provider turn. Content blocks, optional data, duplicate identities, raw text, message types, and the existing order within each queue kind are retained. Non-trigger custom messages remain history-only and do not invent a turn.
resumeQueuedMessages() releases that hold exactly once but does not itself start or continue a model turn. Its promise resolves to true only when raw held steering/follow-up work was released, and to false when no held raw work existed. The caller must use its existing explicit resume action (for example, the interactive chat submission or workflow resume boundary) to drive execution. clearQueue() clears the paused flag when it explicitly removes the final unowned held item; if a protected or interrupt-owned item remains, the gate stays paused.
Agent and AgentState
TheAgent class (from @earendil-works/pi-agent-core) handles the core LLM interaction. Access it via session.agent.
Events
Subscribe to events to receive streaming output and lifecycle notifications.message_start reports the
message the model is about to stream, but an in-process subscriber receives the
provider’s live partial rather than a snapshot of it, and the provider keeps
appending to that same object as the stream runs. Appending a delta to it adds
text the provider already added. A subscriber that attaches part-way through a
turn missed the deltas that came before it and can seed itself from
session.agent.state.streamingMessage, which holds the message currently being
streamed, if any.
Options Reference
Directories
.atomic locations first and legacy .pi locations for compatibility when multiple config directories are supported. Passing an explicit agentDir makes that directory the user override.
cwd is used by DefaultResourceLoader for:
- Project extensions (
.atomic/extensions/, then legacy.pi/extensions/) - Project skills:
.atomic/skills/, then legacy.pi/skills/.agents/skills/incwdand ancestor directories (up to git repo root, or filesystem root when not in a repo)
- Project prompts (
.atomic/prompts/, then legacy.pi/prompts/) - Context files (
AGENTS.override.md,AGENTS.md, orCLAUDE.mdwalking up from cwd) - Session directory naming
agentDir is used by DefaultResourceLoader for:
- Global extensions (
extensions/) - Global skills:
skills/underagentDir(for example~/.atomic/agent/skills/; legacy~/.pi/agent/skills/is also considered by default)~/.agents/skills/
- Global prompts (
prompts/) - Global context files (
AGENTS.override.md,AGENTS.md, orCLAUDE.mdunderagentDir) - Settings (
settings.json) - Custom models (
models.json) - Credentials (
auth.json) - Sessions (
sessions/)
ResourceLoader, cwd and agentDir no longer control resource discovery. They still influence session naming and tool path resolution.
Model
ModelRegistry keeps synchronous reads for extension compatibility, while catalog refresh is asynchronous. Extensions should await modelRegistry.refresh() before synchronous getAll(), find(), or getAvailable() reads when a provider may update its catalog. New SDK integrations use ModelRuntime; await modelRuntime.refresh() reports aborted and per-provider errors, and failed providers retain their last-known models.
If no model is provided:
- Tries to restore from session (if continuing)
- Uses default from settings
- Falls back to first available model
See examples/sdk/02-custom-model.ts
Model catalog persistence and refresh
ModelRuntime.create() restores cached catalogs from local persistence but does not contact
pi.dev unless you opt in. allowModelNetwork (default false) enables a create-time network
refresh, and modelRefreshTimeoutMs (default 15_000) bounds how long that refresh may run
before it is aborted. Pass refreshOnCreate: false to skip the initial catalog and
availability refresh entirely; built-in models remain available.
models-store.json next to models.json — with the default
modelsPath that is ~/.atomic/agent/models-store.json. Set modelsStorePath to choose
another location, or inject modelsStore to control persistence entirely; a runtime created
with modelsPath: null keeps its store in memory. Network refreshes are throttled to once
per provider every four hours unless forced. To force an immediate refresh, call
await modelRuntime.refresh({ allowNetwork: true, force: true, signal }). Setting
ATOMIC_OFFLINE (legacy alias PI_OFFLINE) disables model network access, and a
refresh() call that omits allowNetwork follows that same runtime network policy.
API Keys and OAuth
ModelRuntime is the asynchronous SDK engine for provider composition, credentials, model catalogs, and requests. ModelRegistry remains a thin compatibility facade for extensions; await modelRegistry.complete(model, context, options) routes a request through its runtime with the resolved provider and auth. New SDK integrations should pass modelRuntime to createAgentSession and use modelRuntime.complete() directly when they issue standalone requests.
Credential resolution combines runtime API-key overrides, stored auth.json credentials, environment variables, and the active models.json provider configuration. OAuth acquisition is provider-owned and runs through ModelRuntime.login().
See the complete ModelRuntime credential and model configuration example.
System Prompt
Use aResourceLoader to override the system prompt:
See examples/sdk/03-custom-prompt.ts
Tools
Specify which tools to expose by name:- Built-in tool names enabled by default:
read,bash,edit,write,find,search,ask_user_question,todo finddiscovers filesystem paths by glob;searchsearches file contents with regex patterns across files, directories, globs, and internal URLs.toolsis an allowlist: when provided, only the listed built-in, extension, and custom tool names are exposed.excludedToolsis a blocklist: matching built-in, extension, and custom tool names are omitted from the final registry and active tool set. If both are provided,toolsis applied first andexcludedToolssubtracts from it.noTools: "all"disables all toolsnoTools: "builtin"disables default built-ins while keeping extension and custom tools enabled, except names listed inexcludedTools
Bash tool behavior
Atomic’s built-inbash tool matches upstream pi: when bash is enabled, commands execute through the configured shell with the Atomic process permissions. Use tools, excludedTools, or noTools to decide whether a session exposes the bash tool at all. Atomic no longer provides a command-level allow/deny option for bash; use an operating-system/container sandbox or a custom tool/extension when you need command allowlisting or stronger isolation.
Tools with Custom cwd
When you pass a customcwd, createAgentSession() builds selected built-in tools for that cwd.
See examples/sdk/05-tools.ts
Custom Tools
defineTool() for standalone definitions and arrays like customTools: [myTool]. Inline pi.registerTool({ ... }) already infers parameter types correctly.
Custom tools passed via customTools are combined with extension-registered tools. Extensions loaded by the ResourceLoader can also register tools via pi.registerTool().
If you pass tools, include each custom or extension tool name you want enabled, for example tools: ["read", "bash", "my_tool"]. Use excludedTools to remove a custom or extension tool by name from the final exposed set.
ToolDefinition.constrainedSampling is part of the public SDK and survives defineTool(), customTools, tool wrappers, session/staged inspection, and isolated execution. Use { type: "json_schema", strict: "prefer" | "require" }, { type: "grammar", variants: { openai_lark?: string, openai_regex?: string } }, or false. prefer can fall back; require fails when the active model cannot enforce strict JSON Schema. Grammar constraints require one required string parameter and capable model metadata. Public inspection preserves optional-property identity exactly: an omitted key stays absent, an explicitly present undefined stays present, and false or a config object remains unchanged. The exported ConstrainedSamplingConfig type and extension reference define the exact shape. Typed RPC clients receive the four model capability flags through optional ModelInfo.compat; see RPC.
Factory-created createBashTool() instances receive the same execution-time ATOMIC_SESSION_*/PI_SESSION_* model and session snapshot as the built-in bash tool. Set exposeSessionEnvironment: false only when the subprocess must not receive it. MessageRenderOptions.outputPad is likewise passed to normal and isolated custom message renderers.
Structured output final results
structured_output is not registered in normal agent sessions by default. Add it only when a caller needs a machine-readable final-answer contract by registering the exported factory as a custom tool:
DecisionSchema, the model calls structured_output({ approved, findings }). Array and primitive schemas are also accepted by the factory when the target provider/tool runtime supports them; the captured value is whatever JSON value matches the schema. A successful call stores the params in capture.value, returns them as pretty-printed JSON tool-result text for text print mode, keeps the flat value in tool details, writes the same JSON to the configured output.outputPath when an output file sink is configured, and sets terminate: true so there is no extra follow-up assistant turn. Atomic relies on the tool schema instead of extra structured-output parsing or sidecar validation. Structured-output tool definitions opt out of oversized-result persistence.
Custom tool names are supported, and the prompt metadata follows the configured name. If you use a custom name such as final_decision, include that name in any explicit tools allowlist. If the standard structured_output name is required, register the factory with its default name:
See examples/sdk/05-tools.ts
Extensions
Extensions are loaded by theResourceLoader. DefaultResourceLoader discovers extensions from ~/.atomic/agent/extensions/ and .atomic/extensions/ first, then legacy ~/.pi/agent/extensions/ and .pi/extensions/, plus settings.json extension sources.
pi.events. Pass a shared eventBus to DefaultResourceLoader if you need to emit or listen from outside:
See examples/sdk/06-extensions.ts and Extensions
Skills
See examples/sdk/04-skills.ts
Context Files
See examples/sdk/07-context-files.ts
Slash Commands
See examples/sdk/08-prompt-templates.ts
Session Management
Sessions use a tree structure withid/parentId linking, enabling in-place branching.
See examples/sdk/11-sessions.ts and Session Format
Settings Management
SettingsManager.create(cwd?, agentDir?)- Load from filesSettingsManager.inMemory(settings?)- No file I/O
- Global:
~/.atomic/agent/settings.json, then legacy~/.pi/agent/settings.json - Project:
<cwd>/.atomic/settings.json, then legacy<cwd>/.pi/settings.json
- Settings getters/setters are synchronous for in-memory state.
- Setters enqueue persistence writes asynchronously.
- Call
await settingsManager.flush()when you need a durability boundary (for example, before process exit or before asserting file contents in tests). SettingsManagerdoes not print settings I/O errors. UsesettingsManager.drainErrors()and report them in your app layer.
See examples/sdk/10-settings.ts
ResourceLoader
UseDefaultResourceLoader to discover extensions, skills, prompts, themes, and context files.
Return Value
createAgentSession() returns:
Complete Example
Run Modes
The SDK exports run mode utilities for building custom interfaces on top ofcreateAgentSession():
InteractiveMode
Full TUI interactive mode with editor, chat history, and all built-in commands:runPrintMode
Single-shot mode: send prompts, output result, exit:runRpcMode
JSON-RPC mode for subprocess integration:RPC Mode Alternative
For subprocess-based integration without building with the SDK, use the CLI directly:- You want type safety
- You’re in the same Node.js process
- You need direct access to agent state
- You want to customize tools/extensions programmatically
- You’re integrating from another language
- You want process isolation
- You’re building a language-agnostic client