01 processes, not plugins
No dlopen, no ABI borders, no image rot. A component is a process —
teardown is exit(), and the OS is the perfect disposer.
A component crash does not take core or its peers down.
Niffler is an extremely modular, self-extending agent harness. Components are processes in any language, the bus is NATS, and the agent writes, compiles and spawns its own tools while you talk to it.
v0.2.0 — the bus now runs on our pure-Nim NATS client (no libnats to install); the release ships the niffler-tui terminal client alongside the desktop UI
./var/bin/niffler in a terminal is the admin shell — help/status/catalog/tools/sessions, no chat. Conversations live in niffler-ui and niffler-tui; scripting goes through ./var/bin/cli.
$ make && ui/build/bin/niffler-ui
build core + components + desktop UI · the UI autostarts the harness
→ home bus: claimed nats://127.0.0.1:4222 · root ~/niffler @ 8094748
→ core listening on svc.core.call
→ catalog: store, bash, builder, plugins, skills, systemprompt, fetch, models, provider, llm, grep, mcp, edit, git, observe, logfile, agent, expert, fabric
$ ▌
that terminal is the admin shell (no chat) — conversations live in niffler-ui and niffler-tui
No dlopen, no ABI borders, no image rot. A component is a process —
teardown is exit(), and the OS is the perfect disposer.
A component crash does not take core or its peers down.
Core speaks exactly one wire format: JSON envelopes over NATS.
The codec is ~200 lines and pure std/json — SDKs in Nim,
Go, TypeScript, or whatever you port next.
The agent writes source → calls builder.build → calls
core.spawn → the tool is live. Adding a capability is a
tool call, and the LLM does it to itself mid-conversation. Community
packages install the same way: plugin_install clones,
compiles from source and spawns — approval-gated, always built from
the published code. fabric goes further: the LLM programs
whole control flows in an embedded Nim VM. External MCP servers join
the same bus: the mcp manager spawns one bridge process
per server, and its tools appear as ordinary catalog tools.
Capabilities survive restarts: spawned components are recorded in the
store and restored on boot. The repo holds the reproducible source;
var/bin/ can be rebuilt, while the store engine holds
persistent state (var/barrel-db by default — SQLite or
TiDB via NIF_STORE_BACKEND).
one conversation = one process: the system spawns a
var/bin/session <id> runner per conversation — kill a
runner, every other session keeps going.
every box is a small binary using its language's component SDK —
peers, isolated, individually killable.
comp.tool:
proc weather(city: string): JsonNode =
## Current weather for a city
## - city: the city name
builder.build {
lang: "nim",
source: ... }
core.spawn {
name: "weather",
binary: ... }
{"tool": "weather",
"args": {"city": "Berlin"}}
10 sympy instances, one-shot, graded by the official
swebench 4.1.0 Docker harness — niffler, pi and opencode
side by side over two models. Niffler resolves fewer tasks, at
2.3–2.9× less total tokens per task. Full report:
bench/reports/swe-sympy10-pilot-report.md.
A harness-comparison framework — thirty red-at-base
tasks (17 core, 10 mid-tier targeting 2–10 min/cell, and a 3-task
fan-out tier whose mechanical work varies per item or carries
oversized intermediates — the shapes where guest programs and
scripted bulk work earn their keep) tagged by
kind (general / fabric / expert / selfextend), each verified
green by a reference implementation, with per-turn feedback
loops, protected-file guards and provider token accounting —
niffler / pi / opencode / codewhale × two models, a
--thinking low|max matrix, plus a paired
niffler-expert variant that measures the advisory peer. SWE-bench
Verified and Datacurve's DeepSWE (113 long-horizon tasks) run as
additional benchmarks; bench/container ships a Docker
job image and bench/launch.mjs is a guided launcher.
The full 17-task matrix ran green across all lanes (340/340);
the mid-tier set calibrated 20/20 (niffler vs pi, GLM low); on
the syn-large model (Synthetic's GLM-5.3-Flash) the suite is
green in every cell — 27/27 on both the niffler and pi lanes at
thinking=low
(bench/reports/full27-syn-large-low-report.md), and
the post-diet trimmed-prefix runs stay 27/27 while uncached input
drops ~40% at high effort
(bench/reports/full27-syn-large-{low,high}-trimmed-report.md).
$ git clone git@github.com:gokr/niffler.git && cd niffler
$ make setup # prerequisites (Ubuntu / macOS)
$ make # build core + components + desktop UI, once
$ make install # niffler / niffler-cli / niffler-console on PATH (+ niffler-tui on request)
$ niffler-tui # terminal chat — boots the harness on demand if none runs
$ ui/build/bin/niffler-ui # the desktop UI — autostarts the harness
$ make ui-install # optional: puts niffler-ui on PATH + launcher/app icon (Linux)
$ make test # bus-contract suite — one test per component + smoke + go tests
│
└─ make run / recover / down / ram / uninstall / dev · see docs/MANUAL.md
all Nim deps come from nimble — yaml, htmlparser, natswrapper, bitbarrel — installed automatically on first build.
a harness claims its home bus (.env's NIF_NATS_URL, default nats://127.0.0.1:4222) only when the answering core serves this root — every catalog response carries root + gitHash — and yields loudly to a foreign one. Dev clones set NIF_NATS_SPAWN=1 for an isolated random-port bus, and no child (component, nats-server) outlives its harness.
{
"v": 1,
"id": "01J…",
"op": "call",
"svc": "bash",
"tool": "exec",
"args": { "cmd": "uname -a" },
"meta": { "sessionId": "s-01J…" }
}
core → svc.core.call, components → svc.<name>.call,
events on ev.*. that's the whole bus contract.
spec: docs/WIRE.md · rationale: docs/research/REBOOT.md