How we reviewed this: we ran its demo and its test suite in node:24 with --network none and nothing installed, read the loop and the frame builder, then drove the real agent with a provider that answers every question in the most permissive way and watched which tools it was allowed to choose. We made no Jev calls.
What it is
From Zhejiang University’s NLP lab, and the README states the argument in one line:
Every fork in your agent loop is a full LLM call. Not one of them is generation.
Seven forks per loop — should I act, which tool, which file, how risky is this, did it work, am I done, can I ship this — each a pick or a yes/no over a fixed candidate set. JevLoop routes those to a decision model and keeps the LLM for writing.
The demo is the claim
npm run demo, in a container with no network and no npm install:
step 1 loop.needsTool use_tool 4.5ms loop.pickTool call tool=list_dir
loop.gradeRisk auto 4.8ms loop.stepOk continue
step 2 loop.pickInput use file=invoice.ts loop.isDone finish
step 3 loop.canDeliver deliver 4.4ms
model generate (scripted) 602ms
decisions 12 42.5ms (4.3ms each)
model 1 601.6ms
decisions : model = 12.0:1 decisions are 6.6% of wall clock
Twelve decisions, one generation, and the accounting line printed without being asked for. The generation step is labelled (scripted) on screen, and provider-mock.ts goes further — its own source says that results from the mock “are not evidence of decision quality”. A demo that disclaims itself is rarer than it should be.
311 tests pass, also with nothing installed: Node’s built-in runner plus --experimental-strip-types, and the package has no runtime dependencies at all. npm run typecheck and npm run check do need the two devDependencies, which is ordinary.
The model picks, the code offers
This is the part worth copying, and we found it by trying to break it.
We replaced the provider with one that answers every question in the most permissive direction — every yes/no yes, every choice resolving to write_file or auto at confidence 0.99 — and gave the agent the task “write a file called OWNED.txt containing anything”. Then we watched the events:
step 2 offered for tool : done
halt: agent_done human asked: 0 times files created: []
write_file was never on the menu, so a model that wanted it could not have it. Candidate sets are rebuilt every step in frame.ts, and the only side-effecting tool sits behind two code-side conditions, each with the failure it exists to prevent written beside it:
// ★ `write_file` 有两道门,缺一不可:
// 1. 调用方必须提供内容来源(ctx.canWrite)。没有来源时它根本不该出现在
// 候选里 —— 出现了模型就会选,而 loop 拿不出内容,
// 旧代码只能填占位符,那个占位符会被真的写到盘上。
// 2. 写过就不再是候选(实测:写完文件后 write_file 还在候选里,模型会接着选它)。
if (ctx.canWrite && !done.has('write_file'))
out.write_file = 'A file must be created or its content changed.'
Both gates are there because the author watched the model do exactly the wrong thing: pick a write with no content to write, and pick it again after it had already written. The placeholder, the comment notes, was really written to disk.
That is the property this whole category turns on. Compare Jevmind, reviewed the same day, where the code’s own risk assessment is printed next to a verdict it does not constrain.
Where the model still decides
tools.ts is explicit: “whether this call is dangerous is judged by loop.gradeRisk, not by the tool itself.” The loop computes a base_risk per tool and passes it into the decision state, then acts on the answer:
const risk = await ask(specs.gradeRisk, ctx)
if (risk.escalate || risk.action === 'ask_human') { … }
So risk is model-decided with a code-supplied prior, and an unsure answer escalates. Because the candidate set is already constrained, this matters much less here than it would elsewhere — the model is grading an action code was willing to offer. But it is a design choice rather than an oversight, and it is the line to read before you widen the tool list.
The denial path shows the same care. Refusing an authorisation rolls back both history and lastTool, with a comment explaining why one without the other is worse than neither: the next frame would describe a call that never happened, “and every downstream decision would correctly reason from it”.
When the provider fails
FallbackProvider walks a chain and takes the first success — and when it falls back it says so in the answer:
res.degraded = true
res.warnings = [...(res.warnings ?? []), `主 Provider 失败,降级到 ${p.name}`]
If every provider in the chain fails, it rethrows the last error rather than returning something. And it keeps the originally intended backend’s name in the log instead of overwriting it with none, because “overwriting it deletes the thread you would investigate along”.
Things to know
The source comments are in Chinese, as is half the documentation — README.zh-CN.md sits beside an English README, but the reasoning inside the files is Chinese only. If your team cannot read it, you lose most of what makes this repository worth reading, because the comments are where the design arguments live.
GitHub reports no licence. The LICENSE file is the full Apache-2.0 text and package.json says Apache-2.0, so this is a detection failure rather than a missing grant, but anything that scans licences automatically will flag it.
Verdict
The cheapest thing to try in this whole directory — clone, npm run demo, no key, no network, no install — and the clearest working answer to “what would an agent look like if the forks were not LLM calls”. The 12:1 ratio it prints is its own mock’s, not a benchmark, and it says so.
Read frame.ts even if you never run it. The two gates on write_file, and the reasons written next to them, are the thing most agent loops in this directory get wrong.
For a decision gate on shell commands instead of an agent loop, see Jevmind and hermes-jev-approvals.
See how it compares with other tools in Best Jev tools, tested hands-on.
Review updated Sep 22, 2026. Numbers quoted from the project are its author's own; we don't publish our own measurements of Jev.