feat(tools): add a persistent repl, background processes and image reading - #55
Merged
Conversation
…ading Across the 16 benchmark trials recorded in jobs/, 1,243 of 1,533 tool calls were run_terminal, and 1,017 of those were inline python3/node scripts. Every one of them rebuilt its state from nothing: the video-processing trial opened the same MP4 63 times across 74 scripts, gates.txt was re-parsed 26 times, input.tex 35 times. That is paid twice, in iterations and in tokens, because each script body stays in the conversation for the rest of the run. Two trials hit the 200-iteration ceiling and scored 0. repl keeps an interpreter alive for the turn, so data is loaded once and then queried. It drives a small framed driver rather than python3 -i, because an interactive prompt has no marker saying a statement finished and any reader has to guess. process_start/_output/_stop cover what run_terminal structurally cannot: it refuses an unquoted & and kills at its timeout, leaving no way to run a server or a watcher. These outlive the turn on purpose and end at dispose. read_image exists because the same trials show 131 cv2 and 128 numpy calls inferring what a frame contained, on a task that scored 0 in three of four trials. Tool.execute stays Promise<string>: only Anthropic accepts an image inside a tool result, so the image rides on a following user message, which is the one shape all three providers take. The path is stored rather than the bytes, keeping sessions small at the cost of a file that may vanish — which is reported to the model rather than silently dropped. Deliberately not added: multi_edit or apply_patch. edit_file was called 5 times in 1,533; more editing surface is prompt weight the model already routes around. Commands now run under bash rather than sh. sh is dash on Debian, and a recorded run lost six commands to "Syntax error: Bad for loop variable" with nothing in the message pointing at the shell. Four defects found by reading the diff and probing, not by the suite: plan mode could not see repl source, so a write passed the second gate; background ids were reused after a stop, turning a stale id into a mix-up rather than an error; process_stop killed the shell but not its children; and attached images counted as conversation turns in recentMessages, evicting the user's actual question.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The agent was not using most of its tools. Across the 16 benchmark trials recorded in
jobs/:run_terminalweb_searchread_filecreate_filefind_files/list_files/grepedit_file+write_fileOf the 1,243 shell calls, 1,017 are inline
python3/nodescripts (median 497 chars, p90 1,448). The model does not edit files with the file tools — it writes programs.Those programs are stateless, and that is where the iterations go. In the video-processing trial, 74 scripts open the same MP4 63 times, re-decoding it on every call because there was nowhere to keep the decoded frames.
gates.txtwas re-parsed 26 times across 185 scripts;input.tex35 times. It is paid twice — once in wall clock and iterations, once in tokens, since every script body stays in the conversation for the rest of the run. Prompts reached 113k tokens, and two trials hit the 200-iteration ceiling and scored 0.Two other gaps show in the same data.
run_terminalrefuses an unquoted&and kills at its timeout, so there was no way to run a server, a watcher, or a long build. And the model was inferring image contents rather than seeing them — 131cv2and 128numpycalls building histograms and bounding boxes, on the one task that scored 0 in three of four trials.Deliberately not added:
multi_edit/apply_patch.edit_filewas called 5 times in 1,533. More editing surface is prompt weight the model has already demonstrated it routes around. The five tools here cost ~907 tokens of schema per request (+43% on the tool block), which sits in the cached prefix and is paid once per turn.What
repl— an interpreter that stays alive for the turn, so data is loaded once and then queried. It drives a small framed driver (one JSON-encoded string of source per line in, output plus a per-session UUID sentinel out) rather thanpython3 -i: an interactive prompt interleaves output with>>>, has no marker saying a statement finished, and any reader has to guess — wrongly, on anything that prints something prompt-shaped. Sessions are per-turn and die in the loop'sfinally; one that outlived its turn would answer the next with variables nobody in that conversation set.process_start/process_output/process_stop— three tools rather than a flag onrun_terminal, because a backgrounded command has no exit code to return and no output to wait for. These deliberately outlive the turn and end atdispose: a server started while answering one question has to still be up for the next.read_image—Tool.executestaysPromise<string>. Only Anthropic accepts an image inside atool_result; Gemini'sfunctionResponsewants a JSON object and OpenAI'sfunction_call_outputis a string, so returning one directly would work on one provider and quietly degrade on two. Instead the image rides on a following user message, the one shape all three take. The path is stored rather than the bytes, so sessions stay small and compaction keeps moving short strings — the tradeoff being a file that can vanish between turns, which is reported to the model rather than dropped in silence.sh→bash—shis dash on Debian, which the benchmark containers are. A recorded run lost six commands tosh: 1: Syntax error: Bad for loop variable, with nothing in the message pointing at the shell rather than the command. Falls back toshwhere bash is genuinely absent.Four defects found by reading the diff and probing, none caught by the suite:
replsourcecommandOfreads onlycommand/cmd/script, and the inline-script pattern needs a literalpython3 -c, soopen(p,'w')passed the second gate. Source needs its own classifier — the shell rules read>>as a redirect, sovalue >> 16would have graded ordinary arithmetic as a writebg1read or killed a different process and returned something plausibleprocess_stopkilled the shell, not its childrenrecentMessageskeeps the last 6 user turns; atmaxTurns=2zero real turns survived. On the video task — the caseread_imageexists for — five frames would leave one real turn and five "The image requested above:" stubsVerified
The sweep includes untracked files explicitly.
git ls-filesalone lists only tracked ones, so the first sweep silently skipped all five new test files and reported 1565/98 — the documented trap, and it did fire here.Three of the four fixes were proven by reverting them and watching the right tests go red while the permission-side tests stayed green: plan mode (2 tests), id reuse (1), the history window (3). The orphaned-children fix was proven by direct measurement instead —
pgrepshowed 2 processes before the stop and 1 after, then 2 and 0 once fixed.bun run replay:baselineis byte-identical to a clean worktree at the merge base (69,691,456 chars over 932 iterations), so prompt assembly is unmoved.Not verified, and not implied:
harbor_woopcode/job.yamlreported0/4, mean 0.000, but that number is contaminated: Harbor reused the existingjobs/woopcode-terminal-bench-2/directory and scored three stale Aug 3 trials alongside today's. Only two trials actually ran, and both died on infrastructure —ApiRateLimitError(the key is rate-limited at 4 concurrent trials) andAgentSetupTimeoutError. It does establish that the CLI builds from this tree, installs, and runs 17 iterations without crashing; it establishes nothing about whether the tools help. A real number needs a key with headroom,n_concurrent_trials: 1, and a freshjob_name.run_terminal,web_search,find_files,grepandlist_files.buildContents/buildAnthropicMessages/buildOpenAIInput, but no request carrying an image has reached a real API.classifyCodedoes not detectopen(path, mode)when the mode is a variable, so plan mode could miss that write. The same fail-open already exists forpython3 -cthroughrun_terminal; this matches it rather than widening it.