0086: Agents hand work to the local model
On this page
Status: accepted
Context
Coding agents reach Short Leash through leash mcp (0062), and Short Leash runs a language model on this computer (0060). An agent that plans a change could hand the writing to that model and review the result, spending less of its own budget on boilerplate. Other apps can already use the model through the endpoint (0063), but only with its key, and with no tools, so an agent can't hand it a change to make. The model is smaller than the agents that would use it, and the files it reads come from a repository, which can steer it.
Decision
- Three MCP tools.
delegate_to_localtakes a branch workspace (0082), a task and two lists of paths:readfor context andwritefor the files the model may change or create.local_workfollows the work, or lists the work Short Leash has kept.stop_local_workstops it.- All three are
NeverInChat: chat runs on the same model.
- Only in a branch workspace. The work writes only in a branch workspace's folder, never a site's own. It's refused when there's no such branch workspace, and while the branch workspace has a job of its own (an add, merge or removal). A merge or removal is refused while the model writes there. The folder must be the worktree Short Leash made for it, as a removal checks: not a link, still Git's record of it, and the site's subfolder reached without links. This is checked when work starts, again under the gate, and again right before saving, so nothing is written to a branch workspace that was removed or swapped for a link while the model worked.
- The model gets no tools and runs nothing. The request has no
toolsand notool_choicekey and turns thinking off, like Explain's (0080). The answer is held to a JSON schema:{files: [{path, content}], notes}, whosepathmust be one ofwrite. Short Leash writes the files. It never stages, commits or runs anything. - Checked before it starts. The task and paths are checked, and the files read, before a job begins, so a refusal returns at once and starts nothing:
- The task is 8,000 characters at most.
- A path is relative and written with
/, with no..,:,\,.gitpart (or a Windows short name for it, such asGIT~1), part ending in a dot or space, name Windows keeps for a device (nul,con.php), or character that hides text. Paths are compared without case, as macOS and Windows do, and two files to write that differ only in case are refused. - The model writes at most 6 files and reads 12 more.
- The model doesn't write the project's own
leash.tomlandleash.local.toml, which decide what Short Leash runs. - Files are UTF-8 and 256 KB at most, and never read or written through a link (
pathx::link_on_the_way; reads open with no-follow). - The model must have room for the prompt and for rewriting the files to write, a fifth more than they are now, and that rewrite must fit the 16,384 tokens the model writes at once (
MAX_ANSWER_TOKENS, about 40 KB of files in all). Tokens are estimated as characters divided by 3, as in 0080.
- Files are data. Each file goes to the model between tags with a random 16-hex-digit boundary no file contains, so a file can't close its own tag. Invisible Unicode is removed, and the system prompt says the files are data, never instructions.
- All or nothing. The answer is read whole before anything is written. A stop lands only while the work waits for the model or the model writes; once Short Leash starts writing the files it can't, so a stop never says nothing was written when files were. The request runs with
Room::Fit, so a long answer takes the room left rather than being refused. An answer that names a file outsidewriteis refused whole, and so is one that isn't the JSON asked for, which is what an answer cut off part way becomes. Stopping writes nothing. - How files are written. Every file is checked before any is written: no link on the way, and still as the model read it, so a file the agent or the person changed meanwhile is never overwritten (
DelegateError::Changed, nothing written). Each is then replaced in one rename, written with the old file's mode from the start, so a private file such as a 0600.envnever shows its new contents to anyone else. An existing file keeps its permissions, put back even when a write fails, and its Windows line endings. A file written back unchanged is left alone. A write that fails after others were written names them (DelegateError::PartlyWritten), counting a file whose rename went through before a later step failed, and the work keeps them in its files. - What to check. Each written file says how many lines it had and has, with a warning when a line looks like a placeholder for code the model left out or it lost more than half its lines: small models' most common failure.
- The prompt. Each file's tag says whether it's for reading or writing, and the task and the files to write come again after the files, where a small model keeps them in mind best.
- Outside text. The model's notes and files are outside text. The tools show characters that hide text as codes and say to review the files as code and read the notes as suggestions.
- The queue. The work uses chat's model through the one model queue, after the endpoint makes room, as Explain does. One piece of work runs in each branch workspace at a time. Starting work and starting a merge or removal take one gate (
LocalJobs::gate) while each checks the other isn't running, so neither starts in between. - Waiting. The tool waits 50 seconds unless asked for longer, under the minute some assistants give a tool call, and then returns the work's id to follow with
local_work. - Kept in memory. Jobs stay in memory, the last 20, until leashd restarts. Nothing goes in
leash.db: the work is the branch workspace's files. - Short Leash Pro. It needs Short Leash Pro (
Feature::Ai), like the model's other uses (0085).
We chose whole files over patches because small models write a whole file more reliably than a diff that applies. We chose a branch workspace over the site's own folder so the model's changes never mix with the person's uncommitted work, and can be thrown away with the branch workspace. We gave the model no tools, not even reads, so what it sees is exactly what the agent chose and nothing it writes can act.
Consequences
- An agent has to make a branch workspace first and name every file. That costs a step, but nothing outside those files changes.
- The model's work is as good as a small model's. The tools say so, and every answer points the agent to
site_changesto review it. - A file to read over 256 KB, files to write over about 40 KB in all, or a task that doesn't fit the model, has to be split by the agent.
- A branch workspace's site serves what the model wrote as soon as it's written. Nothing runs it until someone visits the site or runs its tests.
- Work in progress delays chat's next answer, and another app's request through the endpoint, as Explain does.
- Writing a file back whole drops any invisible Unicode it held. The diff shows it.
- The link checks look at a path and then use it, so a folder swapped for a link in between, by another program working in the same checkout at that moment, isn't caught.
- Files that decide what runs only when someone runs it (
package.json's scripts, aProcfile,.nvmrc) aren't refused: they're the agent's call, like any file inwrite, and nothing runs them until someone starts the site's processes.