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.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- 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()
|