forked niri forked from okami.mom/niri

README.md

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 (also default) configures services.nano. It writes services.nano.settings and each entry of services.nano.agents straight to JSON. With no agents it runs one standalone worker.
  • nixosModules.nano-executor configures services.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-auth in open (the default) or local-open mode.

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, and job_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 = true adds 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 through agent_send and agent_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)