openhands-tools 1.58.2__tar.gz → 1.60.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 (111) hide show
  1. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/PKG-INFO +3 -2
  2. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/apply_patch/core.py +4 -0
  3. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/server.py +20 -2
  4. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/child_conversation/definition.py +7 -4
  5. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/editor.py +10 -1
  6. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/glob/impl.py +7 -10
  7. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task/definition.py +2 -1
  8. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task_tracker/definition.py +3 -130
  9. openhands_tools-1.60.0/openhands/tools/terminal/env.py +88 -0
  10. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +3 -0
  11. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/tmux_terminal.py +3 -0
  12. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/windows_terminal.py +29 -45
  13. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands_tools.egg-info/PKG-INFO +3 -2
  14. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands_tools.egg-info/requires.txt +2 -1
  15. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/pyproject.toml +3 -2
  16. openhands_tools-1.58.2/openhands/tools/terminal/env.py +0 -39
  17. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/__init__.py +0 -0
  18. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/apply_patch/__init__.py +0 -0
  19. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/apply_patch/definition.py +0 -0
  20. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  21. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/ask_oracle/definition.py +0 -0
  22. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/ask_oracle/impl.py +0 -0
  23. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/__init__.py +0 -0
  24. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/definition.py +0 -0
  25. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/event_storage.py +0 -0
  26. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/impl.py +0 -0
  27. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  28. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/browser_use/recording.py +0 -0
  29. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/child_conversation/__init__.py +0 -0
  30. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/child_conversation/impl.py +0 -0
  31. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/delegate/__init__.py +0 -0
  32. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/delegate/definition.py +0 -0
  33. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/delegate/impl.py +0 -0
  34. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/delegate/visualizer.py +0 -0
  35. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/__init__.py +0 -0
  36. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/definition.py +0 -0
  37. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/exceptions.py +0 -0
  38. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/impl.py +0 -0
  39. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  40. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/config.py +0 -0
  41. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  42. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  43. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  44. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  45. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/history.py +0 -0
  46. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  47. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/__init__.py +0 -0
  48. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  49. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/edit/definition.py +0 -0
  50. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/edit/impl.py +0 -0
  51. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/file_change.py +0 -0
  52. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  53. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  54. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  55. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  56. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  57. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  58. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  59. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  60. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  61. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/glob/__init__.py +0 -0
  62. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/glob/definition.py +0 -0
  63. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/grep/__init__.py +0 -0
  64. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/grep/definition.py +0 -0
  65. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/grep/impl.py +0 -0
  66. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  67. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  68. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  69. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/__init__.py +0 -0
  70. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/default.py +0 -0
  71. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/gemini.py +0 -0
  72. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/gpt5.py +0 -0
  73. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/planning.py +0 -0
  74. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  75. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  76. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/subagents/default.md +0 -0
  77. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  78. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/preset/task_outcome.py +0 -0
  79. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/py.typed +0 -0
  80. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task/__init__.py +0 -0
  81. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task/impl.py +0 -0
  82. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task/manager.py +0 -0
  83. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/task_tracker/__init__.py +0 -0
  84. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/__init__.py +0 -0
  85. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/constants.py +0 -0
  86. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/definition.py +0 -0
  87. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/descriptions.py +0 -0
  88. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/impl.py +0 -0
  89. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/metadata.py +0 -0
  90. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  91. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  92. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  93. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/process_priority.py +0 -0
  94. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  95. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  96. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  97. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  98. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/utils/command.py +0 -0
  99. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  100. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/tom_consult/__init__.py +0 -0
  101. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/tom_consult/definition.py +0 -0
  102. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/tom_consult/executor.py +0 -0
  103. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/utils/__init__.py +0 -0
  104. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/utils/timeout.py +0 -0
  105. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/workflow/__init__.py +0 -0
  106. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/workflow/definition.py +0 -0
  107. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands/tools/workflow/impl.py +0 -0
  108. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  109. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  110. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/openhands_tools.egg-info/top_level.txt +0 -0
  111. {openhands_tools-1.58.2 → openhands_tools-1.60.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.58.2
3
+ Version: 1.60.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,7 +14,8 @@ Requires-Dist: tree-sitter-bash>=0.23
14
14
  Requires-Dist: binaryornot>=0.6.0
15
15
  Requires-Dist: cachetools
16
16
  Requires-Dist: filelock>=3.20.1
17
- Requires-Dist: libtmux>=0.53.0
17
+ Requires-Dist: libtmux>=0.62.0
18
+ Requires-Dist: psutil>=7.2.2
18
19
  Requires-Dist: pydantic>=2.13.5
19
20
  Requires-Dist: browser-use>=0.8.0
20
21
  Requires-Dist: func-timeout>=4.3.5
@@ -455,6 +455,10 @@ def load_files(paths: list[str], open_fn: Callable[[str], str]) -> dict[str, str
455
455
  FileNotFoundError
456
456
  ) as exc: # pragma: no cover - exercised via higher-level tests
457
457
  raise DiffError(f"Delete File Error: Missing File: {path}") from exc
458
+ except (OSError, UnicodeDecodeError) as exc:
459
+ # An unreadable path (a directory, a denied file, bytes that are not
460
+ # UTF-8 text) must surface as a tool error, not escape the executor.
461
+ raise DiffError(f"Cannot read {path}: {exc}") from exc
458
462
  return orig
459
463
 
460
464
 
@@ -1,4 +1,5 @@
1
1
  from browser_use.dom.markdown_extractor import extract_clean_markdown
2
+ from browser_use.dom.service import DomService
2
3
 
3
4
  from openhands.sdk import get_logger
4
5
  from openhands.tools.browser_use.logging_fix import LogSafeBrowserUseServer
@@ -259,10 +260,27 @@ class CustomBrowserUseServer(LogSafeBrowserUseServer):
259
260
  if not self.browser_session:
260
261
  return "Error: No browser session active"
261
262
 
262
- # Extract clean markdown using the new method
263
+ target_id = self.browser_session.agent_focus_target_id
264
+ if not target_id:
265
+ return "Error: No active page"
266
+
267
+ # Read a fresh DOM tree. The session path reuses the tree cached by the
268
+ # last browser_get_state, which is stale after clicks, timers or
269
+ # history navigation. A separate DomService leaves that cache and its
270
+ # element indices untouched.
271
+ profile = self.browser_session.browser_profile
272
+ dom_service = DomService(
273
+ browser_session=self.browser_session,
274
+ cross_origin_iframes=profile.cross_origin_iframes,
275
+ paint_order_filtering=profile.paint_order_filtering,
276
+ max_iframes=profile.max_iframes,
277
+ max_iframe_depth=profile.max_iframe_depth,
278
+ )
263
279
  try:
264
280
  content, content_stats = await extract_clean_markdown(
265
- browser_session=self.browser_session, extract_links=extract_links
281
+ dom_service=dom_service,
282
+ target_id=target_id,
283
+ extract_links=extract_links,
266
284
  )
267
285
  except Exception as e:
268
286
  logger.exception(
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, ClassVar, Literal, Self
6
6
  from pydantic import Field
7
7
  from rich.text import Text
8
8
 
9
+ from openhands.sdk.event.types import UnmaskedID
9
10
  from openhands.sdk.tool.registry import register_tool
10
11
  from openhands.sdk.tool.tool import (
11
12
  Action,
@@ -36,7 +37,8 @@ class StartChildConversationAction(Action):
36
37
  default="worktree",
37
38
  description=(
38
39
  "'worktree' gives the child its own git worktree and branch when the "
39
- "workspace is a git repository; 'shared' runs it in the same directory."
40
+ "workspace has a usable git base commit; otherwise it shares the "
41
+ "directory. 'shared' always runs it in the same directory."
40
42
  ),
41
43
  )
42
44
 
@@ -54,8 +56,8 @@ class StartChildConversationAction(Action):
54
56
  class StartChildConversationObservation(Observation):
55
57
  """Result of a child conversation launch."""
56
58
 
57
- conversation_id: str | None = None
58
- parent_conversation_id: str | None = None
59
+ conversation_id: UnmaskedID | None = None
60
+ parent_conversation_id: UnmaskedID | None = None
59
61
  status: str | None = None
60
62
  title: str | None = None
61
63
  url: str | None = None
@@ -92,7 +94,8 @@ _DESCRIPTION = (
92
94
  "conversation's history, so include the goal, relevant paths, constraints "
93
95
  "and the expected deliverable. Use `title` to name it. `isolation` controls "
94
96
  "the workspace: `worktree` (default) gives the child its own git worktree "
95
- "and branch when the workspace is a git repository; the worktree starts "
97
+ "and branch when the workspace has a usable git base commit; otherwise "
98
+ "it shares the directory and reports `shared` isolation. The worktree starts "
96
99
  "from the repository's default branch, not from this conversation's branch "
97
100
  "or uncommitted changes, so use `shared` when the child must see them. "
98
101
  "`shared` runs it in the same directory; backends that manage workspaces "
@@ -486,8 +486,12 @@ class FileEditor:
486
486
  content, fall back to UTF-8 so an edit may add characters (arrows, emoji,
487
487
  CJK, ...) the original single-byte encoding lacks, instead of failing and
488
488
  truncating the file. Note that the fallback transcodes the whole file to
489
- UTF-8.
489
+ UTF-8. Editing through a symlink writes to the link's target: a rename
490
+ onto the link path would replace the link with a regular file.
490
491
  """
492
+ if path.is_symlink():
493
+ path = path.resolve()
494
+
491
495
  default = self._encoding_manager.default_encoding
492
496
  if encoding != default and not _is_encodable(file_text, encoding):
493
497
  logger.warning(
@@ -572,6 +576,11 @@ class FileEditor:
572
576
  new_lines.append(line)
573
577
  history_lines.append(line)
574
578
 
579
+ # A final line without a trailing newline would fuse with the inserted
580
+ # content; terminate it first so the insert starts on its own line.
581
+ if new_lines and not new_lines[-1].endswith("\n"):
582
+ new_lines[-1] += "\n"
583
+
575
584
  # Insert new content
576
585
  for line in new_str_lines:
577
586
  new_lines.append(line + "\n")
@@ -168,17 +168,14 @@ class GlobExecutor(ToolExecutor[GlobAction, GlobObservation]):
168
168
  env=sanitized_env(),
169
169
  )
170
170
 
171
- # Parse output into file paths
172
- file_paths = []
171
+ # Parse output into file paths (ripgrep already sorted by --sortr=modified)
172
+ lines = []
173
173
  if result.stdout:
174
- for line in result.stdout.strip().split("\n"):
175
- if line:
176
- file_paths.append(str(Path(line).resolve()))
177
- # Limit to first 100 files
178
- if len(file_paths) >= 100:
179
- break
180
-
181
- truncated = len(file_paths) >= 100
174
+ lines = [line for line in result.stdout.strip().split("\n") if line]
175
+
176
+ # Limit to the first 100 files; truncated only if matches were actually dropped
177
+ truncated = len(lines) > 100
178
+ file_paths = [str(Path(line).resolve()) for line in lines[:100]]
182
179
 
183
180
  return file_paths, truncated
184
181
 
@@ -17,6 +17,7 @@ from pydantic.json_schema import SkipJsonSchema
17
17
  from rich.text import Text
18
18
 
19
19
  from openhands.sdk import ImageContent, TextContent
20
+ from openhands.sdk.event.types import UnmaskedID
20
21
  from openhands.sdk.logger import get_logger
21
22
  from openhands.sdk.subagent import (
22
23
  AgentDefinition,
@@ -75,7 +76,7 @@ class TaskAction(Action):
75
76
  class TaskObservation(Observation):
76
77
  """Observation from a task execution."""
77
78
 
78
- task_id: str = Field(description="The unique identifier of the task.")
79
+ task_id: UnmaskedID = Field(description="The unique identifier of the task.")
79
80
  subagent: str = Field(description="The subagent of the task.")
80
81
  status: str = Field(description="The status of the task.")
81
82
 
@@ -284,138 +284,11 @@ class TaskTrackerExecutor(ToolExecutor[TaskTrackerAction, TaskTrackerObservation
284
284
  pass
285
285
 
286
286
 
287
- # Tool definition with detailed description
288
- TASK_TRACKER_DESCRIPTION = """This tool provides structured task management capabilities for development workflows.
289
- It enables systematic tracking of work items, progress monitoring, and efficient
290
- organization of complex development activities.
287
+ TASK_TRACKER_DESCRIPTION = """Maintain a visible task list for complex, multi-phase work, multiple requirements, or an explicit request for a plan. Skip tracking for trivial, atomic, few-step tasks and informational discussion; implement those directly. When uncertain, favor tracking.
291
288
 
292
- The tool maintains visibility into project status and helps communicate
293
- progress effectively to users.
289
+ At project start, capture requirements as precise, actionable tasks and break complex work into manageable units. View the current list before editing; plan replaces the full list, so preserve relevant tasks and progress. Update the list as work progresses, remove obsolete tasks, and add discovered issues or dependencies.
294
290
 
295
- ## Application Guidelines
296
-
297
- Utilize this tool in the following situations:
298
-
299
- 1. Multi-phase development work - When projects involve multiple sequential or
300
- parallel activities
301
- 2. Complex implementation tasks - Work requiring systematic planning and
302
- coordination across multiple components
303
- 3. Explicit user request for task organization - When users specifically ask
304
- for structured task management
305
- 4. Multiple concurrent requirements - When users present several work items
306
- that need coordination
307
- 5. Project initiation - Capture and organize user requirements at project start
308
- 6. Work commencement - Update task status to in_progress before beginning
309
- implementation. Maintain focus by limiting active work to one task
310
- 7. Task completion - Update status to done and identify any additional work
311
- that emerged during implementation
312
-
313
- ## Situations Where Tool Usage Is Unnecessary
314
-
315
- Avoid using this tool when:
316
-
317
- 1. Single atomic tasks that require no decomposition
318
- 2. Trivial operations where tracking adds no organizational value
319
- 3. Simple activities completable in minimal steps
320
- 4. Pure information exchange or discussion
321
-
322
- Note: For single straightforward tasks, proceed with direct implementation
323
- rather than creating tracking overhead.
324
-
325
- ## Usage Scenarios
326
-
327
- **Scenario A: Feature Development with Validation**
328
- User request: "Build a user authentication system with login/logout functionality.
329
- Don't forget to include input validation and error handling!"
330
-
331
- Response approach: I'll implement a user authentication system with comprehensive
332
- validation. Let me organize this work systematically.
333
-
334
- *Task breakdown includes:*
335
- 1. Design authentication API endpoints and data models
336
- 2. Implement user login/logout controllers with session management
337
- 3. Add client-side form validation and error display
338
- 4. Create backend input sanitization and security checks
339
- 5. Implement comprehensive error handling for edge cases
340
- 6. Perform integration testing and fix any issues
341
-
342
- **Scenario B: Codebase Refactoring**
343
- User request: "I need to update all instances of the method 'fetchData' to
344
- 'retrieveInformation' throughout the entire project"
345
-
346
- Response approach: Let me first analyze your codebase to identify all locations
347
- where 'fetchData' appears.
348
-
349
- *After code analysis*
350
-
351
- I've located 12 occurrences of 'fetchData' across 6 files in your project.
352
- I'll create a systematic plan to update these references.
353
-
354
- *Organized task list includes specific file-by-file updates*
355
-
356
- **Scenario C: Multi-feature Development**
357
- User request: "Set up a blog platform with these components: admin dashboard,
358
- article management, comment system, and user profiles."
359
-
360
- Response approach: I'll help build your blog platform. Let me break down these
361
- components into manageable implementation phases.
362
-
363
- *Creates structured plan with each feature decomposed into specific development tasks*
364
-
365
- ## Counter-examples - Direct Implementation Preferred
366
-
367
- **Counter-example A: Simple inquiry**
368
- User request: "What's the syntax for a for loop in JavaScript?"
369
-
370
- Response approach: Here's how to write a for loop in JavaScript:
371
-
372
- ```javascript
373
- for (let i = 0; i < array.length; i++) {
374
- console.log(array[i]);
375
- }
376
- ```
377
-
378
- *No task tracking needed - single informational response*
379
-
380
- **Counter-example B: Minor modification**
381
- User request: "Please add a docstring to the processPayment function explaining
382
- its parameters."
383
-
384
- Response approach: I'll add a docstring to explain the processPayment function.
385
-
386
- *Direct edit without task tracking for simple modification*
387
-
388
- ## Status Management and Workflow
389
-
390
- 1. **Status Values**: Track work using these states:
391
- - todo: Not yet initiated
392
- - in_progress: Currently active (maintain single focus)
393
- - done: Successfully completed
394
-
395
- 2. **Workflow Practices**:
396
- - Update status dynamically as work progresses
397
- - Mark completion immediately upon task finish
398
- - Limit active work to ONE task at any given time
399
- - Complete current activities before initiating new ones
400
- - Remove obsolete tasks from tracking entirely
401
-
402
- 3. **Completion Criteria**:
403
- - Mark tasks as done only when fully achieved
404
- - Keep status as in_progress if errors, blocks, or partial completion exist
405
- - Create new tasks for discovered issues or dependencies
406
- - Never mark done when:
407
- - Test suites are failing
408
- - Implementation remains incomplete
409
- - Unresolved errors persist
410
- - Required resources are unavailable
411
-
412
- 4. **Task Organization**:
413
- - Write precise, actionable descriptions
414
- - Decompose complex work into manageable units
415
- - Use descriptive, clear naming conventions
416
-
417
- When uncertain, favor using this tool. Proactive task management demonstrates
418
- systematic approach and ensures comprehensive requirement fulfillment.""" # noqa: E501
291
+ Statuses: todo = not started; in_progress = active; done = fully achieved. Mark a task in_progress before starting, keep at most one task in_progress, and finish it before starting another. Mark it done promptly only when completely successful. Leave incomplete, blocked, or failing work in_progress; never mark done while tests fail, errors remain, implementation is partial, or required resources are unavailable.""" # noqa: E501
419
292
 
420
293
 
421
294
  class TaskTrackerTool(ToolDefinition[TaskTrackerAction, TaskTrackerObservation]):
@@ -0,0 +1,88 @@
1
+ """Environment helpers for terminal backends."""
2
+
3
+ import codecs
4
+ import locale
5
+ import os
6
+ import re
7
+ from collections.abc import Mapping
8
+
9
+ from openhands.sdk.logger import get_logger
10
+ from openhands.sdk.utils import sanitized_env
11
+
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
16
+ UTF8_FALLBACK_LOCALE = "C.UTF-8"
17
+
18
+
19
+ def normalize_terminal_env(
20
+ extra_env: Mapping[str, str] | None,
21
+ ) -> dict[str, str] | None:
22
+ """Validate and copy client-provided terminal environment variables."""
23
+ if extra_env is None:
24
+ return None
25
+
26
+ normalized: dict[str, str] = {}
27
+ for key, value in extra_env.items():
28
+ if not isinstance(key, str) or not ENV_VAR_NAME_RE.match(key):
29
+ raise ValueError(f"Invalid terminal environment variable name: {key!r}")
30
+ if not isinstance(value, str):
31
+ raise TypeError(
32
+ "Terminal environment variable values must be strings: "
33
+ f"{key!r}={value!r}"
34
+ )
35
+ normalized[key] = value
36
+ return normalized
37
+
38
+
39
+ def build_terminal_env(extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
40
+ """Return the sanitized process environment plus client-provided overrides."""
41
+ env = sanitized_env()
42
+ normalized = normalize_terminal_env(extra_env)
43
+ if normalized:
44
+ env.update(normalized)
45
+ env = sanitized_env(env)
46
+ return env
47
+
48
+
49
+ def has_utf8_locale(environ: Mapping[str, str]) -> bool:
50
+ """Apply tmux's check: TMUX is set, or the first non-empty of LC_ALL,
51
+ LC_CTYPE, LANG names UTF-8."""
52
+ if "TMUX" in environ:
53
+ return True
54
+ for name in ("LC_ALL", "LC_CTYPE", "LANG"):
55
+ value = environ.get(name)
56
+ if value:
57
+ return "utf-8" in value.lower() or "utf8" in value.lower()
58
+ return False
59
+
60
+
61
+ def ensure_utf8_locale() -> tuple[str, str | None] | None:
62
+ """Fall back to a UTF-8 locale when the process has none.
63
+
64
+ Outside a UTF-8 locale, tmux replaces non-ASCII characters in format output
65
+ with ``_``, which breaks libtmux's parsing of the ``␞`` separator. As in
66
+ PEP 538, only LC_CTYPE is set unless LC_ALL overrides it.
67
+
68
+ Return the variable that was set and its previous value, or None.
69
+ """
70
+ if has_utf8_locale(os.environ):
71
+ return None
72
+ # libtmux decodes tmux output with the encoding Python picked at startup,
73
+ # so a locale set now cannot help if that encoding is not UTF-8.
74
+ encoding = locale.getpreferredencoding(False)
75
+ if codecs.lookup(encoding).name != "utf-8":
76
+ logger.warning(
77
+ "tmux needs a UTF-8 locale, but Python decodes %s; start the "
78
+ "process with a UTF-8 locale or PYTHONUTF8=1",
79
+ encoding,
80
+ )
81
+ return None
82
+ name = "LC_ALL" if os.environ.get("LC_ALL") else "LC_CTYPE"
83
+ previous = os.environ.get(name)
84
+ os.environ[name] = UTF8_FALLBACK_LOCALE
85
+ logger.warning(
86
+ "No UTF-8 locale set; using %s=%s for tmux", name, UTF8_FALLBACK_LOCALE
87
+ )
88
+ return name, previous
@@ -26,6 +26,7 @@ from openhands.tools.terminal.constants import (
26
26
  )
27
27
  from openhands.tools.terminal.env import (
28
28
  build_terminal_env,
29
+ ensure_utf8_locale,
29
30
  normalize_terminal_env,
30
31
  )
31
32
  from openhands.tools.terminal.terminal.process_priority import (
@@ -123,6 +124,8 @@ class TmuxPanePool:
123
124
  if self._initialized:
124
125
  return
125
126
 
127
+ # Before building env, so the shells get the same locale as tmux.
128
+ ensure_utf8_locale()
126
129
  env = build_terminal_env(self.env)
127
130
  session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
128
131
  # Stale handles must not reach another pool after tmux restarts.
@@ -25,6 +25,7 @@ from openhands.tools.terminal.constants import (
25
25
  )
26
26
  from openhands.tools.terminal.env import (
27
27
  build_terminal_env,
28
+ ensure_utf8_locale,
28
29
  normalize_terminal_env,
29
30
  )
30
31
  from openhands.tools.terminal.metadata import CmdOutputMetadata
@@ -214,6 +215,8 @@ class TmuxTerminal(TerminalInterface):
214
215
  if self._initialized:
215
216
  return
216
217
 
218
+ # Before building env, so the shell gets the same locale as tmux.
219
+ ensure_utf8_locale()
217
220
  env = build_terminal_env(self._env)
218
221
  # Disable interactive pagers (git, man, systemctl, ...) so commands that
219
222
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
@@ -12,6 +12,8 @@ import time
12
12
  from collections import deque
13
13
  from collections.abc import Mapping
14
14
 
15
+ import psutil
16
+
15
17
  from openhands.sdk.logger import get_logger
16
18
  from openhands.tools.terminal.constants import (
17
19
  CMD_OUTPUT_PS1_BEGIN,
@@ -348,7 +350,13 @@ class WindowsTerminal(TerminalInterface):
348
350
  self._command_running_event.clear()
349
351
 
350
352
  def _terminate_child_processes(self) -> bool:
351
- """Terminate descendants of the persistent PowerShell process."""
353
+ """Terminate descendants of the persistent PowerShell process.
354
+
355
+ Enumerating descendants and killing them through ``psutil`` avoids the
356
+ WMI query (``Get-CimInstance``) that used to back this method: on a
357
+ loaded Windows runner that query can exceed the subprocess timeout, so
358
+ the child that kept a timed-out command alive was never terminated.
359
+ """
352
360
  if (
353
361
  platform.system() != "Windows"
354
362
  or self.process is None
@@ -356,53 +364,29 @@ class WindowsTerminal(TerminalInterface):
356
364
  ):
357
365
  return False
358
366
 
359
- script = f"""
360
- $root = {self.process.pid}
361
- $childrenByParent = @{{}}
362
- Get-CimInstance Win32_Process | ForEach-Object {{
363
- $parentId = [int]$_.ParentProcessId
364
- if (-not $childrenByParent.ContainsKey($parentId)) {{
365
- $childrenByParent[$parentId] = New-Object System.Collections.Generic.List[int]
366
- }}
367
- $childrenByParent[$parentId].Add([int]$_.ProcessId)
368
- }}
369
- $toStop = New-Object System.Collections.Generic.List[int]
370
- function Add-Descendants([int]$processId) {{
371
- if (-not $childrenByParent.ContainsKey($processId)) {{ return }}
372
- foreach ($childId in $childrenByParent[$processId]) {{
373
- if ($childId -eq $PID) {{ continue }}
374
- $toStop.Add($childId)
375
- Add-Descendants $childId
376
- }}
377
- }}
378
- Add-Descendants $root
379
- for ($i = $toStop.Count - 1; $i -ge 0; $i--) {{
380
- Stop-Process -Id $toStop[$i] -Force -ErrorAction SilentlyContinue
381
- }}
382
- if ($toStop.Count -gt 0) {{ exit 0 }} else {{ exit 1 }}
383
- """
384
- startupinfo = None
385
- startupinfo_cls = getattr(subprocess, "STARTUPINFO", None)
386
- if startupinfo_cls is not None:
387
- startupinfo = startupinfo_cls()
388
- startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 0)
389
- creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
390
-
391
367
  try:
392
- result = subprocess.run(
393
- [self.shell_path, "-NoLogo", "-NoProfile", "-Command", script],
394
- stdout=subprocess.DEVNULL,
395
- stderr=subprocess.DEVNULL,
396
- check=False,
397
- timeout=5.0,
398
- startupinfo=startupinfo,
399
- creationflags=creationflags,
400
- )
401
- return result.returncode == 0
402
- except (subprocess.TimeoutExpired, OSError) as exc:
403
- logger.debug("Failed to terminate PowerShell child processes: %s", exc)
368
+ root = psutil.Process(self.process.pid)
369
+ children = root.children(recursive=True)
370
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as exc:
371
+ logger.debug("Failed to enumerate PowerShell descendants: %s", exc)
404
372
  return False
405
373
 
374
+ if not children:
375
+ return False
376
+
377
+ # Kill leaves first so parents cannot reparent surviving grandchildren
378
+ # before they are terminated.
379
+ terminated = False
380
+ for child in reversed(children):
381
+ try:
382
+ child.kill()
383
+ terminated = True
384
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as exc:
385
+ logger.debug("Failed to terminate child %s: %s", child.pid, exc)
386
+
387
+ psutil.wait_procs(children, timeout=_INTERRUPT_GRACE_SECONDS)
388
+ return terminated
389
+
406
390
  def interrupt(self) -> bool:
407
391
  """Interrupt the active command if the process is still alive."""
408
392
  if self.process is None or self.process.poll() is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.58.2
3
+ Version: 1.60.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,7 +14,8 @@ Requires-Dist: tree-sitter-bash>=0.23
14
14
  Requires-Dist: binaryornot>=0.6.0
15
15
  Requires-Dist: cachetools
16
16
  Requires-Dist: filelock>=3.20.1
17
- Requires-Dist: libtmux>=0.53.0
17
+ Requires-Dist: libtmux>=0.62.0
18
+ Requires-Dist: psutil>=7.2.2
18
19
  Requires-Dist: pydantic>=2.13.5
19
20
  Requires-Dist: browser-use>=0.8.0
20
21
  Requires-Dist: func-timeout>=4.3.5
@@ -5,7 +5,8 @@ tree-sitter-bash>=0.23
5
5
  binaryornot>=0.6.0
6
6
  cachetools
7
7
  filelock>=3.20.1
8
- libtmux>=0.53.0
8
+ libtmux>=0.62.0
9
+ psutil>=7.2.2
9
10
  pydantic>=2.13.5
10
11
  browser-use>=0.8.0
11
12
  func-timeout>=4.3.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.58.2"
3
+ version = "1.60.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -12,7 +12,8 @@ dependencies = [
12
12
  "binaryornot>=0.6.0",
13
13
  "cachetools",
14
14
  "filelock>=3.20.1",
15
- "libtmux>=0.53.0",
15
+ "libtmux>=0.62.0",
16
+ "psutil>=7.2.2",
16
17
  "pydantic>=2.13.5",
17
18
  "browser-use>=0.8.0",
18
19
  "func-timeout>=4.3.5",
@@ -1,39 +0,0 @@
1
- """Environment helpers for terminal backends."""
2
-
3
- import re
4
- from collections.abc import Mapping
5
-
6
- from openhands.sdk.utils import sanitized_env
7
-
8
-
9
- ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
10
-
11
-
12
- def normalize_terminal_env(
13
- extra_env: Mapping[str, str] | None,
14
- ) -> dict[str, str] | None:
15
- """Validate and copy client-provided terminal environment variables."""
16
- if extra_env is None:
17
- return None
18
-
19
- normalized: dict[str, str] = {}
20
- for key, value in extra_env.items():
21
- if not isinstance(key, str) or not ENV_VAR_NAME_RE.match(key):
22
- raise ValueError(f"Invalid terminal environment variable name: {key!r}")
23
- if not isinstance(value, str):
24
- raise TypeError(
25
- "Terminal environment variable values must be strings: "
26
- f"{key!r}={value!r}"
27
- )
28
- normalized[key] = value
29
- return normalized
30
-
31
-
32
- def build_terminal_env(extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
33
- """Return the sanitized process environment plus client-provided overrides."""
34
- env = sanitized_env()
35
- normalized = normalize_terminal_env(extra_env)
36
- if normalized:
37
- env.update(normalized)
38
- env = sanitized_env(env)
39
- return env