Skip to content

feat(coding-agent): add async bash() to IPython kernel - #1187

Open
samsja wants to merge 1 commit into
mainfrom
feat/async-bash-tool
Open

feat(coding-agent): add async bash() to IPython kernel#1187
samsja wants to merge 1 commit into
mainfrom
feat/async-bash-tool

Conversation

@samsja

@samsja samsja commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds an async bash() function to the IPython kernel bootstrap code so await bash("...") runs shell commands via asyncio.create_subprocess_exec without blocking the kernel event loop.

Motivation

%%bash cells block the IPython kernel until the command finishes — the kernel can't respond to interrupts or other messages while a shell command runs. This is the same anti-pattern as time.sleep() polling. await bash("...") uses asyncio.create_subprocess_exec so the kernel event loop stays responsive.

Changes

  • ipython.ts: Added _PrimeAgentBashResult class and async bash() function to RLM_BOOTSTRAP_BASE_CODE. Supports timeout (raises TimeoutError) and cwd parameters. Returns stdout, stderr, and returncode.
  • rlm.ts: Updated the IPython control prompt to prefer await bash("...") over %%bash cells.
  • system-prompt.test.ts: Updated exact-match block and added toContain assertion for the new prompt text.

Usage

result = await bash("echo hello")           # BashResult(stdout="hello\n", ...)
result = await bash("sleep 5", timeout=2)   # raises TimeoutError
print(result.stdout, result.stderr, result.returncode)

Testing

  • npx vitest run test/system-prompt.test.ts — 25/25 pass
  • npx vitest run test/ipython-bootstrap.test.ts — 6/6 pass (including real kernel tests)
  • Manually verified in a real IPython kernel: stdout/stderr capture, exit codes, and timeout all work correctly
  • npm run check — clean

Note

Medium Risk
Changes how agents run shell from IPython (new default path) and executes arbitrary shell via subprocess in the kernel; behavior is additive with %%bash still supported, but long-running commands and env/cwd semantics may differ from magic cells.

Overview
Introduces await bash("...") in the IPython kernel bootstrap (RLM_BOOTSTRAP_BASE_CODE): commands run through asyncio.create_subprocess_exec with optional timeout and cwd, returning a _PrimeAgentBashResult (stdout, stderr, returncode) instead of blocking the kernel like %%bash cells.

RLM / system prompt guidance now prefers await bash(...) for shell work while still documenting %%bash rules when cells are used. CHANGELOG and system-prompt.test.ts are updated to match (including a prefer \await bash`` assertion).

Reviewed by Cursor Bugbot for commit e92f4b0. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add async bash() function to IPython kernel for non-blocking shell execution

  • Adds an awaitable bash(command, *, timeout, cwd) function to the IPython kernel bootstrap in ipython.ts that spawns shell commands via asyncio.create_subprocess_exec without blocking the event loop.
  • Returns a _PrimeAgentBashResult object with stdout, stderr, and returncode fields; stringifies to combined output with exit-code info on failure.
  • Updates the RLM system prompt in rlm.ts to instruct the agent to prefer await bash("...") over %%bash magic commands.
📊 Macroscope summarized e92f4b0. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

Add an async bash() function to the RLM bootstrap code in ipython.ts
that uses asyncio.create_subprocess_exec to run shell commands without
blocking the kernel event loop. Unlike %%bash cells (which block the
kernel until the command finishes), await bash('...') keeps the kernel
responsive to interrupts and other messages while the process runs.

Supports optional timeout (raises TimeoutError) and cwd parameters.
Returns a _PrimeAgentBashResult with stdout, stderr, and returncode.

Update the RLM system prompt to prefer await bash('...') over %%bash
cells.
env = dict(_prime_agent_bash_os.environ)
work_dir = cwd or _prime_agent_bash_os.getcwd()

proc = await _prime_agent_asyncio.create_subprocess_exec(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High tools/ipython.ts:84

bash() reads both stdout and stderr entirely into memory via proc.communicate(). Commands that produce large or unbounded output (e.g., a long-running test process that continuously logs) will buffer everything in the persistent kernel's memory until it is exhausted and the kernel crashes. Consider streaming the output and enforcing a size limit instead of relying on communicate(), which Python's asyncio docs warn against for large or unlimited output.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/tools/ipython.ts around line 84:

`bash()` reads both stdout and stderr entirely into memory via `proc.communicate()`. Commands that produce large or unbounded output (e.g., a long-running test process that continuously logs) will buffer everything in the persistent kernel's memory until it is exhausted and the kernel crashes. Consider streaming the output and enforcing a size limit instead of relying on `communicate()`, which Python's `asyncio` docs warn against for large or unlimited output.

env = dict(_prime_agent_bash_os.environ)
work_dir = cwd or _prime_agent_bash_os.getcwd()

proc = await _prime_agent_asyncio.create_subprocess_exec(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High tools/ipython.ts:84

On timeout, bash() kills only the Bash process while its child processes keep running. For example, await bash('sleep 60', timeout=1) reports a TimeoutError after 1 second, but the sleep child survives and continues executing — along with any file or network side effects — because proc.kill() targets only the Bash PID, not its descendants. Consider launching the subprocess in a new process group/session (start_new_session=True) and sending the signal to the entire group on timeout, e.g. os.killpg(os.getpgid(proc.pid), signal.SIGKILL).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/tools/ipython.ts around line 84:

On timeout, `bash()` kills only the Bash process while its child processes keep running. For example, `await bash('sleep 60', timeout=1)` reports a `TimeoutError` after 1 second, but the `sleep` child survives and continues executing — along with any file or network side effects — because `proc.kill()` targets only the Bash PID, not its descendants. Consider launching the subprocess in a new process group/session (`start_new_session=True`) and sending the signal to the entire group on timeout, e.g. `os.killpg(os.getpgid(proc.pid), signal.SIGKILL)`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e92f4b0. Configure here.

except _prime_agent_asyncio.TimeoutError:
proc.kill()
await proc.wait()
raise TimeoutError(f"bash command timed out after {timeout}s: {command}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphaned processes after timeout or interrupt

High Severity

bash() only kills the parent shell on timeout and has no cleanup if the await is cancelled or interrupted. Child processes started by the command keep running, and an interrupted await bash(...) can leave the whole subprocess alive while the kernel moves on.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e92f4b0. Configure here.

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