Skip to content

Add BackgroundCodeExecToolProvider for long-running commands - #100

Draft
MohamedAkbarally wants to merge 1 commit into
mainfrom
feat/background-code-exec
Draft

MohamedAkbarally wants to merge 1 commit into
mainfrom
feat/background-code-exec

Conversation

@MohamedAkbarally

Copy link
Copy Markdown
Contributor

What

Adds BackgroundCodeExecToolProvider, which wraps any sandboxed CodeExecToolProvider and replaces its blocking code_exec with two tools:

  • code_exec starts a Bash command and waits up to yield_time_ms (clamped between min_wait_sec and max_yield_time_sec). If the command is still running when the wait ends, it keeps running and the result includes a process_id. wait_for="output" returns early when new output appears.
  • process_control polls a process for new output, terminates it with its process tree, or lists the processes still running.
from stirrup.tools.code_backends import BackgroundCodeExecToolProvider
from stirrup.tools.code_backends.docker import DockerCodeExecToolProvider

provider = BackgroundCodeExecToolProvider(DockerCodeExecToolProvider.from_image("my-image-with-tmux"))

This lets agents run builds, test suites and dev servers without blocking a turn until they finish, or until the shell timeout kills them.

How

  • Each command runs in its own tmux session in the sandbox. Combined stdout/stderr goes to a fixed-size ring buffer, so output can't grow without bound.
  • Each result shows only output the model hasn't seen yet: a head/tail preview when it's long, with the full text saved to a file in the sandbox.
  • Terminate uses cgroup v2 cgroup.kill when it's writable, then the process group, then the recorded process tree, and checks that nothing survives. When the provider exits, it terminates everything it started. Each provider instance uses its own process root, so two providers in one sandbox don't touch each other's processes.
  • Commands too long for one exec argument are written to a script in chunks.
  • File operations, output saving and uploads go to the wrapped backend.

Constraints

  • The sandbox must be Linux with Bash, tmux and procps (BACKGROUND_TOOLS). The provider checks this on enter and fails with a clear error.
  • Rejects LocalCodeExecToolProvider (its absolute-path guard blocks the paths the provider uses) and backends with allowed_commands (the provider needs a shell).
  • A subagent with share_parent_exec_env=True gets the backend's blocking code_exec, not process_control.

Testing

  • tests/test_background_execution.py: 12 tests against a real Bash backend on the host. They cover exit codes, backgrounding and polling, wait_for="output", terminating a process tree, cleanup on exit, large-output previews and the saved file, commands over 200 KB, workdir, the minimum wait, and backend validation. They're skipped where the tools aren't installed.
  • Ran end to end through DockerCodeExecToolProvider with a Debian image that has tmux and procps: start, background, poll, terminate and file I/O all worked.
  • ruff, ty and the full suite (-m "not browser") pass.

🤖 Generated with Claude Code

Wraps a sandboxed backend (Docker, E2B, or custom) so code_exec can return
while a command keeps running, with a process_control tool to poll,
terminate, or list those processes. Each command runs in its own tmux
session; output goes to a bounded ring buffer, is previewed head/tail
inline, and saved in full to a file. Teardown terminates every process
the provider started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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