openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. openhands-0.0.0.dist-info/top_level.txt +0 -1
@@ -1,166 +0,0 @@
1
- """Execute bash tool implementation."""
2
-
3
- # Import for type annotation
4
- from typing import TYPE_CHECKING, Literal
5
-
6
- from pydantic import Field
7
-
8
- from openhands.sdk.llm import ImageContent, TextContent
9
- from openhands.sdk.tool import ActionBase, ObservationBase, Tool, ToolAnnotations
10
- from openhands.sdk.utils import maybe_truncate
11
- from openhands.tools.execute_bash.constants import (
12
- MAX_CMD_OUTPUT_SIZE,
13
- NO_CHANGE_TIMEOUT_SECONDS,
14
- )
15
- from openhands.tools.execute_bash.metadata import CmdOutputMetadata
16
-
17
-
18
- if TYPE_CHECKING:
19
- from .impl import BashExecutor
20
-
21
-
22
- class ExecuteBashAction(ActionBase):
23
- """Schema for bash command execution."""
24
-
25
- command: str = Field(
26
- description="The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together." # noqa
27
- )
28
- is_input: bool = Field(
29
- default=False,
30
- description="If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.", # noqa
31
- )
32
- timeout: float | None = Field(
33
- default=None,
34
- description=f"Optional. Sets a maximum time limit (in seconds) for running the command. If the command takes longer than this limit, you’ll be asked whether to continue or stop it. If you don’t set a value, the command will instead pause and ask for confirmation when it produces no new output for {NO_CHANGE_TIMEOUT_SECONDS} seconds. Use a higher value if the command is expected to take a long time (like installation or testing), or if it has a known fixed duration (like sleep).", # noqa
35
- )
36
-
37
-
38
- class ExecuteBashObservation(ObservationBase):
39
- """A ToolResult that can be rendered as a CLI output."""
40
-
41
- output: str = Field(description="The raw output from the tool.")
42
- command: str | None = Field(
43
- default=None,
44
- description="The bash command that was executed. Can be empty string if the observation is from a previous command that hit soft timeout and is not yet finished.", # noqa
45
- )
46
- exit_code: int | None = Field(
47
- default=None,
48
- description="The exit code of the command. -1 indicates the process hit the soft timeout and is not yet finished.", # noqa
49
- )
50
- error: bool = Field(
51
- default=False,
52
- description="Whether there was an error during command execution.",
53
- )
54
- timeout: bool = Field(
55
- default=False, description="Whether the command execution timed out."
56
- )
57
- metadata: CmdOutputMetadata = Field(
58
- default_factory=CmdOutputMetadata,
59
- description="Additional metadata captured from PS1 after command execution.",
60
- )
61
-
62
- @property
63
- def command_id(self) -> int | None:
64
- """Get the command ID from metadata."""
65
- return self.metadata.pid
66
-
67
- @property
68
- def agent_observation(self) -> list[TextContent | ImageContent]:
69
- ret = f"{self.metadata.prefix}{self.output}{self.metadata.suffix}"
70
- if self.metadata.working_dir:
71
- ret += f"\n[Current working directory: {self.metadata.working_dir}]"
72
- if self.metadata.py_interpreter_path:
73
- ret += f"\n[Python interpreter: {self.metadata.py_interpreter_path}]"
74
- if self.metadata.exit_code != -1:
75
- ret += f"\n[Command finished with exit code {self.metadata.exit_code}]"
76
- if self.error:
77
- ret = f"[There was an error during command execution.]\n{ret}"
78
- return [TextContent(text=maybe_truncate(ret, MAX_CMD_OUTPUT_SIZE))]
79
-
80
-
81
- TOOL_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
82
-
83
-
84
- ### Command Execution
85
- * One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
86
- * Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands.
87
- * Soft timeout: Commands have a soft timeout of 10 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details)
88
- * Shell options: Do NOT use `set -e`, `set -eu`, or `set -euo pipefail` in shell scripts or commands in this environment. The runtime may not support them and can cause unusable shell sessions. If you want to run multi-line bash commands, write the commands to a file and then run it, instead.
89
-
90
- ### Long-running Commands
91
- * For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`.
92
- * For commands that may run for a long time (e.g. installation or testing commands), or commands that run for a fixed amount of time (e.g. sleep), you should set the "timeout" parameter of your function call to an appropriate value.
93
- * If a bash command returns exit code `-1`, this means the process hit the soft timeout and is not yet finished. By setting `is_input` to `true`, you can:
94
- - Send empty `command` to retrieve additional logs
95
- - Send text (set `command` to the text) to STDIN of the running process
96
- - Send control commands like `C-c` (Ctrl+C), `C-d` (Ctrl+D), or `C-z` (Ctrl+Z) to interrupt the process
97
- - If you do C-c, you can re-start the process with a longer "timeout" parameter to let it run to completion
98
-
99
- ### Best Practices
100
- * Directory verification: Before creating new directories or files, first verify the parent directory exists and is the correct location.
101
- * Directory management: Try to maintain working directory by using absolute paths and avoiding excessive use of `cd`.
102
-
103
- ### Output Handling
104
- * Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
105
- """ # noqa
106
-
107
-
108
- execute_bash_tool = Tool(
109
- name="execute_bash",
110
- input_schema=ExecuteBashAction,
111
- output_schema=ExecuteBashObservation,
112
- description=TOOL_DESCRIPTION,
113
- annotations=ToolAnnotations(
114
- title="execute_bash",
115
- readOnlyHint=False,
116
- destructiveHint=True,
117
- idempotentHint=False,
118
- openWorldHint=True,
119
- ),
120
- )
121
-
122
-
123
- class BashTool(Tool[ExecuteBashAction, ExecuteBashObservation]):
124
- """A Tool subclass that automatically initializes a BashExecutor with auto-detection.""" # noqa: E501
125
-
126
- executor: "BashExecutor"
127
-
128
- def __init__(
129
- self,
130
- working_dir: str,
131
- username: str | None = None,
132
- no_change_timeout_seconds: int | None = None,
133
- terminal_type: Literal["tmux", "subprocess"] | None = None,
134
- ):
135
- """Initialize BashTool with executor parameters.
136
-
137
- Args:
138
- working_dir: The working directory for bash commands
139
- 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
146
- """
147
- # Import here to avoid circular imports
148
- from openhands.tools.execute_bash.impl import BashExecutor
149
-
150
- # Initialize the executor
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
- )
157
-
158
- # Initialize the parent Tool with the executor
159
- super().__init__(
160
- name=execute_bash_tool.name,
161
- description=TOOL_DESCRIPTION,
162
- input_schema=ExecuteBashAction,
163
- output_schema=ExecuteBashObservation,
164
- annotations=execute_bash_tool.annotations,
165
- executor=executor,
166
- )
@@ -1,38 +0,0 @@
1
- from typing import Literal
2
-
3
- from openhands.sdk.tool import ToolExecutor
4
- from openhands.tools.execute_bash.definition import (
5
- ExecuteBashAction,
6
- ExecuteBashObservation,
7
- )
8
- from openhands.tools.execute_bash.terminal.factory import create_terminal_session
9
-
10
-
11
- class BashExecutor(ToolExecutor):
12
- def __init__(
13
- self,
14
- working_dir: str,
15
- username: str | None = None,
16
- no_change_timeout_seconds: int | None = None,
17
- terminal_type: Literal["tmux", "subprocess"] | None = None,
18
- ):
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
- )
35
- self.session.initialize()
36
-
37
- def __call__(self, action: ExecuteBashAction) -> ExecuteBashObservation:
38
- return self.session.execute(action)
@@ -1,101 +0,0 @@
1
- """Metadata for bash command execution."""
2
-
3
- import json
4
- import re
5
- import traceback
6
-
7
- from pydantic import BaseModel, Field
8
-
9
- from openhands.sdk.logger import get_logger
10
- from openhands.tools.execute_bash.constants import (
11
- CMD_OUTPUT_METADATA_PS1_REGEX,
12
- CMD_OUTPUT_PS1_BEGIN,
13
- CMD_OUTPUT_PS1_END,
14
- )
15
-
16
-
17
- logger = get_logger(__name__)
18
-
19
-
20
- class CmdOutputMetadata(BaseModel):
21
- """Additional metadata captured from PS1"""
22
-
23
- exit_code: int = Field(
24
- default=-1, description="The exit code of the last executed command."
25
- )
26
- pid: int = Field(
27
- default=-1, description="The process ID of the last executed command."
28
- )
29
- username: str | None = Field(
30
- default=None, description="The username of the current user."
31
- )
32
- hostname: str | None = Field(
33
- default=None, description="The hostname of the machine."
34
- )
35
- working_dir: str | None = Field(
36
- default=None, description="The current working directory."
37
- )
38
- py_interpreter_path: str | None = Field(
39
- default=None, description="The path to the current Python interpreter, if any."
40
- )
41
- prefix: str = Field(default="", description="Prefix to add to command output")
42
- suffix: str = Field(default="", description="Suffix to add to command output")
43
-
44
- @classmethod
45
- def to_ps1_prompt(cls) -> str:
46
- """Convert the required metadata into a PS1 prompt."""
47
- prompt = CMD_OUTPUT_PS1_BEGIN
48
- json_str = json.dumps(
49
- {
50
- "pid": "$!",
51
- "exit_code": "$?",
52
- "username": r"\u",
53
- "hostname": r"\h",
54
- "working_dir": r"$(pwd)",
55
- "py_interpreter_path": r'$(command -v python || echo "")',
56
- },
57
- indent=2,
58
- )
59
- # Make sure we escape double quotes in the JSON string
60
- # So that PS1 will keep them as part of the output
61
- prompt += json_str.replace('"', r"\"")
62
- prompt += CMD_OUTPUT_PS1_END + "\n" # Ensure there's a newline at the end
63
- return prompt
64
-
65
- @classmethod
66
- def matches_ps1_metadata(cls, string: str) -> list[re.Match[str]]:
67
- matches = []
68
- for match in CMD_OUTPUT_METADATA_PS1_REGEX.finditer(string):
69
- try:
70
- json.loads(match.group(1).strip()) # Try to parse as JSON
71
- matches.append(match)
72
- except json.JSONDecodeError:
73
- logger.debug(
74
- f"Failed to parse PS1 metadata - Skipping: [{match.group(1)}]"
75
- + traceback.format_exc()
76
- )
77
- continue # Skip if not valid JSON
78
- return matches
79
-
80
- @classmethod
81
- def from_ps1_match(cls, match: re.Match[str]) -> "CmdOutputMetadata":
82
- """Extract the required metadata from a PS1 prompt."""
83
- metadata = json.loads(match.group(1))
84
- # Create a copy of metadata to avoid modifying the original
85
- processed = metadata.copy()
86
- # Convert numeric fields
87
- if "pid" in metadata:
88
- try:
89
- processed["pid"] = int(float(str(metadata["pid"])))
90
- except (ValueError, TypeError):
91
- processed["pid"] = -1
92
- if "exit_code" in metadata:
93
- try:
94
- processed["exit_code"] = int(float(str(metadata["exit_code"])))
95
- except (ValueError, TypeError):
96
- logger.debug(
97
- f"Failed to parse exit code: {metadata['exit_code']}. "
98
- f"Setting to -1."
99
- )
100
- processed["exit_code"] = -1
101
- return cls(**processed)
@@ -1,22 +0,0 @@
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
- ]
@@ -1,113 +0,0 @@
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)
@@ -1,189 +0,0 @@
1
- """Abstract interface for terminal backends."""
2
-
3
- import os
4
- from abc import ABC, abstractmethod
5
-
6
- from openhands.tools.execute_bash.definition import (
7
- ExecuteBashAction,
8
- ExecuteBashObservation,
9
- )
10
-
11
-
12
- class TerminalInterface(ABC):
13
- """Abstract interface for terminal backends.
14
-
15
- This interface abstracts the low-level terminal operations, allowing
16
- different backends (tmux, subprocess, PowerShell) to be used with
17
- the same high-level session controller logic.
18
- """
19
-
20
- def __init__(
21
- self,
22
- work_dir: str,
23
- username: str | None = None,
24
- ):
25
- """Initialize the terminal interface.
26
-
27
- Args:
28
- work_dir: Working directory for the terminal
29
- username: Optional username for the terminal session
30
- """
31
- self.work_dir = work_dir
32
- self.username = username
33
- self._initialized = False
34
- self._closed = False
35
-
36
- @abstractmethod
37
- def initialize(self) -> None:
38
- """Initialize the terminal backend.
39
-
40
- This should set up the terminal session, configure the shell,
41
- and prepare it for command execution.
42
- """
43
- pass
44
-
45
- @abstractmethod
46
- def close(self) -> None:
47
- """Clean up the terminal backend.
48
-
49
- This should properly terminate the terminal session and
50
- clean up any resources.
51
- """
52
- pass
53
-
54
- @abstractmethod
55
- def send_keys(self, text: str, enter: bool = True) -> None:
56
- """Send text/keys to the terminal.
57
-
58
- Args:
59
- text: Text or key sequence to send
60
- enter: Whether to send Enter key after the text
61
- """
62
- pass
63
-
64
- @abstractmethod
65
- def read_screen(self) -> str:
66
- """Read the current terminal screen content.
67
-
68
- Returns:
69
- Current visible content of the terminal screen
70
- """
71
- pass
72
-
73
- @abstractmethod
74
- def clear_screen(self) -> None:
75
- """Clear the terminal screen and history."""
76
- pass
77
-
78
- @abstractmethod
79
- def interrupt(self) -> bool:
80
- """Send interrupt signal (Ctrl+C) to the terminal.
81
-
82
- Returns:
83
- True if interrupt was sent successfully, False otherwise
84
- """
85
- pass
86
-
87
- @abstractmethod
88
- def is_running(self) -> bool:
89
- """Check if a command is currently running in the terminal.
90
-
91
- Returns:
92
- True if a command is running, False otherwise
93
- """
94
- pass
95
-
96
- @property
97
- def initialized(self) -> bool:
98
- """Check if the terminal is initialized."""
99
- return self._initialized
100
-
101
- @property
102
- def closed(self) -> bool:
103
- """Check if the terminal is closed."""
104
- return self._closed
105
-
106
- def is_powershell(self) -> bool:
107
- """Check if this is a PowerShell terminal.
108
-
109
- Returns:
110
- True if this is a PowerShell terminal, False otherwise
111
- """
112
- return False
113
-
114
-
115
- class TerminalSessionBase(ABC):
116
- """Abstract base class for terminal sessions.
117
-
118
- This class defines the common interface for all terminal session implementations,
119
- including tmux-based, subprocess-based, and PowerShell-based sessions.
120
- """
121
-
122
- def __init__(
123
- self,
124
- work_dir: str,
125
- username: str | None = None,
126
- no_change_timeout_seconds: int | None = None,
127
- ):
128
- """Initialize the terminal session.
129
-
130
- Args:
131
- work_dir: Working directory for the session
132
- username: Optional username for the session
133
- no_change_timeout_seconds: Timeout for no output change
134
- """
135
- self.work_dir = work_dir
136
- self.username = username
137
- self.no_change_timeout_seconds = no_change_timeout_seconds
138
- self._initialized = False
139
- self._closed = False
140
- self._cwd = os.path.abspath(work_dir)
141
-
142
- @abstractmethod
143
- def initialize(self) -> None:
144
- """Initialize the terminal session."""
145
- pass
146
-
147
- @abstractmethod
148
- def execute(self, action: ExecuteBashAction) -> ExecuteBashObservation:
149
- """Execute a command in the terminal session.
150
-
151
- Args:
152
- action: The bash action to execute
153
-
154
- Returns:
155
- ExecuteBashObservation with the command result
156
- """
157
- pass
158
-
159
- @abstractmethod
160
- def close(self) -> None:
161
- """Clean up the terminal session."""
162
- pass
163
-
164
- @abstractmethod
165
- def interrupt(self) -> bool:
166
- """Interrupt the currently running command (equivalent to Ctrl+C).
167
-
168
- Returns:
169
- True if interrupt was successful, False otherwise
170
- """
171
- pass
172
-
173
- @abstractmethod
174
- def is_running(self) -> bool:
175
- """Check if a command is currently running.
176
-
177
- Returns:
178
- True if a command is running, False otherwise
179
- """
180
- pass
181
-
182
- @property
183
- def cwd(self) -> str:
184
- """Get the current working directory."""
185
- return self._cwd
186
-
187
- def __del__(self) -> None:
188
- """Ensure the session is closed when the object is destroyed."""
189
- self.close()