A resident, all-in-one agent-tool orchestrator. read, write, edit,
bash, and grep are served by one warm, long-lived engine that bundles a file
cache, a directory-walk cache, and a warm-shell pool β all shared across every
tool and reachable from native Rust, from Node.js (napi-rs), and over a
Unix-socket daemon + CLI.
Built in Rust, inspired by the corsa-bind orchestration model and the vize_carton performance substrate.
The six tools an agent leans on most, behind one engine:
| Tool | What it does |
|---|---|
read |
Windowed file reads served from the warm cache (line offset/limit, line numbers, binary-safe bytes, two line-window conventions). |
write |
Crash-safe atomic writes, or fs.writeFile-compatible in-place ones; symlinks are written through, not replaced. |
edit |
One exact replacement, or a batch of disjoint ones applied atomically β matched against the original file, preserving BOM and CRLF, with diff hunks in the result. |
bash |
Shell commands with ordered output streaming, configurable shell, timeout + process-group kill, and an opt-in warm-shell pool with at-most-once semantics. |
grep |
ripgrep-grade search (grep-searcher + grep-regex) over a cached walk and cached file bytes, with a deterministic global match limit. |
graph |
Cached symbols, outlines, search, definitions, dependency/reverse-dependency traversal (deps/rdeps), bidirectional neighborhoods, and index status. |
Every operation is cancellable: pass an AbortSignal and the native work stops
at its next safe point, with nothing left running once the promise settles.
Three surfaces, one core:
- Native Rust β
hearth_tools::{read,write,edit,bash,grep,graph}(&Engine, ¶ms). - Node.js β
@hearthdev/napi'sHearthEngineclass (typed sync + cancellable async methods, streamingbash). - Daemon + CLI β
hearthd(a resident server) and the thinhearthclient, talking length-prefixed msgpack over a Unix socket.
Full design in docs/ARCHITECTURE.md; full benchmark
methodology in docs/BENCHMARKS.md.
A one-shot tool re-pays its cold costs on every invocation β it re-walks the
tree, re-parses .gitignore, re-opens and re-reads files, re-validates UTF-8,
and spawns a fresh shell. A resident server pays those once and reuses them,
across tools and across calls. That reuse is where β and only where β the speed
comes from.
What that buys, measured (Apple Silicon, --release; see
docs/BENCHMARKS.md for methodology and caveats):
grepbeats ripgrep end-to-end at the CLI: 4.5Γ / 6.5Γ (--trust-cache) on files-with-matches, 5.9Γ on counts, and it wins content mode too. At the engine level (native/napi) it is 9β31Γ the ripgrep engine.readandgrepbeat Node.js and Bunfsunder a fair, sync-vs-sync comparison:read1.1β7.2Γ, areaddir+readFile+regex search 1.3β27Γ.bashgets a resident advantage too: the opt-in warm-shell pool is 3.6Γ faster than spawning a shell per command, while guaranteeing at-most-once execution.editis ~2Γ a naive disk read-replace-write for large files.
Hearth wins where the amortized work it saves exceeds the cost of reaching it. Where it doesn't:
- CLI
read/editof a small file lose tocat/sed. A daemon-client must spawn a process and round-trip a socket; for a trivial op that costs ~as much as a tiny purpose-built tool's entire runtime. We proved it: fd-passing drove the read's marginal cost to ~0 andcatstill won β it's the client's startup floor, not the payload. The read/edit speed win lives in-process (native/napi). writeloses tofs.writeFile. Hearth's write is atomic (crash-safe temp- rename) and cache-coherent, so it does strictly more work; against an equally atomic baseline it is only ~1.1β1.4Γ behind, and that residual gap is inherent to the extra syscalls (temp + rename + stat), not a copy-elision problem.
βββββββββββββββββββββββββ one resident Engine ββββββββββββββββββββββββββ
native Rust βββ€ FileCache β file contents cached, validated by mtime/size β
napi (Node) βββ€ WalkCache β directory walk (+ .gitignore) cached per root β
daemon/CLI βββ€ WarmShells β opt-in pooled shells for bash β
β fs-watch β best-effort proactive invalidation β
β (caches are bounded by an LRU byte budget, so the daemon stays small) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
crates: hearth-proto β hearth-core β hearth-tools β { hearth-daemon, hearth-cli, hearth-napi }
Engine is a cheap Arc-clone handle. The daemon, CLI, and napi addon each
construct exactly one and hold it for their whole lifetime; tools borrow it per
call.
- Cached walk + cached-bytes grep. The directory traversal and
.gitignoreparse happen once (WalkCache); files β€ 4 MiB are searched straight from theFileCache(search_slice), so a repeated search does zeroopen/readsyscalls β only onestatper file for coherence. - UTF-8-validity-cached read. A warm
readis astat, anArcclone, and one copy β validity is validated once and cached, so it skips the re-validation a freshread_to_stringpays. --trust-cache(opt-in). Skips even the per-filestaton warm hits under a single-writer assumption (Hearth owns the workspace); this is the dominant warm-grep cost, so removing it takes warm grep from 1.97 ms β 0.58 ms.- Arena grep sink. Matched lines are appended to one growing buffer + spans,
not a
Stringper line, keeping the parallel search hot path allocation-lean. - Compiled-matcher cache. Regex + glob sets are compiled once per pattern and kept on the engine (via a type-erased extension), so repeated greps don't recompile.
SCM_RIGHTSfd-passing. For CLIread, the client hands the daemon its stdout fd and the daemon writes the cached content straight to it β no payload serialization.- Pipe-based warm-shell pool. Persistent shells with a random 128-bit nonce
marker on both streams and
eval-wrapped commands (fast-fail on incomplete input); subshell-isolated,/dev/nullstdin, timeout kills the group, and any anomaly falls back to a fresh spawn.
- Daemon/CLI: length-prefixed msgpack over a Unix socket, one thread per
connection, engine shared by
Arcclone. - napi: concrete generated TypeScript types at the boundary β no
anyon any tool method. Sync methods plus*Asynctwins that offload to a libuv worker viaAsyncTask(no embedded tokio) and take an optionalAbortSignal, and abashStreamthat delivers ordered output chunks while a command runs. The engine is an explicit object the caller constructs β no hidden global singleton.
- Rust 1.95 (pinned in
rust-toolchain.toml) - Node β₯ 18 and pnpm (for the napi addon)
- Optional for benchmarks:
hyperfine,ripgrep,bun
# Rust workspace (CLI + daemon + core)
cargo build --release
# Node addon (@hearthdev/napi): generates index.js/.d.ts + the native .node
pnpm install
pnpm --filter @hearthdev/napi build # or: build:debugcargo test --workspace --all-targets # unit + contract suites
cargo clippy --workspace --all-targets -- -D warnings
# After building the addon (these all run on Bun too):
pnpm --filter @hearthdev/napi test # the JS contract suite
pnpm --filter @hearthdev/napi run smoke # packaging smoke test
pnpm --filter @hearthdev/napi run test:pi # differential test vs pi's own edit
# implementation; skips if pi is absent
bash scripts/verify-tarball.sh # pack + install + run against that copycargo bench -p hearth-bench # in-process (criterion) micro-benchmarks
bash bench/harness/compare.sh # CLI vs ripgrep / cat / sed (hyperfine)
node bench/harness/node/compare.mjs # fair vs Node fs/promises
bun bench/harness/bun/compare.js # fair vs Bun fsCLI + daemon (repeated calls are warm):
./target/release/hearthd --socket /tmp/hearth.sock --cwd "$PWD" --trust-cache &
./target/release/hearth --socket /tmp/hearth.sock grep -l "TODO" .
./target/release/hearth --socket /tmp/hearth.sock stopThe CLI falls back to an in-process (cold) engine when no daemon is reachable.
Node.js (where read/grep/edit win in-process):
import { HearthEngine } from "@hearthdev/napi";
const eng = new HearthEngine({ cwd: process.cwd(), trustCache: true });
const r = eng.read({ path: "src/main.rs" }); // { content, totalLines, cacheHit }
const b = eng.readBytes({ path: "assets/logo.png" }); // binary-safe Buffer
const controller = new AbortController();
const g = await eng.grepAsync(
{ pattern: "fn ", path: "src", globs: ["*.rs"], maxTotalCount: 100 },
controller.signal,
);
// Several disjoint edits, applied atomically against the original file.
await eng.editBatchAsync({
path: "src/main.rs",
edits: [
{ oldText: "fn old_name", newText: "fn new_name" },
{ oldText: "old_name()", newText: "new_name()" },
],
});
// Output streams while the command runs; a timeout or abort still resolves,
// with the partial output intact.
await eng.bashStream({ command: "cargo build" }, (chunk) =>
process.stdout.write(chunk.text),
);Native Rust:
use hearth_core::Engine;
use hearth_tools::grep;
use hearth_proto::{GrepParams, GrepMode};
let engine = Engine::with_defaults();
let hits = grep(&engine, &GrepParams {
pattern: "fn ".into(), path: "src".into(),
mode: GrepMode::FilesWithMatches, ..Default::default()
})?;| Crate | Role |
|---|---|
hearth-proto |
Shared request/response types (the one contract; serde, camelCase). |
hearth-core |
The resident Engine: the shared caches, warm-shells, and fs-watch. |
hearth-graph |
The I/O-free language registry, symbol extraction, and code-index layer. |
hearth-tools |
The six tools + msgpack transport, built on the engine. |
hearth-daemon |
hearthd β the Unix-socket server. |
hearth-cli |
hearth β the thin client (daemon or inline). |
hearth-napi |
@hearthdev/napi β the Node addon. |
bench |
Corpus generator + criterion benches + CLI/Node/Bun harnesses. |
- The default is always correct; the fast paths (
--trust-cache,--warm-shell) are opt-in and documented with their trade-offs. - Benchmarks are held to a fair standard (sync-vs-sync, path-set equality,
atomicity caveats) β see
docs/BENCHMARKS.mdbefore quoting a number, anddocs/BENCHMARKS.md#what-the-correctness-guarantees-costfor what cancellation, streaming and atomic batch editing actually cost. crates/hearth-napi/index.jsandindex.d.tsare generated but committed: they are the package's public API surface, so a change to them belongs in a diff. CI fails if they drift from the Rust source.- Publishing
@hearthdev/napiis tag-driven; the procedure and the reasoning behind it live in.claude/skills/release-napi/. - Rust edition 2024, functional-leaning style, no hidden global state.