The problem
The first version of your browser agent — a program that drives a web page on its own — only clicks. Jev picks which numbered element, code clicks it, each step takes 300 ms, and a forty-step task is done in half a minute.
Version two has to type into a search box — and Jev does not write. It picks among the things you offer it; it never hands back a string of your own words. The tempting fix is to swap the whole loop for a large model: it can write, so let it decide where to click while it is there.
Do that and three things get worse at once. Every step goes from 300 ms to 2 s. Every step goes from a hundredth of a cent to a few cents. And what comes back is, again, something no code can verify. One step needed words, and you paid for it with the other sixteen.
The solution
The Ghostwriter pattern says: split every step into a decision and a piece of writing, and only call the writer on the branch that actually needs words.
Jev still answers which operation this step is and which element it applies to. Only when the answer is "type" do you hand the field name and the goal to a small writing model and ask for the words to put in. In a seventeen-step task that usually happens once or twice.
Whatever the writer produces goes through a check: one text field, not empty, not longer than the cap. If it fails or times out, fall back to something dumb — the keyword straight out of the goal — and record which route produced the words. A dead ghostwriter must not kill the task.
Ask · Answer · Act
- AskPut in the goal, the page and the fields you could fill, and ask two things at once: which operation this step is, and, if it is typing, which field.
- AnswerOperation 'type' at 0.88, and the search box at 0.79.
- ActSince it is typing, the ghostwriter is asked for the words, they are checked for being non-empty and short enough, and then typed in.
Where the name comes from
A ghostwriter. The decisions stay yours — who to meet, what to agree to, what to refuse. Only the polished sentences are written by somebody else. The writer cannot overrule you, and if the prose is bad you are still the one who decided.
Use it when
The actions can be listed, but one or two of them need free text: a field value, a commit message, an argument to a tool.
Not when
The whole task is writing (an article, an email) — that is a large model’s job and Jev can at most review it afterwards. Or the words already exist in the material: let Jev pick the passage and have code copy it exactly.
Projects that do this
- jev-ultrafast — Browser agent where Jev picks each operation and element.
- jev-browser — Fast, cheap browser automation with Jev picking each step.
- foreman — Software factory foreman using Jev to supervise coding workers.
Read this code
- jev-ultrafast
jev_ultrafast/model.py:160-198The writer is called only on the typing step, and whatever it writes is checked for shape before it is typed in.
- jev-browser
src/navigate.ts:173-196The writing model runs once or twice in a whole task, about 48 tokens each time; with no API key it falls back to picking a keyword out of the goal.
- jev-browser
lib.ts:123-131The fallback path, and the line in the log that records which route actually produced the words.
Show me the code
const { answers } = await jev(state, {
op: choice("Which operation comes next?", ops),
"type.target": choice("Which field would be typed into?", fields),
});
if (answers.op.choice !== "type") return act(answers); // nothing to write on this step
let text, via = "writer";
try { text = TextSchema.parse(await smallLlm({ field, goal })).text; }
catch { text = keywordFrom(goal); via = "fallback"; } // the writer failed, the decision did not
trace.push({ step, via });
await typeInto(answers["type.target"].choice, text);How jevbooks recognises it
How do we know a project does this? We put one question about its README to Jev and it returns a percentage; the higher, the more it looks like it. If you copy the question, copy one thing: ask whether the text mentions this one concrete thing, never whether the project is good.
{
"decide-generate-split": {
"type": "noul",
"instructions": {
"what": "Does the text describe Jev choosing the action or target while a separate text model writes any free text (a typed value, a message, a tool argument), with Jev never producing text?",
"not_for": "Projects that use only Jev, or a single LLM that both decides and writes."
},
"criteria": {
"true": "The text explicitly describes this. One sentence is enough; other content does not cancel it.",
"false": "The text does not describe this."
}
}
}