Skip to content

Commit 935839a

Browse files
Adding tmux support.
1 parent 1545d76 commit 935839a

4 files changed

Lines changed: 396 additions & 13 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ limitations under the License.
2727
* 🔌 **MCP server integration:** Connect any [Model Context Protocol](https://modelcontextprotocol.io) server as a tool source via the `--mcp` CLI flag. Supports both HTTP (Streamable HTTP) and stdio-based servers.
2828
* 👁️ **Image loading:** Agents can load and visually inspect image files (plots, screenshots, diagrams) via the built-in `load_image` tool — always available, no flags needed.
2929
* 🎨 **Image tools:** Visual image diffing (`diff_images`), OCR text extraction from images (`screen_ocr`), and a canvas for drawing shapes, text, and annotations (`canvas_create`, `canvas_draw`) — always available.
30+
* 🖵 **Tmux multi-screen:** Agents can create and operate multiple independent shell sessions concurrently via tmux (`--tmux` flag or `BPSA_TMUX=1`). Run builds, servers, and tests in parallel across named screen sessions.
3031
* 🎤 **Dictation input:** Dictate prompts via microphone using Whisper or ElevenLabs transcription (`/dictation` command, requires `BPSA_DICTATION_TRANSCRIBER` env var).
3132
***Native Python execution:** Execute Python code natively via `exec` for unrestricted processing.
3233
* 🌍 **Multi-language support:** Code in multiple languages beyond Python (Pascal, PHP, C++, Java and more).
@@ -94,6 +95,7 @@ $ bpsa --load-instructions # Load CLAUDE.md, AGENTS.md, etc. at startup
9495
$ bpsa --browser # Enable Playwright browser integration
9596
$ bpsa --gui-x11 # Enable native GUI interaction (xdotool/ImageMagick)
9697
$ bpsa --image # Enable image analysis and drawing tools
98+
$ bpsa --tmux # Enable tmux multi-screen tools
9799
$ bpsa --mcp http://localhost:8000/mcp # Connect an HTTP MCP server
98100
$ bpsa --mcp 'npx -y @modelcontextprotocol/server-filesystem /' # Connect a stdio MCP server
99101
```

docs/CLI.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ These variables enable optional tool sets. Each corresponds to a CLI flag. Setti
7373
| `BPSA_BROWSER` | `--browser` | Enable Playwright browser integration (navigate, click, type, etc.) |
7474
| `BPSA_GUI` | `--gui-x11` | Enable native GUI interaction tools (screenshot, click, type via xdotool/ImageMagick on X11) |
7575
| `BPSA_IMAGE` | `--image` | Enable image analysis and drawing tools (diff_images, screen_ocr, canvas drawing) |
76+
| `BPSA_TMUX` | `--tmux` | Enable tmux multi-screen tools (create, send, read, list, destroy, wait) |
7677

7778
### Context Compression Variables
7879

@@ -199,6 +200,51 @@ Use `prompt_toolkit` for:
199200
| `/verbose` | Toggle verbose output |
200201
| `/dictation [on\|off]` | Toggle dictation (requires `BPSA_DICTATION_TRANSCRIBER`) |
201202

203+
## Tmux Multi-Screen Tools
204+
205+
The `--tmux` flag (or `BPSA_TMUX=1` env var) enables tools that let the agent create and operate multiple independent shell sessions concurrently via tmux. This is useful for running long-running processes (servers, builds, file watchers) in parallel while the agent continues other work.
206+
207+
**Requires:** `tmux` installed on the system.
208+
209+
```bash
210+
bpsa --tmux
211+
212+
# Or via environment variable:
213+
export BPSA_TMUX=1
214+
bpsa
215+
```
216+
217+
### Available Tools
218+
219+
| Tool | Description |
220+
|------|-------------|
221+
| `tmux_create(session_name, command?)` | Create a new named screen session (default command: `bash`) |
222+
| `tmux_send(session_name, text, press_enter?)` | Send keystrokes to a session (default: presses Enter after text) |
223+
| `tmux_read(session_name, lines?)` | Read the current screen content / scrollback (default: 100 lines, max: 2000) |
224+
| `tmux_list()` | List all active agent screen sessions |
225+
| `tmux_destroy(session_name)` | Kill a session and all its processes |
226+
| `tmux_wait(session_name, pattern, timeout?, interval?)` | Poll until a text pattern appears (default: 60s timeout, 1s interval) |
227+
228+
### Example Agent Workflow
229+
230+
```
231+
tmux_create("server")
232+
tmux_send("server", "python app.py")
233+
tmux_wait("server", "Listening on port 8080")
234+
235+
tmux_create("tests")
236+
tmux_send("tests", "pytest tests/ -v")
237+
tmux_wait("tests", "passed")
238+
tmux_read("tests")
239+
240+
tmux_destroy("server")
241+
tmux_destroy("tests")
242+
```
243+
244+
### Session Namespacing
245+
246+
All sessions are automatically prefixed with `bpsa_` internally to avoid collisions with user tmux sessions. The agent uses short names (e.g., `server`) while tmux sees `bpsa_server`. Sessions are automatically cleaned up when the process exits.
247+
202248
## MCP Server Integration
203249

204250
The `--mcp` flag connects [Model Context Protocol](https://modelcontextprotocol.io) servers as additional tool sources. Tools exposed by MCP servers are automatically available to the agent alongside the built-in tools.
@@ -482,3 +528,4 @@ bppas lib/ --strip-comments -o interfaces.txt
482528
- `prompt_toolkit` - REPL input handling (optional, falls back to basic `input()`)
483529
- `rich` - Output formatting (already a project dependency)
484530
- `argparse` - CLI argument parsing (stdlib)
531+
- `tmux` - Multi-screen session management (optional, required for `--tmux`)

src/smolagents/bp_cli.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
BPSA_COMPRESSION_PRESERVE_FINAL_ANSWER_STEPS - Keep final_answer steps (default: 1)
3838
BPSA_COMPRESSION_MIN_CHARS - Min chars before compressing (default: 4096)
3939
40+
Tmux multi-screen tools (requires tmux installed):
41+
BPSA_TMUX - Enable tmux multi-screen tools (0 or 1, default: 0)
42+
4043
Dictation input (requires `pip install bpsa[dictation]`):
4144
BPSA_DICTATION_TRANSCRIBER - Transcriber name: 'whisper' or 'elevenlabs' (required for /dictation)
4245
BPSA_DICTATION_MODEL - Model name passed to transcriber (optional, whisper only)
@@ -337,7 +340,7 @@ def build_model(override_model_id=None):
337340
return model
338341

339342

340-
def build_agent(model, approval_callback=None, browser_enabled=False, gui_enabled=False, image_enabled=False, mcp_servers=None):
343+
def build_agent(model, approval_callback=None, browser_enabled=False, gui_enabled=False, image_enabled=False, tmux_enabled=False, mcp_servers=None):
341344
from smolagents import CodeAgent
342345
from smolagents.bp_thinkers import (
343346
DEFAULT_THINKER_COMPRESSION, DEFAULT_THINKER_MAX_STEPS,
@@ -371,6 +374,10 @@ def build_agent(model, approval_callback=None, browser_enabled=False, gui_enable
371374
gui_manager, gui_tools = create_gui_tools()
372375
tools.extend(gui_tools)
373376

377+
if tmux_enabled:
378+
from smolagents.bp_tools_tmux import create_tmux_tools
379+
tools.extend(create_tmux_tools())
380+
374381
copilot_model_id = get_env("BPSA_COPILOT_MODEL_ID", default=None)
375382
if copilot_model_id:
376383
from smolagents.bp_tools import GitHubCopilotCoder
@@ -1675,7 +1682,7 @@ def cmd_undo(agent, args: str):
16751682
console.print(f"[yellow]Only {actual} of {n} requested steps were removable (protected system prompt steps).[/]")
16761683

16771684

1678-
def cmd_repeat(agent, model, n, prompt_text, session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=False, image_enabled=False):
1685+
def cmd_repeat(agent, model, n, prompt_text, session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=False, image_enabled=False, tmux_enabled=False):
16791686
"""Run a prompt N times, each on a fresh agent with snapshotted context."""
16801687
from smolagents.bp_session import load_session_from_dict, save_session_to_dict
16811688
from smolagents.monitoring import LogLevel
@@ -1707,7 +1714,7 @@ def cmd_repeat(agent, model, n, prompt_text, session_stats, verbose, instruction
17071714
os.chdir(original_folder)
17081715

17091716
# Create fresh agent and restore snapshot
1710-
cycle_agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled)
1717+
cycle_agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled)
17111718
load_session_from_dict(snapshot, cycle_agent)
17121719

17131720
# Prepare prompt (prepend instructions on first_turn only)
@@ -1804,13 +1811,13 @@ def prepend_instructions(task: str, instructions: str | None) -> str:
18041811
return task
18051812

18061813

1807-
def run_one_shot(task: str, skip_instructions: bool = False, auto_approve: bool = True, browser_enabled: bool = False, gui_enabled: bool = False, image_enabled: bool = False, mcp_servers=None):
1814+
def run_one_shot(task: str, skip_instructions: bool = False, auto_approve: bool = True, browser_enabled: bool = False, gui_enabled: bool = False, image_enabled: bool = False, tmux_enabled: bool = False, mcp_servers=None):
18081815
global _auto_approve
18091816
_auto_approve = auto_approve
18101817
try_load_dotenv()
18111818
check_required_env()
18121819
model = build_model()
1813-
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
1820+
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
18141821
instructions = None
18151822
if not skip_instructions:
18161823
console.print("[dim]Loading agent instructions...[/]")
@@ -1835,14 +1842,14 @@ def run_one_shot(task: str, skip_instructions: bool = False, auto_approve: bool
18351842
_shutdown_mcp(agent)
18361843

18371844

1838-
def run_repl(skip_instructions: bool = False, auto_approve: bool = True, browser_enabled: bool = False, gui_enabled: bool = False, image_enabled: bool = False, mcp_servers=None):
1845+
def run_repl(skip_instructions: bool = False, auto_approve: bool = True, browser_enabled: bool = False, gui_enabled: bool = False, image_enabled: bool = False, tmux_enabled: bool = False, mcp_servers=None):
18391846
global _auto_approve
18401847
_auto_approve = auto_approve
18411848
try_load_dotenv()
18421849
check_required_env()
18431850

18441851
model = build_model()
1845-
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
1852+
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
18461853
model_id = get_env("BPSA_MODEL_ID")
18471854
server_model = get_env("BPSA_SERVER_MODEL", default="OpenAIServerModel")
18481855
tool_count = count_tools(agent)
@@ -2063,7 +2070,7 @@ def get_input():
20632070
_shutdown_browser(agent)
20642071
_shutdown_gui(agent)
20652072
_shutdown_mcp(agent)
2066-
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
2073+
agent = build_agent(model, approval_callback=interactive_approval_callback, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
20672074
session_stats = {
20682075
"turns": 0,
20692076
"total_time": 0.0,
@@ -2206,7 +2213,7 @@ def get_input():
22062213
except ValueError:
22072214
console.print("[red]N must be a positive integer. Usage: /repeat <N> <prompt>[/]")
22082215
continue
2209-
cmd_repeat(agent, model, repeat_n, parts[1], session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled)
2216+
cmd_repeat(agent, model, repeat_n, parts[1], session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled)
22102217
continue
22112218
elif cmd == "/repeat-prompt":
22122219
parts = cmd_args.strip().split(None, 1)
@@ -2223,7 +2230,7 @@ def get_input():
22232230
file_content = load_file_as_prompt(parts[1])
22242231
if file_content is None:
22252232
continue
2226-
cmd_repeat(agent, model, repeat_n, file_content, session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled)
2233+
cmd_repeat(agent, model, repeat_n, file_content, session_stats, verbose, instructions, first_turn, browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled)
22272234
continue
22282235
elif cmd == "/session-save":
22292236
cmd_session_save(agent, session_stats, cmd_args)
@@ -2380,6 +2387,10 @@ def main():
23802387
"--image", action="store_true",
23812388
help="Enable image analysis and drawing tools (diff_images, screen_ocr, canvas drawing)",
23822389
)
2390+
parser.add_argument(
2391+
"--tmux", action="store_true",
2392+
help="Enable tmux multi-screen tools (create, send, read, list, destroy, wait)",
2393+
)
23832394
parser.add_argument(
23842395
"--mcp", action="append", metavar="URL_OR_CMD", dest="mcp",
23852396
help="Connect an MCP server. Use a URL for HTTP servers or a shell command for stdio servers. Can be repeated for multiple servers.",
@@ -2396,6 +2407,7 @@ def main():
23962407
browser_enabled = args.browser or get_env_bool("BPSA_BROWSER")
23972408
gui_enabled = args.gui_x11 or get_env_bool("BPSA_GUI")
23982409
image_enabled = args.image or get_env_bool("BPSA_IMAGE")
2410+
tmux_enabled = args.tmux or get_env_bool("BPSA_TMUX")
23992411
env_mcp = get_env("BPSA_MCP", "")
24002412
env_mcp_list = [s.strip() for s in env_mcp.splitlines() if s.strip()]
24012413
mcp_servers = _parse_mcp_servers((args.mcp or []) + env_mcp_list) or None
@@ -2404,15 +2416,15 @@ def main():
24042416
if not sys.stdin.isatty() and args.command is None:
24052417
task = sys.stdin.read().strip()
24062418
if task:
2407-
run_one_shot(task, skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
2419+
run_one_shot(task, skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
24082420
else:
24092421
fail("No input provided via pipe.")
24102422
return
24112423

24122424
if args.command == "run":
2413-
run_one_shot(args.task, skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
2425+
run_one_shot(args.task, skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
24142426
else:
2415-
run_repl(skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, mcp_servers=mcp_servers)
2427+
run_repl(skip_instructions=skip_instructions, auto_approve=auto_approve, browser_enabled=browser_enabled, gui_enabled=gui_enabled, image_enabled=image_enabled, tmux_enabled=tmux_enabled, mcp_servers=mcp_servers)
24162428

24172429

24182430
if __name__ == "__main__":

0 commit comments

Comments
 (0)