Docs
Core & Custom Backends
@nanocodana/core is the engine the adapters wrap: the agent loop, the tools, skills, approval gating, and MCP wiring. It imports no Node builtins and touches no DOM, so it runs wherever JavaScript does — a serverless function, an edge runtime, a container, a remote sandbox, or embedded in your own app — over whatever storage you hand it.
npm install @nanocodana/core aiimport { NanoCodana } from '@nanocodana/core'
const agent = new NanoCodana({
model,
fs: myFileSystem, // any IFileSystem
// or: sandbox: mySandbox
})
// Same streaming surface as the adapters.
const result = await agent.stream({ messages })Which package do I want?#
One question decides it — where do the files live?
| Files live… | Use |
|---|---|
| on a real disk | NodeAgent— laptop, server, or a serverless function's own disk (clone → work → push). |
| in a browser tab | BrowserAgent — in-memory or IndexedDB. |
| anywhere else | core — a database, blob storage, a remote sandbox: you hand the agent its filesystem. |
The easy way: hydrate + onFilesChange#
You usually don't need to implement a filesystem at all. Load the project from your store into the built-in in-memory FS via initialFiles, let the agent work at memory speed, and persist every change back with the onFilesChange hook. This is exactly how Sharables works — swap its IndexedDB for your database and the pattern is unchanged:
const project = await db.loadFiles(projectId) // [{ path, content }, ...]
const agent = new NanoCodana({
model,
initialFiles: project,
onFilesChange: (changes) => {
// Fires for every file the agent creates, edits, or deletes.
for (const { path, content } of changes) {
content === undefined ? db.deleteFile(projectId, path) : db.saveFile(projectId, path, content)
}
},
})Too big to hydrate? Load files lazily#
When a project is too large to pull in full, give a file's content a function instead of a string. List the paths cheaply up front; each file's bytes are fetched on first read — sync or async — and cached. A write replaces the provider before it ever runs.
const paths = await db.listPaths(projectId) // cheap: names only, no content
const agent = new NanoCodana({
model,
initialFiles: paths.map((path) => ({
path,
content: () => db.readFile(projectId, path), // fetched on first read
})),
onFilesChange: persist, // writes/deletes still flow back to your store
})Runnable example: apps/serverless — a function that hydrates a project from a database per invocation and persists the agent's edits back.
Ready-made filesystems from just-bash#
fs takes any IFileSystem, and just-bash ships four backends that satisfy it. Most of the time you don't implement anything — you install it, pick one, and pass it:
| Backend | What it is |
|---|---|
| InMemoryFs | Pure in-memory (core's default). Fast, ephemeral, supports the lazy providers above. |
| ReadWriteFs | Direct read-write access to a real directory (what NodeAgent uses). Point it at a workspace, never at trusted code. |
| OverlayFs | Copy-on-write over a real directory: reads come from disk, writes stay in memory. Let an agent explore a real repo without ever touching it. |
| MountableFs | A unified namespace combining backends at mount points — e.g. read-only knowledge at /mnt, a read-write workspace at /home. |
import { OverlayFs } from 'just-bash'
// Reads fall through to the real repo; writes are captured in memory —
// the agent works over a live copy it can't corrupt.
const fs = new OverlayFs({ root: '/path/to/repo' })
const agent = new NanoCodana({ model, fs })just-bash is a direct dependency of yours, not a transitive one — @nanocodana/core vendors a Node-free build of the shell and doesn't depend on the package at runtime. Run npm install just-bash to use these backends.Note that
OverlayFs, ReadWriteFs and the Sandbox need node:fs, so they are Node-only. In a browser or on an edge runtime, use InMemoryFs (core's default) or your own IFileSystem.The full contract: IFileSystem#
If none of those fit — reads and writes should go straight to your backend with no in-memory copy — implement IFileSystem yourself (exported as a type from @nanocodana/core) and pass it as fs. It's a POSIX-flavored async contract:
| Group | Methods |
|---|---|
| content | readFile · readFileBuffer · writeFile · appendFile |
| structure | mkdir · readdir · readdirWithFileTypes? · rm · cp · mv |
| metadata | stat · lstat · exists · chmod · utimes |
| links | symlink · link · readlink · realpath |
| paths | resolvePath · getAllPaths |
Reference implementations to crib from: core's MemoryFileSystem (in-memory), and just-bash's ReadWriteFs(real disk — what the Node adapter uses). Your editor's jump-to-definition on IFileSystem shows every signature with JSDoc.
Or bring a sandbox#
Pass sandbox instead of fs when you have an execution environment — the agent routes both file operations and shell commands through it. This is the integration point for remote sandboxes and microVMs.
What you inherit for free#
Everything documented in the Concepts section works identically on core: streaming, approval gating, skills, image generation, MCP, and per-call overrides.
Size#
A browser app loads 182 kB gzipped up front, with the shell deferred to its own chunk — +338 kB on the first Bash call, and never fetched if the agent doesn't shell out. Measured on a real app.
On disk, node_modules holds 59 MB — essentially all AI SDK, and none of it ships: your bundler takes only what you import. The shell is vendored in as one pre-built file, so just-bash and its ~78 MB of runtimes never enter your tree at all. To drop the shell from the build rather than defer it, import @nanocodana/core/no-bash — on a Cloudflare Worker that's 854 KiB → 448 KiB gzipped. virtualBash: false won't do it: no bundler can eliminate a reachable dynamic import on a runtime flag.