Skip to content

Commit 96bf1a6

Browse files
Migrate CLI subprocess shell and refactor BashSession architecture (#109)
Co-authored-by: openhands <[email protected]>
1 parent 5dce76b commit 96bf1a6

File tree

13 files changed

+1496
-264
lines changed

13 files changed

+1496
-264
lines changed
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Core tool interface
12
from openhands.tools.execute_bash.definition import (
23
BashTool,
34
ExecuteBashAction,
@@ -6,11 +7,24 @@
67
)
78
from openhands.tools.execute_bash.impl import BashExecutor
89

10+
# Terminal session architecture - import from sessions package
11+
from openhands.tools.execute_bash.terminal import (
12+
TerminalCommandStatus,
13+
TerminalSession,
14+
create_terminal_session,
15+
)
16+
917

1018
__all__ = [
19+
# === Core Tool Interface ===
20+
"BashTool",
1121
"execute_bash_tool",
1222
"ExecuteBashAction",
1323
"ExecuteBashObservation",
1424
"BashExecutor",
15-
"BashTool",
25+
# === Terminal Session Architecture ===
26+
"TerminalSession",
27+
"TerminalCommandStatus",
28+
"TerminalSession",
29+
"create_terminal_session",
1630
]

openhands/tools/execute_bash/definition.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Execute bash tool implementation."""
22

3+
# Import for type annotation
4+
from typing import TYPE_CHECKING, Literal
5+
36
from pydantic import Field
47

58
from openhands.sdk.tool import ActionBase, ObservationBase, Tool, ToolAnnotations
@@ -11,6 +14,10 @@
1114
)
1215

1316

17+
if TYPE_CHECKING:
18+
from .impl import BashExecutor
19+
20+
1421
class ExecuteBashAction(ActionBase):
1522
"""Schema for bash command execution."""
1623

@@ -114,24 +121,39 @@ def agent_observation(self) -> str:
114121

115122

116123
class BashTool(Tool[ExecuteBashAction, ExecuteBashObservation]):
117-
"""A Tool subclass that automatically initializes a BashExecutor."""
124+
"""A Tool subclass that automatically initializes a BashExecutor with auto-detection.""" # noqa: E501
125+
126+
executor: "BashExecutor"
118127

119128
def __init__(
120129
self,
121130
working_dir: str,
122131
username: str | None = None,
132+
no_change_timeout_seconds: int | None = None,
133+
terminal_type: Literal["tmux", "subprocess"] | None = None,
123134
):
124135
"""Initialize BashTool with executor parameters.
125136
126137
Args:
127138
working_dir: The working directory for bash commands
128139
username: Optional username for the bash session
140+
no_change_timeout_seconds: Timeout for no output change
141+
terminal_type: Force a specific session type:
142+
('tmux', 'subprocess').
143+
If None, auto-detect based on system capabilities:
144+
- On Windows: PowerShell if available, otherwise subprocess
145+
- On Unix-like: tmux if available, otherwise subprocess
129146
"""
130147
# Import here to avoid circular imports
131148
from openhands.tools.execute_bash.impl import BashExecutor
132149

133150
# Initialize the executor
134-
executor = BashExecutor(working_dir=working_dir, username=username)
151+
executor = BashExecutor(
152+
working_dir=working_dir,
153+
username=username,
154+
no_change_timeout_seconds=no_change_timeout_seconds,
155+
terminal_type=terminal_type,
156+
)
135157

136158
# Initialize the parent Tool with the executor
137159
super().__init__(

openhands/tools/execute_bash/impl.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
1+
from typing import Literal
2+
13
from openhands.sdk.tool import ToolExecutor
2-
from openhands.tools.execute_bash.bash_session import BashSession
34
from openhands.tools.execute_bash.definition import (
45
ExecuteBashAction,
56
ExecuteBashObservation,
67
)
8+
from openhands.tools.execute_bash.terminal.factory import create_terminal_session
79

810

911
class BashExecutor(ToolExecutor):
1012
def __init__(
1113
self,
1214
working_dir: str,
1315
username: str | None = None,
16+
no_change_timeout_seconds: int | None = None,
17+
terminal_type: Literal["tmux", "subprocess"] | None = None,
1418
):
15-
self.session = BashSession(working_dir, username=username)
19+
"""Initialize BashExecutor with auto-detected or specified session type.
20+
21+
Args:
22+
working_dir: Working directory for bash commands
23+
username: Optional username for the bash session
24+
no_change_timeout_seconds: Timeout for no output change
25+
terminal_type: Force a specific session type:
26+
('tmux', 'subprocess').
27+
If None, auto-detect based on system capabilities
28+
"""
29+
self.session = create_terminal_session(
30+
work_dir=working_dir,
31+
username=username,
32+
no_change_timeout_seconds=no_change_timeout_seconds,
33+
terminal_type=terminal_type,
34+
)
1635
self.session.initialize()
1736

1837
def __call__(self, action: ExecuteBashAction) -> ExecuteBashObservation:
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from openhands.tools.execute_bash.terminal.factory import create_terminal_session
2+
from openhands.tools.execute_bash.terminal.interface import (
3+
TerminalInterface,
4+
TerminalSessionBase,
5+
)
6+
from openhands.tools.execute_bash.terminal.subprocess_terminal import SubprocessTerminal
7+
from openhands.tools.execute_bash.terminal.terminal_session import (
8+
TerminalCommandStatus,
9+
TerminalSession,
10+
)
11+
from openhands.tools.execute_bash.terminal.tmux_terminal import TmuxTerminal
12+
13+
14+
__all__ = [
15+
"TerminalInterface",
16+
"TerminalSessionBase",
17+
"TmuxTerminal",
18+
"SubprocessTerminal",
19+
"TerminalSession",
20+
"TerminalCommandStatus",
21+
"create_terminal_session",
22+
]
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Factory for creating appropriate terminal sessions based on system capabilities."""
2+
3+
import platform
4+
import subprocess
5+
from typing import Literal
6+
7+
from openhands.sdk.logger import get_logger
8+
from openhands.tools.execute_bash.terminal.terminal_session import TerminalSession
9+
10+
11+
logger = get_logger(__name__)
12+
13+
14+
def _is_tmux_available() -> bool:
15+
"""Check if tmux is available on the system."""
16+
try:
17+
result = subprocess.run(
18+
["tmux", "-V"],
19+
capture_output=True,
20+
text=True,
21+
timeout=5.0,
22+
)
23+
return result.returncode == 0
24+
except (subprocess.TimeoutExpired, FileNotFoundError):
25+
return False
26+
27+
28+
def _is_powershell_available() -> bool:
29+
"""Check if PowerShell is available on the system."""
30+
if platform.system() == "Windows":
31+
# Check for Windows PowerShell
32+
powershell_cmd = "powershell"
33+
else:
34+
# Check for PowerShell Core (pwsh) on non-Windows systems
35+
powershell_cmd = "pwsh"
36+
37+
try:
38+
result = subprocess.run(
39+
[powershell_cmd, "-Command", "Write-Host 'PowerShell Available'"],
40+
capture_output=True,
41+
text=True,
42+
timeout=5.0,
43+
)
44+
return result.returncode == 0
45+
except (subprocess.TimeoutExpired, FileNotFoundError):
46+
return False
47+
48+
49+
def create_terminal_session(
50+
work_dir: str,
51+
username: str | None = None,
52+
no_change_timeout_seconds: int | None = None,
53+
terminal_type: Literal["tmux", "subprocess"] | None = None,
54+
) -> TerminalSession:
55+
"""Create an appropriate terminal session based on system capabilities.
56+
57+
Args:
58+
work_dir: Working directory for the session
59+
username: Optional username for the session
60+
no_change_timeout_seconds: Timeout for no output change
61+
terminal_type: Force a specific session type ('tmux', 'subprocess')
62+
If None, auto-detect based on system capabilities
63+
64+
Returns:
65+
TerminalSession instance
66+
67+
Raises:
68+
RuntimeError: If the requested session type is not available
69+
"""
70+
from openhands.tools.execute_bash.terminal.terminal_session import TerminalSession
71+
72+
if terminal_type:
73+
# Force specific session type
74+
if terminal_type == "tmux":
75+
if not _is_tmux_available():
76+
raise RuntimeError("Tmux is not available on this system")
77+
from openhands.tools.execute_bash.terminal.tmux_terminal import TmuxTerminal
78+
79+
logger.info("Using forced TmuxTerminal")
80+
terminal = TmuxTerminal(work_dir, username)
81+
return TerminalSession(terminal, no_change_timeout_seconds)
82+
elif terminal_type == "subprocess":
83+
from openhands.tools.execute_bash.terminal.subprocess_terminal import (
84+
SubprocessTerminal,
85+
)
86+
87+
logger.info("Using forced SubprocessTerminal")
88+
terminal = SubprocessTerminal(work_dir, username)
89+
return TerminalSession(terminal, no_change_timeout_seconds)
90+
else:
91+
raise ValueError(f"Unknown session type: {terminal_type}")
92+
93+
# Auto-detect based on system capabilities
94+
system = platform.system()
95+
96+
if system == "Windows":
97+
raise NotImplementedError("Windows is not supported yet for OpenHands V1.")
98+
else:
99+
# On Unix-like systems, prefer tmux if available, otherwise use subprocess
100+
if _is_tmux_available():
101+
from openhands.tools.execute_bash.terminal.tmux_terminal import TmuxTerminal
102+
103+
logger.info("Auto-detected: Using TmuxTerminal (tmux available)")
104+
terminal = TmuxTerminal(work_dir, username)
105+
return TerminalSession(terminal, no_change_timeout_seconds)
106+
else:
107+
from openhands.tools.execute_bash.terminal.subprocess_terminal import (
108+
SubprocessTerminal,
109+
)
110+
111+
logger.info("Auto-detected: Using SubprocessTerminal (tmux not available)")
112+
terminal = SubprocessTerminal(work_dir, username)
113+
return TerminalSession(terminal, no_change_timeout_seconds)

0 commit comments

Comments
 (0)