The Agent Loop¶
New in v2.6.0. Whittl used to decide how much a model could do by which backend it was on. Claude got the full tool set and an agentic loop; everyone else got a short, capped loop with a handful of tools. Give the same model the same task in Claude Code and in Whittl, and Claude Code won by a wide margin, because the harness gave the model less.
v2.6.0 rebuilt that harness around one idea: what a model gets is decided by what the model can do, not by who sells it. Every backend sends the same tool set. One record per model decides the loop shape, the round budget, session memory and the tool surface. A model that cannot call tools reliably gets the same agent loop through scripts. Every model can look at the app it built. And the guards that end a run judge what the model does, not which tier it is.
The capability record¶
When you pick a model, Whittl builds a record for it: whether it calls tools natively and reliably, how big its context window is, whether it can see images, and a tier. Where the record comes from:
- OpenRouter publishes capability metadata for every model; the record is read from it.
- Claude, DeepSeek and Gemini on their own backends use a table Whittl ships.
- Ollama models are asked once through the local server, which reports the model's template and window.
The record is built in the background when the backend is selected, so the first request already knows the model. It is also updated by evidence during a session. A model that sends malformed tool calls, or that describes an edit in prose instead of making it, is marked as not reliable and takes the script route below. Research that ends in a question back to you is not counted against it, and neither is an answer in prose from a model that has already made three good tool rounds: often prose is the right answer.
Everything downstream reads the record:
| Decision | Read from the record |
|---|---|
| Loop shape | native tools that are reliable get the multi-turn loop; otherwise code mode or the single-turn loop |
| Round budget | by tier |
| Session memory | reliable native tools and a window of at least 100K tokens |
| Tool surface | a window under 64K tokens, or a low tier, gets a smaller core set of about ten tools instead of the full set |
| Fresh builds | a reliable native model builds through the loop, file by file, running the app as it goes |
| Prompt | a small or weak model is asked for one tool call per turn and whole-file rewrites |
Code mode¶
A model that cannot call tools reliably used to get a text-only path: one shot, no verification, no way to read a file it had not been given. Code mode gives it the same agent loop in a different shape. The model writes a short Python script against a small whittl API, Whittl runs it in a separate process, and only what the script prints comes back:
import whittl
src = whittl.read("main.py")
whittl.edit("main.py", 'BG = "#17181c"', 'BG = "#f5f5f5"')
print(whittl.run(timeout=10))
The API is files, read, search, glob, write, edit, delete, run and tests. Every path is kept inside the project, a script that fails leaves the project exactly as it was, and an edit whose old text does not match reports the file's real text back so the next attempt can copy it. An indented block, pasted inside the call, is understood. With Agent Mode on, a script may also import the project's own modules.
Models with native tool calling get the same thing as one more tool, run_script, so one call can search, read, edit and run where a dozen rounds would have been needed.
Code mode also catches a model in the act. If a model with native tools answers a change request with prose and no tool call, Whittl does not nudge it twice: it switches to scripts for the rest of that request. And if the prose contained the whole file, Whittl applies it directly, with the same archive and safety checks a normal edit gets.
Seeing the app¶
run_code tells the model whether the app started and what it printed. probe_app tells it what is on the window. Whittl builds the app offscreen, walks its widgets, optionally clicks a button or types into a field, and reports every control with its class, name, text, size, position and visibility, plus a note about anything that has no size or sits outside the window:
QLabel#stage "No images loaded" 582x332 at (9,9)
QPushButton "Next" 80x20 at (397,348)
Worth knowing:
- QPushButton "Export" is outside the window (at 2000,20 in a 600x400 window)
That reading is text, so every model gets it, not only the ones with vision. The probe also takes a picture of the window, and Whittl shows up to three of them in the chat at the end of the turn, so you see what the model looked at. PySide6 projects only in this release.
Memory and reasoning¶
A long run used to forget. Every tool result older than three rounds was cut to a hundred characters, so a traceback the model saw in round two was gone before it was fixed. Results are now compacted by budget, a share of the model's window, oldest first, into a line that says what was done: read main.py (348 lines), or for a run, the exit line and the tail of the traceback. The newest rounds stay whole. A turn with no session to resume is told what the previous turn read, changed and ran.
A thinking model used to re-derive its plan after every tool result, because the loop replayed its turn as text and tool calls only. Each backend now carries its own reasoning across a tool round: Claude its thinking blocks, OpenRouter its reasoning details, DeepSeek its reasoning content, Gemini its thought signatures, Ollama its thinking. One provider's reasoning is never sent to another.
One prompt, guards on behaviour¶
Every model gets the same instruction: understand the request, change the code, verify the change. Verify is a step in the workflow, not a suggestion. The prompt names exactly the tools that were sent, so a model is never told about a tool it cannot call.
The guards that end or rescue a run judge what happens, not which model it is:
- No new information. Each round's results are compared with everything the model has already seen. A file not read before, a search with results not seen, a run whose output changed all count as progress. Six rounds that bring nothing new end the run.
- The same call again and again. The same call three times in a row, or three times within six calls, ends the run.
- Edits that do not land. Four edits in a row that fail end the run. Reads do not count, and one edit that lands resets it.
- A reply stuck repeating itself is ended; a model in that state does not recover.
- A reply that is all reasoning. When a round spends its whole output on thinking and produces nothing, it is asked again with the least reasoning the model allows, twice at most, and the chat says so.
- A server error or a dropped connection is retried up to four times, with a pause between tries. A timeout is not retried: the turn ends, everything already changed is kept, and "continue" picks it up.
- The round limit. A run that reaches its limit while still making changes continues automatically for a few more passes while each pass makes progress, and says so in the chat; one that stalls stops and tells you to say "continue".
- Stop stops, immediately, even mid-round.
Reading the chat¶
Each tool call shows as the call and one indented line saying what came back:
→ Looking at the app
5 widgets, 2 worth knowing about, clicked 'Next'
→ Reading main.py
348 lines
→ Running a script
Changes: edited main.py
The full result goes to the model, never to the chat. Every reply is signed with the model that wrote it, and when a provider answers with a different model from the one you picked (DeepSeek's Pro tier served by Flash, or OpenRouter's auto router), the reply is signed with the one that actually answered.
What's next¶
- The AI's Tools: every tool, including
run_scriptandprobe_app - Agent Mode: the
bashtool and the web tools - Think Mode: the reasoning each backend carries