- TypeScript 98%
- Nix 1%
- CSS <1%
- JavaScript <1%
- Docker <1%
- 3 more languages <1%
Nano #
Nano is a harness for long-lived, self-modifying agents. Each agent has its own
identity (soul.md), long-term Markdown memory, a model loop with fallback and
context compaction, and optional Discord presence. A control plane supervises
one worker process per agent and serves a web dashboard. Tools run through a
separate executor process, either on the server or on another machine.
Nano is written in TypeScript on Node.js and assembled from Cordis plugins. Nix is the configuration format; the runtime reads the JSON that Nix generates.
Architecture #
| Process | Entry point | Role |
|---|---|---|
Control plane (nano-server) |
apps/server |
Reads agent files, spawns and supervises workers, proxies /agents/<id>/…, serves the dashboard at /ui/. |
Worker (nano) |
packages/nano-runtime |
One per agent: model loop, memory, context management, Discord, HTTP/RPC API. |
Executor (nano-executor) |
apps/client |
Runs shell, read_file, edit_file, and image_tool for a worker over HTTP. It has no model and no session. |
| Terminal client | apps/tui |
Chats with a worker and mounts your current directory as its tool workspace. See apps/tui/README.md. |
| Web dashboard | apps/web |
Vite app served by the control plane at /ui/. |
Repository layout #
apps/ server, client (executor), tui, web
packages/ SDKs and shared libraries (@nano/runtime-api, @nano/protocol, @nano/config, …)
packages/plugins/ built-in features, one @nano/plugin-* package each
agents/ agent definitions (*.example.* files are templates)
docs/ configuration reference and plugin architecture guide
tests/ repository-level tests
Quick start (source checkout) #
Requires Node.js ^20.19.0 || >=22.12.0, npm 10 or newer, and Nix to
evaluate the configuration.
npm ci
cp config.example.nix config.nix
cp agents/mira.example.nix agents/mira.nix
# Edit both files. Credentials go in a separate JSON secrets file (see secrets.example.json).
nix eval --json --file config.nix > config.json
nix eval --json --file agents/mira.nix > agents/mira.json
npm start -- --config config.json
npm start builds the workspace packages and the web UI, then starts the
control plane. It loads every .json file in agents/ except files ending in
.example.json, and starts one worker for each. Open
http://127.0.0.1:3000/ui/ to see the dashboard.
The bind address and port come from the server section of the config
(default 127.0.0.1:3000); --host and --port override them.
To run a single worker without the control plane:
npm run start:worker -- --config config.json --secrets /path/to/secrets.json
NixOS deployment #
The flake provides the nano package (nano, nano-server, and
nano-executor binaries, with the built UI included) and two modules:
nixosModules.nano(alsodefault) configuresservices.nano. It writesservices.nano.settingsand each entry ofservices.nano.agentsstraight to JSON. With no agents it runs one standalone worker.nixosModules.nano-executorconfiguresservices.nano-executor, a remote tool executor.
services.nano = {
enable = true;
settings = import ./config.nix;
agents.mira = import ./agents/mira.nix;
secretsFile = "/run/agenix/nanoSecrets"; # decrypted contents must be JSON
};
The dashboard is at http://<host>:<services.nano.controlPort>/ui/ (default
port 3000). Every setting is listed in docs/config.md.
Agents #
Each file in agents/ defines one agent. An agent file holds only what the
control plane owns. Everything the agent runs on (models, fallback, tools,
context, Discord, subagents) goes in the runtime config files named by
config. A standalone worker reads the same files with --config.
| Field | Type | Required | Default |
|---|---|---|---|
id |
string matching [a-zA-Z0-9_-]+ |
no | filename without .json |
name |
string | no | id |
port |
integer 1..65535 |
no | control port + 1 + the file's sorted index |
home |
path | no | data/agents/<id> |
client |
local, or an HTTP(S) executor URL |
yes | — |
workspace |
path | no | repository root (used when client = "local") |
config |
path or list of paths | no | — |
secrets |
path | no | — |
settings |
object of string/number/boolean | no | {} |
Relative paths resolve from the repository root. Startup fails on unknown keys,
on a config or secrets file that does not exist, and on two agents that
share an id, port, home, Discord token, or enabled bridge port.
config and secrets are passed to the worker as --config and
--secrets. Config files are deep-merged in list order: objects merge
recursively, and arrays and scalars replace earlier values. The secrets file is
merged last. Agents can therefore share a base file and each add an override
file on top:
config = [ "/etc/nano/config.json" "/etc/nano/agents/mira.json" ];
secrets = "/run/agenix/nanoSecrets"; # JSON contents; the filename may be extensionless
Credentials are passed as files, never as environment variables, because the
agent's shell tool inherits the worker's environment. Keep secrets files at
mode 0600 and out of the Nix store.
settings become the worker's environment, keyed by uppercase names. Only
a few settings still use this mechanism; new behavior belongs in typed JSON
config. Variables the server manages (PORT, HOME, NANO_CONFIG,
NANO_SECRETS, NANO_HOME, the NANO_CLIENT_*, NANO_CONTROL_*,
NANO_TOOL_CLIENT_*, and NANO_WORKER_* prefixes, and similar) are rejected.
The worker currently reads these:
| Setting | Default | Purpose |
|---|---|---|
IMAGE_TOOL_MAX_BYTES |
1000000 |
Largest image the worker's HTTP server accepts (sets its request body limit). |
COOLDOWN_CHANNELS |
— | Per-channel windows when the agent may respond, as repeated channelId,startHHMM,endHHMM triples. |
COOLDOWN_TZ |
system zone | IANA time zone for the cooldown windows. |
DISCORD_EMBEDDING_BACKFILL |
true |
Embed old Discord messages in the background for search. |
DISCORD_EMBEDDING_BATCH_SIZE |
64 |
Messages per backfill batch. |
DISCORD_EMBEDDING_IDLE_MS / DISCORD_EMBEDDING_RETRY_MS |
30000 / 60000 |
Backfill polling and retry intervals. |
DISCORD_SEARCH_CATCHUP_BATCH_SIZE |
16 |
Messages embedded before each search. |
NANO_MIGRATE_LEGACY_STATE |
false |
One-time copy of root-level session snapshots into the agent's state directory. |
ANTIGRAVITY_BRIDGE_ENABLED / _PORT |
— / 8000 |
Bridge declaration; the control plane checks the port is unique. |
CODEX_BRIDGE_ENABLED / _PORT |
— / 8001 |
Same, for the Codex bridge. |
Keep executor endpoints on loopback or a trusted private network.
Memory #
An agent's memory is stored on the server under its home, never on an
attached executor:
| Path | Contents |
|---|---|
<home>/soul.md |
The agent's identity and how it describes itself |
<home>/memories/ |
Long-term Markdown memory: core notes, journals, people files |
<home>/state/ |
Session, rest-snapshot, and failover state |
<home>/artifacts/ |
Durable full tool-output artifacts, with configured secrets redacted |
<home>/nano.db |
Search index, revision history for soul.md and memories/core.md, other durable data |
The Markdown files are the source of truth. The agent manages them itself with
its memory tools (memory:// paths), so to reorganize or repair memory, ask the
agent to do it. Treat soul.md and memories/ as a private diary: do not read
or edit them unless the agent asks. To back up or move an agent, stop its
worker and copy the whole home directory, not just the Markdown.
Tool results larger than 50KiB show a bounded head/tail preview and an
artifact://<id> reference. Use read_file with start_line/end_line to
retrieve omitted output; long-line pages provide a start_column continuation.
Artifacts remain readable after session restarts. Explicit shell max_lines
also preserves the full output in an artifact. Existing shell capture-limit
errors are unchanged.
Interfaces #
Web dashboard #
The control plane serves the dashboard at /ui/. To build it without
starting the server, run npm run build:web; for live reload, run
npm run dev:web.
DeepSeek Harness view #
Each worker serves the DeepSeek Harness web UI, including its mobile shell, as
a live view of the agent's main conversation. Through the control plane it is
at /agents/<agent-id>/dsh/; a standalone worker serves it at /dsh/. The page
runs the published DSH browser bundle against Nano directly, with no separate
DSH backend.
- It shows the main conversation: history, streamed text and reasoning, tool calls grouped by step, and whether a turn is running.
- The composer sends text prompts. Stop interrupts the active turn.
- Reasoning and token usage appear only for replies produced while the page's worker was running. After a worker restart, a stopped turn reloads as completed.
- Browsers cannot send bearer tokens, so this view needs
nano.rpc-authinopen(the default) orlocal-openmode.
Terminal client #
npm run start --workspace @nano/tui -- --control http://127.0.0.1:3000 --agent mira
The TUI routes the agent's tools to your current directory while it is connected. Remote setups and flags are in apps/tui/README.md.
Executor #
npm run start:client # listens on 0.0.0.0:3002; --host and --port override
The executor runs shell, read_file, edit_file, and image_tool from its
own home directory. Set an agent's client to its URL to send that agent's
tools there.
Discord #
The Discord owner (discord.owner_id) has these slash commands:
| Command | Effect |
|---|---|
/model info, /model presets |
Show the current model selection and the configured presets. |
/model swap preset:<name> |
Switch presets by handing off to a fresh session (below). |
/fast [enabled:true|false] |
Toggle priority service tier for Codex models. It applies to later requests, including subagents and summaries, and resets to the config's fast value on restart. |
/interrupt |
Stop the active generation or foreground blocking job and keep the session. |
/discard |
Stop all session work and archive the raw session to memories/discarded-<datetime>.json. |
/compact [force:true] [snapcompact:true] |
Compact context now. force uses the summary model and asks the primary model to journal its play-by-play next turn. snapcompact keeps the dropped span as a bitmap frame. |
memory_grep excludes timestamped discarded-*.json and
frontend-discarded-*.json session archives. They remain available through explicit
memory reads; ordinary JSON memories stay searchable.
Model swaps. The outgoing session is asked to journal (write to
memory://journal/) and call end_session, and a new session then starts on
the chosen preset. Events that arrive during the handoff are queued and replayed
into the new session. If the agent cannot finish in time, or a mutating tool or
compaction will not stop, the runner archives the session or keeps the old
model. The whole swap takes at most about 110 seconds. The chosen preset name is
persisted and looked up again from config on restart. The same swap is available
over HTTP as POST /awp/swap-model. Full lifecycle and failure behavior:
docs/config.md.
Other behavior:
- Shell calls still running after two minutes move to the background. The agent
manages them with
job_list,job_status,job_stdout, andjob_cancel. - The owner gets a DM on a terminal model API error, or when the same transient error happens three times. Alerts are deduplicated for six hours, and a Mute this error forever button silences that error.
frontend.enable = trueadds a second agent inside the worker. It handles every Discord event except the owner's DMs, and shares memory with the main agent but has its own session. It has no executor, skills, or access to the owner's DMs, and it talks to the main agent throughagent_sendandagent_channel. See docs/config.md.
Plugins #
Built-in features are the @nano/plugin-* packages in packages/plugins/.
They use the same public API as third-party plugins: Cordis name, inject,
provide, Config, ctx.plugin, and ctx.effect, plus Nano's typed feature
contracts.
import type { Plugin } from "@nano/runtime-api"
import { nano, registerTool } from "@nano/runtime-api"
import { defineOperation, defineValidator } from "@nano/protocol"
import { registerOperation } from "@nano/protocol/rpc"
Plugins are trusted code that runs in the host process, not in a sandbox. They
must be imported statically into the application. The plugins section of the
config can only enable or configure plugins already composed in; it never loads
code. Import packages only through their declared exports. See the
plugin architecture guide for the API, package
layout, and plugin inventory.
Development #
npm run typecheck # builds packages, then runs tsc across all workspaces
npm test # workspace tests plus tests/*.test.ts
npm run build # packages and web UI
npm run dev # control plane with file watching (dev:worker, dev:client also exist)