A harness that builds
itself out — mid-conversation.

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

any languagenimgotypescriptnatssvelte 5
niffler-ui — the desktop chat interface
niffler-uithe desktop chat UI: sessions, streaming tokens, tool runs, approvals, model controls
niffler-tui — the terminal chat client
niffler-tuithe terminal chat client (a plugin — the installed niffler-tui wrapper boots the harness on demand)

./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.

gokr@dev: ~/niffler

$ 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

# why

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.

02 one protocol

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.

03 self-extension

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.

04 persistence of shape

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).

# architecture

core (nim) conversation loop · supervisor
catalog · dispatch

one conversation = one process: the system spawns a var/bin/session <id> runner per conversation — kill a runner, every other session keeps going.

svc.core.call · ev.* · NATS
bash
nim sdk
builder
nim sdk
store
nim sdk
plugins
nim sdk
skills
nim sdk
systemprompt
nim sdk
fetch
nim sdk
models
go sdk
provider
go sdk
llm
go sdk
mcp
go sdk
edit
nim sdk
grep
nim sdk
git
nim sdk
observe
nim sdk
logfile
nim sdk
agent
nim sdk
expert
nim sdk
fabric
nim sdk
your tool
any language

every box is a small binary using its language's component SDK —
peers, isolated, individually killable.

# the self-extension loop

write
comp.tool:
  proc weather(city: string): JsonNode =
    ## Current weather for a city
    ## - city: the city name
build
builder.build {
  lang: "nim",
  source: ... }
spawn
core.spawn {
  name: "weather",
  binary: ... }
call
{"tool": "weather",
 "args": {"city": "Berlin"}}

# measured, not claimed

01 SWE-bench Verified pilot

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.

02 harness bench

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).

# quickstart

terminal

$ 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

requirements

  • nim 2.x
  • go
  • nats-server (built from source)
  • node / npm
  • wails cli (UI only)
  • trafilatura (optional, richer HTML extraction)

all Nim deps come from nimble — yaml, htmlparser, natswrapper, bitbarrel — installed automatically on first build.

one clone = one instance

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.

# one wire to rule them

envelope.json
{
  "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