The world's first hybrid local-and-cloud agent harness you'll find addictive.
Most agent harnesses buy every part of the loop from the same expensive vendor. Deciding what to do next, compressing a 3,000-line test log, and gating a risky command are treated as one job, priced as one job, and sent to one model.
FE!N splits the loop into slots and lets you bind a different model to each — so a frontier model does the thinking while a 3B model on your laptop does the reading. TypeScript, zero runtime dependencies, 189 tests.
import { Agent, Router, AnthropicPort, OllamaPort, defaultTools } from "fein";
const cloud = new AnthropicPort({ id: "cloud", model: "claude-sonnet-5",
apiKey: process.env.ANTHROPIC_API_KEY,
costPerMTokIn: 3, costPerMTokOut: 15 });
const local = new OllamaPort({ id: "local", model: "qwen2.5:3b" });
const router = new Router()
.bind("think", cloud) // decides what happens
.bind("observe", local, { fallbacks: [cloud] }) // compresses observations
.bind("verify", cloud); // gates subagent mutations
await new Agent({ router, tools: defaultTools() }).run("Why is the test suite failing?");The loop code does not change when you rebind. Same harness, all-cloud, all-local, or any mixture — with a ledger that tells you what the difference actually cost.
No API key, no GPU, no network — every model in the demo is scripted, so what you're watching is the harness:
npm install && npm run demobindings
think cloud/sonnet-sim [cloud]
observe local/qwen3b-sim [local] -> cloud/sonnet-sim
[2] think · cloud/sonnet-sim cloud
A TypeScript project. Running the test suite to find the failure.
tool shell(command: "npm test") via think
ok $ npm test ok 1 - unit/parser handles case 1 …
digest shell: 3100 → 43 tok (99% smaller · local/qwen3b-sim)
cache: prefix stable — 3 msg reused, 2 new
ledger
calls 4 · $0.0024 · 0.2s
local 1 calls $0.0000
cloud 3 calls $0.0024
cache hit 10.1% saved $0.0011
The think model decided to run npm test itself — its authority is untouched —
but it never saw the 330-line log. A local model compressed it to 43 tokens
first. That saving compounds over every remaining turn, it reclaims context
window, and the raw log never left the machine.
| Slot | Job | Why it's separable |
|---|---|---|
think |
Decide what happens next | The hard reasoning. Keep it frontier. |
observe |
Compress bulky output before the think model sees it | Output smaller than input; the saving compounds; the raw data never leaves the machine |
verify |
Gate a subagent's world-changing calls | Rare, so it can afford to be expensive |
title |
Name the session | Trivial |
execute |
Drive a delegated sub-task end to end | The light tier of plan-execute delegation. Unbound by default — an unbound slot advertises nothing |
advise |
One consultation when the driver is stuck | Remote guidance, local control: the advisor never gets tools, only returns text. Unbound by default |
The names are ReAct's. Thought → think, which also emits the Action — native
tool calling fuses thought and action into one completion, and there is no
action slot because tools are executed by code, not a model. Observation →
observe. verify and title are control-plane roles ReAct doesn't have.
Any slot takes any model. Every slot takes a fallback chain, so a dead local runtime degrades that slot to the cloud rather than taking the session down. A port that fails twice in a row is demoted behind its alternates and probed every few calls — counted in calls, not wall-clock, so replays stay honest — which spares every subsequent call the doomed connection attempt.
Bindings are static by default; adaptive routing is opt-in per slot. A binding can carry a policy that switches on facts the loop reports, never on wall-clock or luck — so the same transcript re-derives the same decisions, and every decision lands in the trace and the ledger:
escalate-on-stuck(forthink): the loop guard notices the model going in circles → same port, higher thinking effort. Never a mid-epoch port swap: prompt caches are keyed per model, and one model's signed reasoning blocks are a provider error when replayed to another. WithrestartTo, the ladder gains a top: once every rung is spent, the policy requests an early compaction and the new epoch restarts on the stronger port — the one boundary where a think-model swap is free, because the summary restart pays the cache cost anyway and the lens replays no reasoning across an epoch. The switch reads only epoch-frozen facts, so it provably cannot flip mid-epoch, and the old model writes the hand-off summary (it reads its own context at the cached rate).escalate-on-reject(forobserve): a local digest that fails the quality gate gets one retry on the cloud port. The observe slot's calls carry a fresh, small context each time, so this swap has no cache stake at all.right-size(side slots only): trivially small requests go to the small model even when the slot's default is big.
The one safe place to re-point think itself is a subagent boundary —
fresh context, nothing to break. Two ways to use it:
- Static: set
subagents.thinkSlotand every child runs on that binding. - Plan-execute (bind the
executeslot to enable): the spawn tool gains atierparameter the think model fills per step —"light"runs the whole sub-task on the execute binding,"heavy"on the think model's own. Anacceptanceparameter makes the planner state what "done" means, and the child reports against it. The per-spawn choice beatsthinkSlotconfig, which beats inheritance.
Two lessons from everyone who tried this are enforced in code, not prose. A
cheap model that starts going in circles rarely recovers, so a light-tier
child stops on the first guard fire and reports what blocked it — the
planner escalates, re-splits, or does the step itself; the harness never
re-routes on its own. And delegation quality comes from decomposition, not
from the model's raw judgment, so the tier guidance lives in the tool schema
and a frozen prompt section that only exists when execute is bound —
otherwise the whole feature costs zero tokens.
And the inversion: advise — the cheap model drives, the frontier model
consults. Bind advise to a strong model and a stuck driver spends one
bounded, tool-less consultation instead of grinding: the advisor sees the
task, the guard's diagnosis, and a token-capped excerpt, and returns text
that lands in the driver's context as an appended note. The driver keeps
every tool and every decision — remote guidance, local control. Capped at
two consultations per run; a fail-fast light-tier child gets exactly one
reprieve before it bails. localFirstProfile() (or FEIN_PROFILE=local-first)
ships this shape: everything local except advise and verify on the cloud —
the measured trade is a local driver recovering roughly three-fifths of its
gap to the frontier for a fraction of the frontier's cost.
There was another slot and we deleted it. A toolformer turned the think
model's one-line intent into concrete tool arguments. Measured, it cost +11 to
+15 think-model output tokens on every call and saved zero — the intent has to carry
the arguments verbatim, so it is structurally a superset of what it replaces.
The write-up with numbers is in DESIGN.md. The lesson:
delegate a stage only when the delegate can produce more than it was given, or
knows something the caller does not.
Not competitors — they differ in how much control you surrender:
| Unit | Fixed overhead | What you give up | |
|---|---|---|---|
| Subagent | A whole task | ~600–900 tok/spawn, fresh context, cold cache | Every intermediate decision |
| Slot | One stage of one decision | ~150 tok | Nothing |
Reading forty files to find one symbol → subagent. "Run this exact command and its output is 20k tokens" → slot; you cannot hand that to a subagent without also handing over the choice of command. A few tool calls you could make yourself → neither.
Hybrid execution creates a hazard pure-cloud harnesses don't have: it is very easy to save tokens in a way that costs more than it saves, by rewriting history the provider had already cached. FE!N treats prefix stability as an invariant, not an aspiration:
- Render monotonicity — every render strictly extends the last.
PrefixGuardhashes each render and reports a break the moment one happens, attributed to the slot that caused it. Cache misses become reproducible bugs, not a bill. - Checked prompt sections — the system prompt is assembled from named parts
with declared volatility, and
SectionGuardcatches a "frozen" section that changed.PrefixGuardsays the prefix broke at message 4;SectionGuardsays theidentitysection changed between turns. The second is actionable. - Lookback-aware anchors — a breakpoint reaches back only 20 content blocks, and one turn with six parallel tool calls is thirteen. Two such turns put the previous anchor out of reach and you pay full price forever, silently.
- Append instead of edit —
registerDeferred+surfaceTool()add a tool mid-session without touching the tool block;injectContext()adds operator context as a system-role message rather than editing the system prompt. - Epochs, not sliding windows — dropping old messages shifts every token after them and misses on every subsequent turn, forever.
- Ordered concurrency — parallel tool results append in call order, never completion order, so the transcript never depends on machine timing.
Two mechanisms, deliberately layered, because the free one should run first.
Spill (model-free): oversized tool output is written to .fein/spill/ and
replaced with a head/tail preview plus a path the model can grep. Lossless,
idempotent, never exceeds its cap, never grows.
Digest (one inference): a local model compresses the full text semantically.
They are complementary, and the fixture proves it — a 332-line log with the one
failure on line 241: the preview misses it, the observe model finds it. So both
run, and the lens prefers digest → preview → raw. Spill also fixes the
observe model's worst property: a summary that dropped a detail now has a route back
to the source.
Digestion is chunked to the observe model's context window, and the chunk cap is locality-aware — a local observe model reads 16 chunks (marginal cost is wall-clock), a cloud one declines chunked work outright, because spill already bounded the damage for free. That constant is the hybrid argument in miniature.
A local model can drive, not just assist. ReactPort wraps any text-only
model and presents a native tool-calling interface, so the loop never learns
ReAct exists — it moves tools into the prompt, rewrites history into the
Thought/Action/Observation transcript the model speaks, stops generation before
the model can invent its own Observation:, and repairs malformed output
locally.
That last point is the classic ReAct failure and it is silent: left alone, a
model will happily write Observation: the file contains… and reason about a
result no tool produced. The fix is mechanical — a stop sequence — not a polite
request.
Type while it works. Your line lands at the next turn boundary, never
mid-turn: injecting between an Action and its Observation would hand the model a
user message where a tool result belongs. A second concurrent run() is refused,
because two writers interleaving on the transcript make message order depend on
scheduling — which breaks the cache intermittently and undebuggably.
A ReAct loop rarely fails by crashing. It fails by continuing — calling the same tool, getting the same answer, reasoning about it again. Every turn looks reasonable; only the sequence is insane, and the model cannot see its own loop from the inside.
LoopGuard catches repeats, oscillation (A→B→A→B), and stalling. The
discriminator is same call, same result — repeating a call whose answer
changed is legitimate (polling a build, retrying a flake), so it never fires on
real work. Each problem warns once; a guard that repeats itself is another loop.
Running out of turns forces a real answer with no tools offered rather than returning a leftover fragment. Removing the capability is a guarantee; asking is a request.
All discovered from the workspace — nothing requires a config file.
Durable sessions (node:sqlite, no dependency). fein chat --resume <id>
replays them. Compaction is a fork: the epoch spawns a child seeded by the
summary, the parent keeps every event, the link is recorded. "Compacted" means
relocated, not lost. A session interrupted between a tool call and its result
is repaired on resume — otherwise it is not merely degraded but permanently
unresumable, since every provider rejects an unanswered tool call.
Recall — FTS5 search across every prior session, exposed as session_search
rather than auto-injected behind the model's back. Tool output is deliberately
not indexed, so recall returns decisions instead of log lines.
Identity vs. convention — ~/.fein/SOUL.md is who the agent is; it is
yours, so it is trusted. A SOUL.md in the repo is fenced like any project
file, because the trust boundary is who can write the file, not what it's called.
Skills — reusable procedures as Markdown. The index lives in the frozen prompt; bodies load on demand. Loading every body up front burns tokens on unused skills and means writing a skill invalidates every cached conversation.
Hooks — functions and/or executables in .fein/hooks/<event>/. beforeTool
can deny; a hook that can only observe is a logging system, not a safety
mechanism. Observability hooks that throw are ignored; a beforeTool hook that
throws fails closed.
Subagents — depth capped in code, and a SpawnBudget shared by reference
across the whole tree. A per-agent limit is not a limit: breadth^depth growth
measured 40 agents from a "cap" of 3.
Scheduled jobs — durable POSIX cron under the same permission machinery as
interactive work, read-only unless you pass --write. No backfill: a laptop
closed overnight wakes to zero pending runs, not eleven.
fein chat [--resume <id>] fein run "<prompt>" fein demo
fein sessions list | show <id> | search <q> | lineage <id>
fein skills list | show <name> fein hooks
fein cron list | add | rm | enable | disable | runs | run | serve~/.fein/SOUL.md who the agent is (trusted, tier 1)
.fein/sessions.db .fein/jobs.db durable sessions + scheduled jobs
.fein/skills/ .fein/hooks/<event>/ skills + lifecycle hooks
.fein/spill/ bulky tool output, retrievable
AGENTS.md | CLAUDE.md | SOUL.md project context (fenced, tier 2)
src/
core/ types · transcript (append-only log) · loop · guards · steering
context/ lens + PrefixGuard · spill · repair
models/ router · react-port · providers/{anthropic,openai,ollama,scripted}
steps/ observe · verify · subagent · react · prompts · sections
tools/ registry · builtin · edit/glob/grep
cache/ limits (breakpoints, lookback, minimums) · keeper
session/ store (SQLite+FTS5) · persist · search-tool
skills/ hooks/ schedule/ telemetry/ledger
config/ profiles · workspace cli/ bench/
See ARCHITECTURE.md for why these boundaries, and DESIGN.md for the reasoning behind each rule — including an honest list of what is still unsolved.
npm test # 189 tests
npm run bench # offline, deterministic, free — mechanism cost
npm run bench:live # real models — the correctness questionThe benchmark prices each mechanism against a control on tasks chosen so each has a case it should win and a case where it can only cost. Measured: the observe model is 88% cheaper on its case, 43% more expensive where it cannot help, netting −58% across four tasks. It paid for itself immediately by catching a bug where the observe model ran, billed, and had its output silently discarded.
Requires Node ≥ 22.5 (for built-in node:sqlite).
FE!N was built after reading four open-source harnesses side by side. All are MIT-licensed. No code was copied — the value was in design decisions, and every adoption is a fresh implementation with its own invariants and tests. COMPARISON.md documents what was taken, what was declined, and what survived contact unchanged.
- deepseek-harness — "everything is a plugin." Taught us spill (bounded preview + retrieval locator, model-free), model-free result pruning, loop-hygiene guards, and the principle that a canonical order matters because it is a cache prefix.
- pi — layered agent packages. Taught us the turn as a first-class concept (one assistant response plus its tool calls) and a nested event taxonomy.
- nanobot — a deliberately small, readable core. Taught us steering (mid-turn message injection via a queue rather than a racing second run), typed turns, and the defensive passes that make a persisted history safe to replay — which surfaced a real bug where an interrupted session was permanently unresumable.
- hermes-agent — sessions as infrastructure, deep context engineering. Taught us named prompt sections (which turned our own headline invariant from a convention into a checked one), a rotation-stable cache scope derived from the compaction lineage root, using the provider's real reported usage instead of a character estimate, and bounded error-body reads.
Also informed by the published behavior of Claude Code and Codex, and by
Anthropic's prompt-caching documentation for the breakpoint, lookback, TTL, and
minimum-prefix rules encoded in src/cache/limits.ts.
MIT © Ziboyan Wang