The entire agent is a fenced code block.
A 4B model running on your GPU has no function-calling API, no constrained-decoding mode and no vendor SDK. So the harness that makes it act — read files, edit code, generate whole apps, schedule tasks — is about as small as a harness can be: one fenced block, a six-step loop, and a context manager that is ruthless about what the model gets to remember. Here's the whole thing, and why every part of it is small on purpose.
The protocol: one block, identical for every provider
The model is told: if you want to use a tool, reply with only this.
```tool
{"tool": "code.search", "args": {"q": "def promedio"}}
```
That's it. That's the protocol. The harness looks for that block, JSON.parses it, runs the tool, and feeds the result back as the next turn. The same block works with Gemma on WebGPU, with the 1.2B ONNX fallback, with OpenAI, with Anthropic and with a local Ollama — because it doesn't depend on anybody's function-calling implementation. It depends on a model's ability to emit fenced JSON, which is the one thing every instruction-tuned model on Earth has been trained to do.
There is a second block type, and it is the reason Claw exists at all: if the answer is an app, the model replies with a complete HTML document, and the app appears in a sandboxed iframe next to the chat. Generating a program and rendering it is the same code path as answering a question.
The loop, in full
The whole agentic core is: model → is there a tool block? → run it → append the result → ask again. With one guard rail that matters more than any of the rest:
const MAX_STEPS = 6;
Six. A small model that has taken six actions without producing an answer is not one step from success — it's in a loop, usually re-reading the same file or re-running the same search. Capping the loop turns "the tab hangs and the fans spin up" into "here's what I found, and here's where I stopped." The system prompt reinforces it in plain words: don't repeat the same tool; if the app is created, it's done — answer and stop.
The context manager: ACE-lite
Here's the constraint that shapes everything above it. Depending on the machine, the model gets between 4k and 32k tokens of context — and the small fallback works best with a 5,000-token budget, roughly a fifth of what a hosted model would casually accept. Long histories don't just cost money here; they make the model measurably dumber and slower.
So history isn't a list you append to. It's a budget you spend, re-decided on every turn:
| Rule | Why |
|---|---|
| The last 6 messages always survive | Recency is never wrong in a chat. This is the floor, not the strategy. |
| Older messages compete for what's left, scored BM25-lite + IDF against the current query | The message about the vault from twenty turns ago should come back when you say "vault" — and stay evicted when you don't. |
| No single message may exceed 12,000 characters, truncated in the middle | One pasted README shouldn't consume the whole budget. Head and tail matter; the middle of a file rarely does. |
| Evicted messages leave a visible marker ("…N older messages omitted") | A model that knows something is missing asks. A model that thinks it saw everything invents. |
This is a port of an attention-based context-eviction technique down to a place where the attentions aren't reachable: in a browser, you can't inspect what the model attended to, so relevance is approximated lexically at the message level. Cruder — and it fits in a file you can read in one sitting.
Why the agent lives in the prompt and not in the weights
We did heal our own agentic Gemma-4 E4B. It doesn't load in the browser: the runtime expects the artisan packaging and our export is prefill_decode. Rather than block the product on a conversion, the decision was written into the code as a dated comment: the brain is Google's base build, the agentic behaviour is the system prompt.
That turned out to be more than a workaround. Prompt-level agency is portable: the same instructions make a 1.2B fallback, a 4B Gemma and a frontier API model behave like the same product, with the same tools and the same protocol. Weight-level agency would have been better in exactly one configuration and useless in the other four.
What else the harness carries
- Permissions per scope. Files, apps, vault, tasks, internet — each asks the first time and is revocable in a panel. The model never gets a capability the user hasn't granted; the tool layer refuses before the tool runs.
- Memory. Facts you tell it to keep are stored locally and injected into the context of every turn — a small, permanent block that survives eviction because it isn't part of the history.
- A live system snapshot. Time, language, open folder, current file. Small models hallucinate context constantly; telling them the truth up front is cheaper than correcting them later.
- Skills. User-authored instruction bundles the agent can install and reuse — a way to teach the product a workflow without touching its code.
- Language. The prompt pins the reply language to the browser's, across fourteen languages. A local-first tool that only speaks English isn't local to most of the planet.
The honest edges
- Fenced JSON is not a grammar. Sometimes a small model emits a block with a missing brace, and the parser has to be forgiving. Constrained decoding would fix this properly; it isn't available at this layer yet.
- Six steps is a real ceiling. Genuinely multi-step refactors don't fit. That's a deliberate trade: a fast honest stop beats a slow confident mess.
- Lexical relevance is a proxy. BM25 doesn't understand that "the thing we discussed" refers to the vault. When it evicts something it shouldn't, the marker is what saves the answer.
Harness source: js/agent.js, js/context.js, js/permissions.js, js/skills.js in elffuss-claw and elffuss-code. Previous: the runtime. Next: your machine.
Update (Aug 2026): I finally measured this limitation instead of just naming it. The engine described on this page recovered 15 of every 100 facts it was later asked for; textbook BM25 recovers 65. The rules table above was the problem, not the fix. → no. 16.