Design
September 20, 2026 · View on GitHub
Why a framework
The Jev projects that work (typesafe-mario, jev-plays-pokemon-red, jev-tetris, jev-doom-agent, the 3D room builder, jev-for-chrome) all rebuilt the same machinery: structured small state, code-enumerated candidates, fan-out of Choice/Noul/Score in one request, strict answer validation, a visible fallback, a stall counter fed back as a fact, and a trace. The primitives are trivial to call; the machinery around them is where the effort and the bugs are. jev-dev-kit packages the machinery so a new app only writes its domain part.
The contract an app implements
interface App<S> {
// The world as Jev should see it: small, structured, facts not raw numbers.
observe(): Promise<S>;
encode(state: S, run: RunState): JevState; // task, page/world facts, recent actions, counters
// Decisions. Each is a question; candidates come from `fixed` or from `options`.
decisions: Record<string, {
kind: 'choice' | 'noul' | 'score';
fixed?: Candidate[]; // e.g. the 7 Mario macros
options?: (state: S) => Promise<Candidate[]>; // e.g. elements on the page, legal moves
rules: string | Record<string, string>;
}>;
// Effects. Runs the chosen candidate; may ask for text through the provider.
act(choice: Chosen, state: S, text: TextProvider): Promise<Outcome>;
// Optional. Code-owned steps that need no decision (walk a route, dismiss a dialog).
routine?(state: S): Promise<'handled' | 'ask'>;
}
Candidate = { id, description, ...facts }. Descriptions are consequence-first sentences, not names (Tetris finding: one contradictory description halved the score; richer structured objects made options indistinguishable).
TextProvider = (ctx: { goal, field, state }) => Promise<string>; implementations: a small chat model (DeepSeek, any OpenAI-compatible), a regex/word-highlighting extractor, or a fixed table.
OptionsProvider implementations: a function (most apps), or a language model asked once per new environment to enumerate and describe candidates, cached.
What the runtime does every step
routinefirst. If code can handle the step, Jev is not asked (Pokémon: no questions between the bedroom and the lab).observe→optionsfor each decision →encode→ one request with every question in it, plus two standing cross-checks:goal_doneandstuck(Noul), answered without seeing the main choice.- Validate: chosen id must be an offered candidate and must agree with its own distribution (two-decimal rounding tolerance). Invalid → ask once more → then fallback.
- Veto: DONE below the goal_done threshold or BLOCKED below the stuck threshold is withheld once, with a notice in the next state.
act, then record the worded outcome (page changed,no visible change,navigated to …,error: …) into the history that the nextencodesees. Jev has no memory; the history is the memory.- Loop control: consecutive no-change limit, same-choice-N-times-in-M-steps limit, decision budget, model-call budget. Every stop reason is a code, every fallback is labelled
via: 'fallback'in the trace, never disguised as a decision.
Suspension
A step can stop half-way when a value must come from outside: the text provider (or act) throws NeedsInput(request); runLoop returns status: 'suspended' with the request and a resume(value). Resuming re-runs act for the same chosen candidate with the value answering its first text request; Jev is not asked again. State lives in the closure, so a relay keeps the result object in a session map and calls resume when the client comes back.
Batch mode
Many tasks are not loops but one fan-out: "which of these 200 items belong", "where does each of these go". runBatch(items, decisions) sends all questions in as few requests as the provider allows and returns the distributions. No history, no loop control, just validation and tracing. The 3D room builder is this mode.
Evaluation
jev-dev-kit eval runs a task set and, for each, two controls: shuffled candidate order (should collapse to chance if Jev is doing the work) and a keyword-only picker over the same descriptions (Jev must beat it). Latency, cost and per-step traces are recorded in the same JSON the extension's Copy trace produces.
No providers
The kit never talks to the network. JevClient is a function type; the app supplies it (TypeSafe's SDK, OpenRouter, Cloudflare, a cache, a fixture). The same holds for the text callback: the kit defines TextProvider and the reply format (parseFieldText), the app decides which model or code answers. This keeps the kit's job to one thing: making inputs and outputs valid for Jev.
Consumers
- jev-for-chrome:
observe= content-script snapshot;options= element table;act= trusted input;text= text helper. Itssrc/sharedmoves here. - Anything else: a game, a triage queue, a scene builder.
Non-goals
Free-form generation, planning across many steps, memory beyond the encoded history. If a task needs those, put a language model above jev-dev-kit, not inside it.