openhands-tools 1.9.1__tar.gz → 1.11.0__tar.gz

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.
Files changed (84) hide show
  1. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/PKG-INFO +1 -1
  2. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/impl.py +2 -0
  3. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/logging_fix.py +1 -1
  4. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/shell.py +8 -1
  5. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/glob/impl.py +7 -1
  6. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/grep/impl.py +13 -2
  7. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/planning_file_editor/definition.py +41 -3
  8. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/preset/planning.py +11 -2
  9. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/factory.py +3 -0
  10. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +2 -1
  11. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/terminal_session.py +15 -0
  12. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/tmux_terminal.py +34 -10
  13. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/utils/command.py +1 -1
  14. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands_tools.egg-info/PKG-INFO +1 -1
  15. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/pyproject.toml +1 -1
  16. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/__init__.py +0 -0
  17. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/apply_patch/__init__.py +0 -0
  18. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/apply_patch/core.py +0 -0
  19. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/apply_patch/definition.py +0 -0
  20. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/__init__.py +0 -0
  21. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/definition.py +0 -0
  22. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/impl_windows.py +0 -0
  23. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/browser_use/server.py +0 -0
  24. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/__init__.py +0 -0
  25. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/definition.py +0 -0
  26. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/impl.py +0 -0
  27. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/registration.py +0 -0
  28. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/templates/delegate_tool_description.j2 +0 -0
  29. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/delegate/visualizer.py +0 -0
  30. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/__init__.py +0 -0
  31. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/definition.py +0 -0
  32. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/editor.py +0 -0
  33. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/exceptions.py +0 -0
  34. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/impl.py +0 -0
  35. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  36. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/config.py +0 -0
  37. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  38. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  39. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  40. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  41. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/file_editor/utils/history.py +0 -0
  42. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/__init__.py +0 -0
  43. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  44. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/edit/definition.py +0 -0
  45. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/edit/impl.py +0 -0
  46. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  47. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  48. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  49. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  50. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  51. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  52. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  53. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  54. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  55. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/glob/__init__.py +0 -0
  56. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/glob/definition.py +0 -0
  57. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/grep/__init__.py +0 -0
  58. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/grep/definition.py +0 -0
  59. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  60. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  61. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/preset/__init__.py +0 -0
  62. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/preset/default.py +0 -0
  63. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/preset/gemini.py +0 -0
  64. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/preset/gpt5.py +0 -0
  65. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/py.typed +0 -0
  66. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/task_tracker/__init__.py +0 -0
  67. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/task_tracker/definition.py +0 -0
  68. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/__init__.py +0 -0
  69. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/constants.py +0 -0
  70. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/definition.py +0 -0
  71. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/impl.py +0 -0
  72. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/metadata.py +0 -0
  73. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  74. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  75. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/tom_consult/__init__.py +0 -0
  76. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/tom_consult/definition.py +0 -0
  77. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/tom_consult/executor.py +0 -0
  78. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/utils/__init__.py +0 -0
  79. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands/tools/utils/timeout.py +0 -0
  80. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  81. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  82. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands_tools.egg-info/requires.txt +0 -0
  83. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/openhands_tools.egg-info/top_level.txt +0 -0
  84. {openhands_tools-1.9.1 → openhands_tools-1.11.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.9.1
3
+ Version: 1.11.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
14
14
 
15
15
  from openhands.sdk.logger import DEBUG, get_logger
16
16
  from openhands.sdk.tool import ToolExecutor
17
+ from openhands.sdk.utils import sanitized_env
17
18
  from openhands.sdk.utils.async_executor import AsyncExecutor
18
19
  from openhands.tools.browser_use.definition import BrowserAction, BrowserObservation
19
20
  from openhands.tools.browser_use.server import CustomBrowserUseServer
@@ -43,6 +44,7 @@ def _install_chromium() -> bool:
43
44
  capture_output=True,
44
45
  text=True,
45
46
  timeout=300, # 5 minutes timeout for installation
47
+ env=sanitized_env(),
46
48
  )
47
49
 
48
50
  if result.returncode == 0:
@@ -15,7 +15,7 @@ from openhands.sdk.utils.deprecation import warn_cleanup
15
15
 
16
16
  warn_cleanup(
17
17
  "Monkey patching to prevent browser_use logging interference",
18
- cleanup_by="1.10.0",
18
+ cleanup_by="1.15.0",
19
19
  details=(
20
20
  "This workaround should be removed once browser_use PR #3717 "
21
21
  "(https://github.com/browser-use/browser-use/pull/3717) is merged "
@@ -2,6 +2,7 @@ import os
2
2
  import subprocess
3
3
  import time
4
4
 
5
+ from openhands.sdk.utils import sanitized_env
5
6
  from openhands.sdk.utils.truncate import maybe_truncate
6
7
  from openhands.tools.file_editor.utils.constants import (
7
8
  CONTENT_TRUNCATED_NOTICE,
@@ -32,7 +33,12 @@ def run_shell_cmd(
32
33
  process: subprocess.Popen[str] | None = None
33
34
  try:
34
35
  process = subprocess.Popen(
35
- cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
36
+ cmd,
37
+ shell=True,
38
+ stdout=subprocess.PIPE,
39
+ stderr=subprocess.PIPE,
40
+ text=True,
41
+ env=sanitized_env(),
36
42
  )
37
43
 
38
44
  stdout, stderr = process.communicate(timeout=timeout)
@@ -65,6 +71,7 @@ def check_tool_installed(tool_name: str) -> bool:
65
71
  check=True,
66
72
  cwd=os.getcwd(),
67
73
  capture_output=True,
74
+ env=sanitized_env(),
68
75
  )
69
76
  return True
70
77
  except (subprocess.CalledProcessError, FileNotFoundError):
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from typing import TYPE_CHECKING
9
9
 
10
10
  from openhands.sdk.tool import ToolExecutor
11
+ from openhands.sdk.utils import sanitized_env
11
12
 
12
13
 
13
14
  if TYPE_CHECKING:
@@ -149,7 +150,12 @@ class GlobExecutor(ToolExecutor[GlobAction, GlobObservation]):
149
150
 
150
151
  # Execute ripgrep
151
152
  result = subprocess.run(
152
- cmd, capture_output=True, text=True, timeout=30, check=False
153
+ cmd,
154
+ capture_output=True,
155
+ text=True,
156
+ timeout=30,
157
+ check=False,
158
+ env=sanitized_env(),
153
159
  )
154
160
 
155
161
  # Parse output into file paths
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from openhands.sdk.tool import ToolExecutor
9
+ from openhands.sdk.utils import sanitized_env
9
10
 
10
11
 
11
12
  if TYPE_CHECKING:
@@ -154,7 +155,12 @@ class GrepExecutor(ToolExecutor[GrepAction, GrepObservation]):
154
155
 
155
156
  # Execute ripgrep
156
157
  result = subprocess.run(
157
- cmd, capture_output=True, text=True, timeout=30, check=False
158
+ cmd,
159
+ capture_output=True,
160
+ text=True,
161
+ timeout=30,
162
+ check=False,
163
+ env=sanitized_env(),
158
164
  )
159
165
 
160
166
  # Parse output into file paths
@@ -209,7 +215,12 @@ class GrepExecutor(ToolExecutor[GrepAction, GrepObservation]):
209
215
 
210
216
  # Execute grep
211
217
  result = subprocess.run(
212
- cmd, capture_output=True, text=True, timeout=30, check=False
218
+ cmd,
219
+ capture_output=True,
220
+ text=True,
221
+ timeout=30,
222
+ check=False,
223
+ env=sanitized_env(),
213
224
  )
214
225
 
215
226
  # Parse output into file paths
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
8
8
  if TYPE_CHECKING:
9
9
  from openhands.sdk.conversation.state import ConversationState
10
10
 
11
+ from openhands.sdk.logger import get_logger
11
12
  from openhands.sdk.tool import (
12
13
  ToolAnnotations,
13
14
  ToolDefinition,
@@ -20,7 +21,12 @@ from openhands.tools.file_editor.definition import (
20
21
  )
21
22
 
22
23
 
23
- # Hardcoded plan filename
24
+ logger = get_logger(__name__)
25
+
26
+ # Default config directory and plan filename
27
+ # PLAN.md is now stored in .agents_tmp/ to keep workspace root clean
28
+ # and separate agent temporary files from user content
29
+ DEFAULT_CONFIG_DIR = ".agents_tmp"
24
30
  PLAN_FILENAME = "PLAN.md"
25
31
 
26
32
 
@@ -62,11 +68,17 @@ class PlanningFileEditorTool(
62
68
  def create(
63
69
  cls,
64
70
  conv_state: "ConversationState",
71
+ plan_path: str | None = None,
65
72
  ) -> Sequence["PlanningFileEditorTool"]:
66
73
  """Initialize PlanningFileEditorTool.
67
74
 
68
75
  Args:
69
76
  conv_state: Conversation state to get working directory from.
77
+ plan_path: Optional absolute path to PLAN.md file. If not provided,
78
+ defaults to {working_dir}/.agents_tmp/PLAN.md.
79
+
80
+ Raises:
81
+ ValueError: If plan_path is provided but is not an absolute path.
70
82
  """
71
83
  # Import here to avoid circular imports
72
84
  from openhands.tools.planning_file_editor.impl import (
@@ -74,8 +86,31 @@ class PlanningFileEditorTool(
74
86
  )
75
87
 
76
88
  working_dir = conv_state.workspace.working_dir
77
- workspace_root = Path(working_dir).resolve()
78
- plan_path = str(workspace_root / PLAN_FILENAME)
89
+
90
+ # Validate plan_path is absolute if provided
91
+ if plan_path is not None and not Path(plan_path).is_absolute():
92
+ raise ValueError(f"plan_path must be an absolute path, got: {plan_path}")
93
+
94
+ # Use provided plan_path or fall back to .agents_tmp/PLAN.md at workspace root
95
+ if plan_path is None:
96
+ workspace_root = Path(working_dir).resolve()
97
+
98
+ # Check for legacy PLAN.md at workspace root
99
+ legacy_plan_path = workspace_root / PLAN_FILENAME
100
+ if legacy_plan_path.exists():
101
+ # Use legacy location for backward compatibility
102
+ new_recommended_path = (
103
+ workspace_root / DEFAULT_CONFIG_DIR / PLAN_FILENAME
104
+ )
105
+ logger.warning(
106
+ f"Found PLAN.md at legacy location {legacy_plan_path}. "
107
+ f"Consider moving it to {new_recommended_path} "
108
+ f"for consistency with OpenHands conventions."
109
+ )
110
+ plan_path = str(legacy_plan_path)
111
+ else:
112
+ # Use new default location
113
+ plan_path = str(workspace_root / DEFAULT_CONFIG_DIR / PLAN_FILENAME)
79
114
 
80
115
  # Initialize PLAN.md with headers if it doesn't exist
81
116
  plan_file = Path(plan_path)
@@ -83,7 +118,10 @@ class PlanningFileEditorTool(
83
118
  # Import here to avoid circular imports
84
119
  from openhands.tools.preset.planning import get_plan_headers
85
120
 
121
+ # Ensure parent directory exists
122
+ plan_file.parent.mkdir(parents=True, exist_ok=True)
86
123
  plan_file.write_text(get_plan_headers())
124
+ logger.info(f"Created new PLAN.md at {plan_path}")
87
125
 
88
126
  # Create executor with restricted edit access to PLAN.md only
89
127
  executor = PlanningFileEditorExecutor(
@@ -102,9 +102,13 @@ def register_planning_tools() -> None:
102
102
  logger.debug("Tool: PlanningFileEditorTool registered.")
103
103
 
104
104
 
105
- def get_planning_tools() -> list[Tool]:
105
+ def get_planning_tools(plan_path: str | None = None) -> list[Tool]:
106
106
  """Get the planning agent tool specifications.
107
107
 
108
+ Args:
109
+ plan_path: Optional absolute path to PLAN.md file. If provided, will be
110
+ passed to PlanningFileEditorTool via params.
111
+
108
112
  Returns:
109
113
  List of tools optimized for planning and analysis tasks, including
110
114
  file viewing and PLAN.md editing capabilities for advanced
@@ -117,10 +121,15 @@ def get_planning_tools() -> list[Tool]:
117
121
  from openhands.tools.grep import GrepTool
118
122
  from openhands.tools.planning_file_editor import PlanningFileEditorTool
119
123
 
124
+ # Build params for PlanningFileEditorTool if plan_path is provided
125
+ planning_tool_params = {}
126
+ if plan_path:
127
+ planning_tool_params["plan_path"] = plan_path
128
+
120
129
  return [
121
130
  Tool(name=GlobTool.name),
122
131
  Tool(name=GrepTool.name),
123
- Tool(name=PlanningFileEditorTool.name),
132
+ Tool(name=PlanningFileEditorTool.name, params=planning_tool_params),
124
133
  ]
125
134
 
126
135
 
@@ -5,6 +5,7 @@ import subprocess
5
5
  from typing import Literal
6
6
 
7
7
  from openhands.sdk.logger import get_logger
8
+ from openhands.sdk.utils import sanitized_env
8
9
  from openhands.tools.terminal.terminal.terminal_session import TerminalSession
9
10
 
10
11
 
@@ -19,6 +20,7 @@ def _is_tmux_available() -> bool:
19
20
  capture_output=True,
20
21
  text=True,
21
22
  timeout=5.0,
23
+ env=sanitized_env(),
22
24
  )
23
25
  return result.returncode == 0
24
26
  except (subprocess.TimeoutExpired, FileNotFoundError):
@@ -40,6 +42,7 @@ def _is_powershell_available() -> bool:
40
42
  capture_output=True,
41
43
  text=True,
42
44
  timeout=5.0,
45
+ env=sanitized_env(),
43
46
  )
44
47
  return result.returncode == 0
45
48
  except (subprocess.TimeoutExpired, FileNotFoundError):
@@ -13,6 +13,7 @@ import time
13
13
  from collections import deque
14
14
 
15
15
  from openhands.sdk.logger import get_logger
16
+ from openhands.sdk.utils import sanitized_env
16
17
  from openhands.tools.terminal.constants import (
17
18
  CMD_OUTPUT_PS1_BEGIN,
18
19
  CMD_OUTPUT_PS1_END,
@@ -106,7 +107,7 @@ class SubprocessTerminal(TerminalInterface):
106
107
  logger.info(f"Using shell: {resolved_shell_path}")
107
108
 
108
109
  # Inherit environment variables from the parent process
109
- env = os.environ.copy()
110
+ env = sanitized_env()
110
111
  env["PS1"] = self.PS1
111
112
  env["PS2"] = ""
112
113
  env["TERM"] = "xterm-256color"
@@ -5,8 +5,10 @@ import time
5
5
  from enum import Enum
6
6
 
7
7
  from openhands.sdk.logger import get_logger
8
+ from openhands.sdk.utils import maybe_truncate
8
9
  from openhands.tools.terminal.constants import (
9
10
  CMD_OUTPUT_PS1_END,
11
+ MAX_CMD_OUTPUT_SIZE,
10
12
  NO_CHANGE_TIMEOUT_SECONDS,
11
13
  POLL_INTERVAL,
12
14
  TIMEOUT_MESSAGE_TEMPLATE,
@@ -183,6 +185,10 @@ class TerminalSession(TerminalSessionBase):
183
185
  raw_command_output,
184
186
  metadata,
185
187
  )
188
+ command_output = maybe_truncate(
189
+ command_output, truncate_after=MAX_CMD_OUTPUT_SIZE
190
+ )
191
+
186
192
  self.prev_status = TerminalCommandStatus.COMPLETED
187
193
  self.prev_output = "" # Reset previous command output
188
194
  self._ready_for_next_command()
@@ -221,6 +227,9 @@ class TerminalSession(TerminalSessionBase):
221
227
  metadata,
222
228
  continue_prefix="[Below is the output of the previous command.]\n",
223
229
  )
230
+ command_output = maybe_truncate(
231
+ command_output, truncate_after=MAX_CMD_OUTPUT_SIZE
232
+ )
224
233
  return TerminalObservation.from_text(
225
234
  command=command,
226
235
  text=command_output,
@@ -257,6 +266,9 @@ class TerminalSession(TerminalSessionBase):
257
266
  metadata,
258
267
  continue_prefix="[Below is the output of the previous command.]\n",
259
268
  )
269
+ command_output = maybe_truncate(
270
+ command_output, truncate_after=MAX_CMD_OUTPUT_SIZE
271
+ )
260
272
  return TerminalObservation.from_text(
261
273
  command=command,
262
274
  exit_code=metadata.exit_code,
@@ -391,6 +403,9 @@ class TerminalSession(TerminalSessionBase):
391
403
  metadata,
392
404
  continue_prefix="[Below is the output of the previous command.]\n",
393
405
  )
406
+ command_output = maybe_truncate(
407
+ command_output, truncate_after=MAX_CMD_OUTPUT_SIZE
408
+ )
394
409
  obs = TerminalObservation.from_text(
395
410
  command=command,
396
411
  text=command_output,
@@ -1,12 +1,13 @@
1
1
  """Tmux-based terminal backend implementation."""
2
2
 
3
- import os
4
3
  import time
5
4
  import uuid
6
5
 
7
6
  import libtmux
7
+ from libtmux.exc import TmuxObjectDoesNotExist
8
8
 
9
9
  from openhands.sdk.logger import get_logger
10
+ from openhands.sdk.utils import sanitized_env
10
11
  from openhands.tools.terminal.constants import HISTORY_LIMIT
11
12
  from openhands.tools.terminal.metadata import CmdOutputMetadata
12
13
  from openhands.tools.terminal.terminal import TerminalInterface
@@ -41,7 +42,8 @@ class TmuxTerminal(TerminalInterface):
41
42
  if self._initialized:
42
43
  return
43
44
 
44
- self.server = libtmux.Server()
45
+ env = sanitized_env()
46
+ self.server = libtmux.Server(environment=env)
45
47
  _shell_command = "/bin/bash"
46
48
  if self.username in ["root", "openhands"]:
47
49
  # This starts a non-login (new) shell for the given user
@@ -51,14 +53,36 @@ class TmuxTerminal(TerminalInterface):
51
53
 
52
54
  logger.debug(f"Initializing tmux terminal with command: {window_command}")
53
55
  session_name = f"openhands-{self.username}-{uuid.uuid4()}"
54
- self.session = self.server.new_session(
55
- session_name=session_name,
56
- start_directory=self.work_dir,
57
- kill_session=True,
58
- x=1000,
59
- y=1000,
60
- )
61
- for k, v in os.environ.items():
56
+
57
+ # Retry session creation to handle race conditions in libtmux
58
+ # where the session is created but can't be found immediately
59
+ max_retries = 3
60
+ retry_delay = 0.5
61
+ last_error = None
62
+ for attempt in range(max_retries):
63
+ try:
64
+ self.session = self.server.new_session(
65
+ session_name=session_name,
66
+ start_directory=self.work_dir,
67
+ kill_session=True,
68
+ x=1000,
69
+ y=1000,
70
+ )
71
+ break
72
+ except TmuxObjectDoesNotExist as e:
73
+ last_error = e
74
+ if attempt < max_retries - 1:
75
+ logger.warning(
76
+ f"Tmux session creation failed (attempt {attempt + 1}/"
77
+ f"{max_retries}), retrying in {retry_delay}s: {e}"
78
+ )
79
+ time.sleep(retry_delay)
80
+ retry_delay *= 2 # Exponential backoff
81
+ else:
82
+ raise RuntimeError(
83
+ f"Failed to create tmux session after {max_retries} attempts"
84
+ ) from last_error
85
+ for k, v in env.items():
62
86
  self.session.set_environment(k, v)
63
87
 
64
88
  # Set history limit to a large number to avoid losing history
@@ -141,7 +141,7 @@ def escape_bash_special_chars(command: str) -> str:
141
141
  remaining = command[last_pos:]
142
142
  parts.append(remaining)
143
143
  return "".join(parts)
144
- except (ParsingError, NotImplementedError, TypeError):
144
+ except (ParsingError, NotImplementedError, TypeError, AttributeError):
145
145
  logger.debug(
146
146
  f"Failed to parse bash commands for special characters escape\n[input]: "
147
147
  f"{command}\n[warning]: {traceback.format_exc()}\nThe original command "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.9.1
3
+ Version: 1.11.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.9.1"
3
+ version = "1.11.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"