Skip to content

feat(tools): add a persistent repl, background processes and image reading - #55

Merged
mangit955 merged 1 commit into
mainfrom
feat/repl-processes-images
Aug 5, 2026
Merged

feat(tools): add a persistent repl, background processes and image reading#55
mangit955 merged 1 commit into
mainfrom
feat/repl-processes-images

Conversation

@mangit955

Copy link
Copy Markdown
Owner

Why

The agent was not using most of its tools. Across the 16 benchmark trials recorded in jobs/:

tool calls
run_terminal 1,243 (81%)
web_search 132
read_file 59
create_file 49
find_files / list_files / grep 38
edit_file + write_file 10

Of the 1,243 shell calls, 1,017 are inline python3/node scripts (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.txt was re-parsed 26 times across 185 scripts; input.tex 35 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_terminal refuses 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 — 131 cv2 and 128 numpy calls 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_file was 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 than python3 -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's finally; 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 on run_terminal, because a backgrounded command has no exit code to return and no output to wait for. These deliberately outlive the turn and end at dispose: a server started while answering one question has to still be up for the next.

read_imageTool.execute stays Promise<string>. Only Anthropic accepts an image inside a tool_result; Gemini's functionResponse wants a JSON object and OpenAI's function_call_output is 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.

shbashsh is dash on Debian, which the benchmark containers are. A recorded run lost six commands to sh: 1: Syntax error: Bad for loop variable, with nothing in the message pointing at the shell rather than the command. Falls back to sh where bash is genuinely absent.

Four defects found by reading the diff and probing, none caught by the suite:

defect consequence
plan mode could not see repl source commandOf reads only command/cmd/script, and the inline-script pattern needs a literal python3 -c, so open(p,'w') passed the second gate. Source needs its own classifier — the shell rules read >> as a redirect, so value >> 16 would have graded ordinary arithmetic as a write
background ids reused after a stop a model holds the whole transcript, so a stale bg1 read or killed a different process and returned something plausible
process_stop killed the shell, not its children measured: 2 processes before the stop, 1 alive after — the exact leak the tool claims to prevent
attached images counted as conversation turns recentMessages keeps the last 6 user turns; at maxTurns=2 zero real turns survived. On the video task — the case read_image exists for — five frames would leave one real turn and five "The image requested above:" stubs

Verified

bun run verify --all                 4 gates passed (docs lint, docs surface, type check, tests)
bun run verify --staged              3 gates passed; pre-commit hook re-ran it on commit
reverse-order sweep, 103 files       1670 pass, 0 fail
each new test file in isolation      pass

The sweep includes untracked files explicitly. git ls-files alone 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 — pgrep showed 2 processes before the stop and 1 after, then 2 and 0 once fixed.

bun run replay:baseline is 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:

  • The benchmark produced no usable signal. A run against harbor_woopcode/job.yaml reported 0/4, mean 0.000, but that number is contaminated: Harbor reused the existing jobs/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) and AgentSetupTimeoutError. 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 fresh job_name.
  • None of the new tools has been exercised by a live model. The 17 iterations that ran used only run_terminal, web_search, find_files, grep and list_files.
  • Live provider image rendering is untested. The three render paths are unit-tested against buildContents / buildAnthropicMessages / buildOpenAIInput, but no request carrying an image has reached a real API.
  • The TUI's rendering of a long-running process is unexercised.
  • classifyCode does not detect open(path, mode) when the mode is a variable, so plan mode could miss that write. The same fail-open already exists for python3 -c through run_terminal; this matches it rather than widening it.

…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.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
woop-code Ready Ready Preview Aug 5, 2026 5:51pm

@mangit955
mangit955 marked this pull request as ready for review August 5, 2026 17:52
@mangit955
mangit955 merged commit 0acee19 into main Aug 5, 2026
6 checks passed
@mangit955
mangit955 deleted the feat/repl-processes-images branch August 5, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant