openhands-tools 1.50.1__tar.gz → 1.52.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 (106) hide show
  1. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/PKG-INFO +2 -2
  2. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/ask_oracle/definition.py +3 -1
  3. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/definition.py +20 -1
  4. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/definition.py +3 -1
  5. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/edit/definition.py +3 -1
  6. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/list_directory/definition.py +3 -1
  7. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/read_file/definition.py +3 -1
  8. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/write_file/definition.py +3 -1
  9. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/glob/definition.py +3 -1
  10. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/grep/definition.py +5 -1
  11. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/planning_file_editor/definition.py +3 -1
  12. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task/definition.py +53 -6
  13. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task/manager.py +19 -1
  14. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task_tracker/definition.py +5 -1
  15. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/definition.py +5 -1
  16. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +4 -1
  17. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/tmux_terminal.py +4 -2
  18. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/workflow/definition.py +14 -5
  19. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/workflow/impl.py +7 -1
  20. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands_tools.egg-info/PKG-INFO +2 -2
  21. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands_tools.egg-info/requires.txt +1 -1
  22. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/pyproject.toml +2 -2
  23. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/__init__.py +0 -0
  24. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/apply_patch/__init__.py +0 -0
  25. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/apply_patch/core.py +0 -0
  26. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/apply_patch/definition.py +0 -0
  27. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  28. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/ask_oracle/impl.py +0 -0
  29. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/__init__.py +0 -0
  30. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/event_storage.py +0 -0
  31. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/impl.py +0 -0
  32. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  33. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/recording.py +0 -0
  34. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/browser_use/server.py +0 -0
  35. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/delegate/__init__.py +0 -0
  36. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/delegate/definition.py +0 -0
  37. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/delegate/impl.py +0 -0
  38. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/delegate/visualizer.py +0 -0
  39. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/__init__.py +0 -0
  40. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/editor.py +0 -0
  41. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/exceptions.py +0 -0
  42. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/impl.py +0 -0
  43. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  44. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/config.py +0 -0
  45. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  46. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  47. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  48. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  49. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/history.py +0 -0
  50. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  51. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/__init__.py +0 -0
  52. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  53. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/edit/impl.py +0 -0
  54. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/file_change.py +0 -0
  55. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  56. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  57. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  58. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  59. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  60. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  61. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/glob/__init__.py +0 -0
  62. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/glob/impl.py +0 -0
  63. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/grep/__init__.py +0 -0
  64. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/grep/impl.py +0 -0
  65. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  66. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  67. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/__init__.py +0 -0
  68. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/default.py +0 -0
  69. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/gemini.py +0 -0
  70. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/gpt5.py +0 -0
  71. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/planning.py +0 -0
  72. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  73. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  74. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/subagents/default.md +0 -0
  75. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  76. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/preset/task_outcome.py +0 -0
  77. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/py.typed +0 -0
  78. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task/__init__.py +0 -0
  79. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task/impl.py +0 -0
  80. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/task_tracker/__init__.py +0 -0
  81. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/__init__.py +0 -0
  82. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/constants.py +0 -0
  83. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/descriptions.py +0 -0
  84. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/env.py +0 -0
  85. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/impl.py +0 -0
  86. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/metadata.py +0 -0
  87. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  88. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  89. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  90. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  91. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  92. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
  93. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  94. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  95. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/utils/command.py +0 -0
  96. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  97. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/tom_consult/__init__.py +0 -0
  98. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/tom_consult/definition.py +0 -0
  99. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/tom_consult/executor.py +0 -0
  100. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/utils/__init__.py +0 -0
  101. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/utils/timeout.py +0 -0
  102. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands/tools/workflow/__init__.py +0 -0
  103. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  104. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  105. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/openhands_tools.egg-info/top_level.txt +0 -0
  106. {openhands_tools-1.50.1 → openhands_tools-1.52.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.50.1
3
+ Version: 1.52.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
@@ -13,7 +13,7 @@ Requires-Dist: tree-sitter-bash>=0.23
13
13
  Requires-Dist: binaryornot>=0.6.0
14
14
  Requires-Dist: cachetools
15
15
  Requires-Dist: libtmux>=0.53.0
16
- Requires-Dist: pydantic>=2.11.7
16
+ Requires-Dist: pydantic>=2.13.5
17
17
  Requires-Dist: browser-use>=0.8.0
18
18
  Requires-Dist: func-timeout>=4.3.5
19
19
  Requires-Dist: tom-swe>=1.0.3
@@ -1,7 +1,7 @@
1
1
  """Action, observation, and tool definitions for the ask_oracle tool."""
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import TYPE_CHECKING, Final, Self
4
+ from typing import TYPE_CHECKING, ClassVar, Final, Self
5
5
 
6
6
  from pydantic import Field
7
7
  from rich.text import Text
@@ -82,6 +82,8 @@ _DESCRIPTION = (
82
82
  class AskOracleTool(ToolDefinition[AskOracleAction, AskOracleObservation]):
83
83
  """Tool for consulting the Oracle (a saved LLM profile named "oracle")."""
84
84
 
85
+ user_selectable: ClassVar[bool] = False
86
+
85
87
  @classmethod
86
88
  def create(
87
89
  cls,
@@ -775,6 +775,18 @@ class BrowserStopRecordingTool(
775
775
  ]
776
776
 
777
777
 
778
+ BROWSER_PROMPT_GUIDANCE = """\
779
+ <BROWSER_TOOLS>
780
+ You have a browser for navigating pages and interacting with web UIs.
781
+ * Try curl/wget/fetch first. Use the browser only when simpler tools fail or the page requires JS/interaction.
782
+ * ALWAYS call `browser_get_state` before EVERY `browser_click` or `browser_type` — indices change after each action. Flow: navigate → get_state → interact → get_state → get_content.
783
+ * Max 10 browser actions per sub-task. If stuck, switch approach entirely.
784
+ * If 20+ total steps without converging, stop exploring and commit to your best answer.
785
+ * On 403/CAPTCHA/login wall: try one alternative, then abandon the browser.
786
+ * Do NOT submit forms or create accounts unless explicitly asked.
787
+ </BROWSER_TOOLS>""" # noqa: E501
788
+
789
+
778
790
  class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
779
791
  """A set of all browser tools.
780
792
 
@@ -785,6 +797,10 @@ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
785
797
  when created and automatically installs it if missing.
786
798
  """
787
799
 
800
+ catalog_description: ClassVar[str] = (
801
+ "Browse the web: navigate pages, click, type and read content."
802
+ )
803
+
788
804
  # Shared executor: reuse a single Chromium/CDP instance across parent
789
805
  # and subagents to avoid CDP port conflicts in sandbox containers.
790
806
  _shared_executor: ClassVar["BrowserToolExecutor | None"] = None
@@ -870,7 +886,10 @@ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
870
886
  BrowserStopRecordingTool,
871
887
  ]:
872
888
  tools.extend(tool_class.create(executor))
873
- return tools
889
+ return [
890
+ tool.model_copy(update={"prompt_guidance": BROWSER_PROMPT_GUIDANCE})
891
+ for tool in tools
892
+ ]
874
893
 
875
894
 
876
895
  register_tool(BrowserToolSet.name, BrowserToolSet)
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Literal
5
+ from typing import TYPE_CHECKING, ClassVar, Literal
6
6
 
7
7
  from pydantic import Field, PrivateAttr
8
8
 
@@ -193,6 +193,8 @@ Remember: when making multiple file edits in a row to the same file, you should
193
193
  class FileEditorTool(ToolDefinition[FileEditorAction, FileEditorObservation]):
194
194
  """A ToolDefinition subclass that automatically initializes a FileEditorExecutor."""
195
195
 
196
+ catalog_description: ClassVar[str] = "View, create and edit files."
197
+
196
198
  def declared_resources(self, action: Action) -> DeclaredResources:
197
199
  """Declare file resources accessed by this action.
198
200
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import Field
8
8
 
@@ -96,6 +96,8 @@ Examples:
96
96
  class EditTool(ToolDefinition[EditAction, EditObservation]):
97
97
  """Tool for editing files via find/replace."""
98
98
 
99
+ user_selectable: ClassVar[bool] = False
100
+
99
101
  def declared_resources(self, action: Action) -> DeclaredResources:
100
102
  """Lock on the target file path so concurrent edits to the same
101
103
  file are serialized, while edits to different files run in parallel.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from datetime import datetime
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
  from rich.text import Text
@@ -141,6 +141,8 @@ MAX_ENTRIES = 500
141
141
  class ListDirectoryTool(ToolDefinition[ListDirectoryAction, ListDirectoryObservation]):
142
142
  """Tool for listing directory contents with metadata."""
143
143
 
144
+ user_selectable: ClassVar[bool] = False
145
+
144
146
  def declared_resources(self, action: Action) -> DeclaredResources: # noqa: ARG002
145
147
  """Declare resource usage for parallel execution.
146
148
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import Field
8
8
  from rich.text import Text
@@ -109,6 +109,8 @@ MAX_LINES_PER_READ = 1000
109
109
  class ReadFileTool(ToolDefinition[ReadFileAction, ReadFileObservation]):
110
110
  """Tool for reading file contents with pagination support."""
111
111
 
112
+ user_selectable: ClassVar[bool] = False
113
+
112
114
  def declared_resources(self, action: Action) -> DeclaredResources:
113
115
  """Lock on the target file path so a read never sees
114
116
  partially-written content from a concurrent write.
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import Field
8
8
 
@@ -69,6 +69,8 @@ Examples:
69
69
  class WriteFileTool(ToolDefinition[WriteFileAction, WriteFileObservation]):
70
70
  """Tool for writing complete file contents."""
71
71
 
72
+ user_selectable: ClassVar[bool] = False
73
+
72
74
  def declared_resources(self, action: Action) -> DeclaredResources:
73
75
  """Lock on the target file path so concurrent writes to the same
74
76
  file are serialized, while writes to different files run in parallel.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from collections.abc import Sequence
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import Field
8
8
 
@@ -65,6 +65,8 @@ Examples:
65
65
  class GlobTool(ToolDefinition[GlobAction, GlobObservation]):
66
66
  """A ToolDefinition subclass that automatically initializes a GlobExecutor."""
67
67
 
68
+ catalog_description: ClassVar[str] = "Find files by name pattern, such as **/*.ts."
69
+
68
70
  def declared_resources(self, action: Action) -> DeclaredResources:
69
71
  """Declare resource usage based on the active backend.
70
72
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  from collections.abc import Sequence
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
  from pydantic import Field
8
8
 
@@ -67,6 +67,10 @@ TOOL_DESCRIPTION = """Fast content search tool.
67
67
  class GrepTool(ToolDefinition[GrepAction, GrepObservation]):
68
68
  """A ToolDefinition subclass that automatically initializes a GrepExecutor."""
69
69
 
70
+ catalog_description: ClassVar[str] = (
71
+ "Search file contents with regular expressions."
72
+ )
73
+
70
74
  def declared_resources(self, action: Action) -> DeclaredResources:
71
75
  """Declare resource usage for parallel execution.
72
76
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, ClassVar
6
6
 
7
7
 
8
8
  if TYPE_CHECKING:
@@ -65,6 +65,8 @@ class PlanningFileEditorTool(
65
65
  ):
66
66
  """A planning file editor tool with read-all, edit-PLAN.md-only access."""
67
67
 
68
+ user_selectable: ClassVar[bool] = False
69
+
68
70
  @classmethod
69
71
  def create(
70
72
  cls,
@@ -10,14 +10,20 @@ and TaskToolSet (the entry-point that wires up a TaskManager-backed executor).
10
10
  """
11
11
 
12
12
  from collections.abc import Sequence
13
- from typing import TYPE_CHECKING, Final
13
+ from typing import TYPE_CHECKING, ClassVar, Final
14
14
 
15
15
  from pydantic import Field
16
16
  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.subagent import get_factory_info, get_registered_agent_definitions
20
+ from openhands.sdk.logger import get_logger
21
+ from openhands.sdk.subagent import (
22
+ AgentDefinition,
23
+ SubAgentScope,
24
+ get_factory_info,
25
+ get_registered_agent_definitions,
26
+ )
21
27
  from openhands.sdk.tool import (
22
28
  Action,
23
29
  DeclaredResources,
@@ -29,11 +35,15 @@ from openhands.sdk.tool import (
29
35
 
30
36
 
31
37
  if TYPE_CHECKING:
38
+ from openhands.sdk.agent.base import AgentBase
32
39
  from openhands.sdk.conversation.state import ConversationState
33
40
  from openhands.tools.task.impl import TaskExecutor
34
41
  from openhands.tools.task.manager import ConfirmationHandler
35
42
 
36
43
 
44
+ logger = get_logger(__name__)
45
+
46
+
37
47
  class TaskAction(Action):
38
48
  """Schema for launching a sub-agent task."""
39
49
 
@@ -169,6 +179,8 @@ Example — Perform a multi-step task involving code editing and shell commands:
169
179
  class TaskTool(ToolDefinition[TaskAction, TaskObservation]):
170
180
  """Tool for launching (blocking) sub-agent tasks."""
171
181
 
182
+ user_selectable: ClassVar[bool] = False
183
+
172
184
  def declared_resources(self, action: Action) -> DeclaredResources: # noqa: ARG002
173
185
  return DeclaredResources(keys=(), declared=True)
174
186
 
@@ -213,11 +225,16 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
213
225
  )
214
226
  """
215
227
 
228
+ catalog_description: ClassVar[str] = (
229
+ "Delegate a self-contained sub-task to a separate agent."
230
+ )
231
+
216
232
  @classmethod
217
233
  def create(
218
234
  cls,
219
- conv_state: "ConversationState", # noqa: ARG003
235
+ conv_state: "ConversationState",
220
236
  confirmation_handler: "ConfirmationHandler | None" = None,
237
+ sub_agent_scope: SubAgentScope | dict[str, bool] | None = None,
221
238
  ) -> list[ToolDefinition]:
222
239
  """Create the task tool.
223
240
 
@@ -227,15 +244,30 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
227
244
  confirmation policy requires user approval. Receives
228
245
  `(task_id, pending_actions)` and must return `True` to
229
246
  approve or `False` to reject.
247
+ sub_agent_scope: Which of this agent's capabilities the sub-agents
248
+ it offers and starts must stay within.
230
249
 
231
250
  Returns:
232
251
  List containing a single TaskTool.
233
252
  """
234
253
  from openhands.tools.task.impl import TaskExecutor, TaskManager
235
254
 
236
- agent_types_info = get_factory_info()
255
+ scope = SubAgentScope.model_validate(sub_agent_scope or {})
256
+ registered = {
257
+ d.name
258
+ for d in get_registered_agent_definitions()
259
+ if not scope.restricts or _offered(scope, conv_state.agent, d)
260
+ }
261
+ if not scope.restricts:
262
+ agent_types_info = get_factory_info()
263
+ elif registered:
264
+ agent_types_info = get_factory_info(lambda d: d.name in registered)
265
+ else:
266
+ agent_types_info = (
267
+ "- None: every registered agent uses tools or MCP servers "
268
+ "this agent does not have."
269
+ )
237
270
 
238
- registered = {d.name for d in get_registered_agent_definitions()}
239
271
  task_tool_examples = "\n".join(
240
272
  ex for name, ex in TASK_TOOL_EXAMPLES.items() if name in registered
241
273
  )
@@ -245,7 +277,9 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
245
277
  task_tool_examples=task_tool_examples,
246
278
  )
247
279
 
248
- manager = TaskManager(confirmation_handler=confirmation_handler)
280
+ manager = TaskManager(
281
+ confirmation_handler=confirmation_handler, sub_agent_scope=scope
282
+ )
249
283
  task_executor = TaskExecutor(manager=manager)
250
284
 
251
285
  tools: list[ToolDefinition] = []
@@ -258,6 +292,19 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
258
292
  return tools
259
293
 
260
294
 
295
+ def _offered(
296
+ scope: SubAgentScope, parent: "AgentBase", definition: AgentDefinition
297
+ ) -> bool:
298
+ missing = scope.missing_from(parent, definition)
299
+ if missing:
300
+ logger.info(
301
+ "Not offering sub-agent %r: it uses %s, which this agent lacks",
302
+ definition.name,
303
+ ", ".join(missing),
304
+ )
305
+ return not missing
306
+
307
+
261
308
  # Automatically register when this module is imported
262
309
  register_tool(TaskToolSet.name, TaskToolSet)
263
310
  register_tool(TaskTool.name, TaskTool)
@@ -35,6 +35,7 @@ from openhands.sdk.logger import get_logger
35
35
  from openhands.sdk.observability.laminar import detached_delegate_context
36
36
  from openhands.sdk.security import ConfirmationPolicyBase
37
37
  from openhands.sdk.subagent.registry import AgentFactory, get_agent_factory
38
+ from openhands.sdk.subagent.scope import SubAgentScope, scope_delegation_tools
38
39
 
39
40
 
40
41
  if TYPE_CHECKING:
@@ -98,9 +99,11 @@ class TaskManager:
98
99
  def __init__(
99
100
  self,
100
101
  confirmation_handler: ConfirmationHandler | None = None,
102
+ sub_agent_scope: SubAgentScope | None = None,
101
103
  ):
102
104
  self._parent_conversation: LocalConversation | None = None
103
105
  self._confirmation_handler = confirmation_handler
106
+ self._sub_agent_scope = sub_agent_scope or SubAgentScope()
104
107
 
105
108
  self._tasks: dict[str, Task] = {}
106
109
  self._tasks_lock = threading.Lock()
@@ -375,9 +378,24 @@ class TaskManager:
375
378
 
376
379
  sub_agent = factory.factory_func(sub_agent_llm)
377
380
 
381
+ if self._sub_agent_scope.restricts and not isinstance(sub_agent, Agent):
382
+ raise ValueError(
383
+ f"Agent '{factory.definition.name}' runs as "
384
+ f"{type(sub_agent).__name__}, whose tools cannot be checked."
385
+ )
386
+ missing = self._sub_agent_scope.missing_from(parent.agent, sub_agent)
387
+ if missing:
388
+ raise ValueError(
389
+ f"Agent '{factory.definition.name}' uses {', '.join(missing)}, "
390
+ "which this agent does not have."
391
+ )
392
+
378
393
  # ensuring that the sub-agent LLM has stream deactivated
379
394
  sub_agent = sub_agent.model_copy(
380
- update={"llm": sub_agent.llm.model_copy(update={"stream": False})}
395
+ update={
396
+ "llm": sub_agent.llm.model_copy(update={"stream": False}),
397
+ "tools": scope_delegation_tools(sub_agent.tools, self._sub_agent_scope),
398
+ }
381
399
  )
382
400
  return sub_agent
383
401
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from collections.abc import Sequence
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Literal
4
+ from typing import TYPE_CHECKING, ClassVar, Literal
5
5
 
6
6
  from pydantic import BaseModel, Field, ValidationError
7
7
 
@@ -403,6 +403,10 @@ systematic approach and ensures comprehensive requirement fulfillment.""" # noq
403
403
  class TaskTrackerTool(ToolDefinition[TaskTrackerAction, TaskTrackerObservation]):
404
404
  """A ToolDefinition subclass that automatically initializes a TaskTrackerExecutor.""" # noqa: E501
405
405
 
406
+ catalog_description: ClassVar[str] = (
407
+ "Keep a running task list to organise multi-step work."
408
+ )
409
+
406
410
  @classmethod
407
411
  def create(cls, conv_state: "ConversationState") -> Sequence["TaskTrackerTool"]:
408
412
  """Initialize TaskTrackerTool with a TaskTrackerExecutor.
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import platform
5
5
  from collections.abc import Mapping, Sequence
6
- from typing import TYPE_CHECKING, Literal
6
+ from typing import TYPE_CHECKING, ClassVar, Literal
7
7
 
8
8
  from pydantic import Field
9
9
 
@@ -268,6 +268,10 @@ class TerminalObservation(Observation):
268
268
  class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
269
269
  """A ToolDefinition subclass that automatically initializes a TerminalExecutor with auto-detection.""" # noqa: E501
270
270
 
271
+ catalog_description: ClassVar[str] = (
272
+ "Run shell commands in a persistent terminal session."
273
+ )
274
+
271
275
  def declared_resources(self, action: Action) -> DeclaredResources: # noqa: ARG002
272
276
  # When using the tmux backend, TmuxPanePool handles concurrency
273
277
  # internally via pane-level isolation — opt out of framework
@@ -118,7 +118,10 @@ class TmuxPanePool:
118
118
  return
119
119
 
120
120
  env = build_terminal_env(self.env)
121
- self._server = libtmux.Server(socket_name=TMUX_SOCKET_NAME, environment=env)
121
+ # Stale handles must not reach another pool after tmux restarts.
122
+ self._server = libtmux.Server(
123
+ socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
124
+ )
122
125
  session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
123
126
  self._session = self._server.new_session(
124
127
  session_name=session_name,
@@ -122,8 +122,10 @@ class TmuxTerminal(TerminalInterface):
122
122
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
123
123
  env.setdefault("GIT_PAGER", "cat")
124
124
  env.setdefault("PAGER", "cat")
125
- # Use a dedicated socket to isolate OpenHands sessions from the user's tmux
126
- self.server = libtmux.Server(socket_name=TMUX_SOCKET_NAME, environment=env)
125
+ # Never reuse a socket: tmux numeric IDs restart at zero after server exit.
126
+ self.server = libtmux.Server(
127
+ socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
128
+ )
127
129
  _shell_command = "/bin/bash"
128
130
  if self.username in ["root", "openhands"]:
129
131
  # This starts a non-login (new) shell for the given user
@@ -3,10 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Sequence
6
- from typing import TYPE_CHECKING, Final, Literal
6
+ from typing import TYPE_CHECKING, ClassVar, Final, Literal
7
7
 
8
8
  from pydantic import Field
9
9
 
10
+ from openhands.sdk.subagent import SubAgentScope
10
11
  from openhands.sdk.tool import (
11
12
  Action,
12
13
  Observation,
@@ -142,15 +143,22 @@ class WorkflowTool(ToolDefinition[WorkflowAction, WorkflowObservation]):
142
143
  (e.g., in tests or extensions).
143
144
  """
144
145
 
146
+ user_selectable: ClassVar[bool] = False
147
+
145
148
  @classmethod
146
149
  def create(
147
150
  cls,
148
151
  conv_state: ConversationState | None = None, # noqa: ARG003
149
152
  executor: WorkflowExecutor | None = None,
150
153
  description: str = _WORKFLOW_DESCRIPTION,
154
+ sub_agent_scope: SubAgentScope | dict[str, bool] | None = None,
151
155
  ) -> Sequence[WorkflowTool]:
152
156
  from openhands.tools.workflow.impl import WorkflowExecutor
153
157
 
158
+ if executor is None:
159
+ executor = WorkflowExecutor(
160
+ sub_agent_scope=SubAgentScope.model_validate(sub_agent_scope or {})
161
+ )
154
162
  return [
155
163
  cls(
156
164
  action_type=WorkflowAction,
@@ -163,7 +171,7 @@ class WorkflowTool(ToolDefinition[WorkflowAction, WorkflowObservation]):
163
171
  idempotentHint=False,
164
172
  openWorldHint=True,
165
173
  ),
166
- executor=executor if executor is not None else WorkflowExecutor(),
174
+ executor=executor,
167
175
  )
168
176
  ]
169
177
 
@@ -171,14 +179,15 @@ class WorkflowTool(ToolDefinition[WorkflowAction, WorkflowObservation]):
171
179
  class WorkflowToolSet(ToolDefinition[WorkflowAction, WorkflowObservation]):
172
180
  """Tool set that creates the dynamic workflow tool."""
173
181
 
182
+ user_selectable: ClassVar[bool] = False
183
+
174
184
  @classmethod
175
185
  def create(
176
186
  cls,
177
187
  conv_state: ConversationState, # noqa: ARG003
188
+ sub_agent_scope: SubAgentScope | dict[str, bool] | None = None,
178
189
  ) -> Sequence[WorkflowTool]:
179
- from openhands.tools.workflow.impl import WorkflowExecutor
180
-
181
- return WorkflowTool.create(executor=WorkflowExecutor())
190
+ return WorkflowTool.create(sub_agent_scope=sub_agent_scope)
182
191
 
183
192
 
184
193
  register_tool(WorkflowToolSet.name, WorkflowToolSet)
@@ -10,6 +10,7 @@ from collections.abc import Callable, Sequence
10
10
  from typing import TYPE_CHECKING, Any, Protocol
11
11
 
12
12
  from openhands.sdk.logger import get_logger
13
+ from openhands.sdk.subagent import SubAgentScope
13
14
  from openhands.sdk.tool import ToolExecutor
14
15
  from openhands.tools.task.manager import TaskManager
15
16
  from openhands.tools.workflow.definition import WorkflowObservation
@@ -80,13 +81,14 @@ class WorkflowContext:
80
81
  parent_conversation: LocalConversation,
81
82
  max_concurrency: int,
82
83
  manager: _TaskStarter | None = None,
84
+ sub_agent_scope: SubAgentScope | None = None,
83
85
  ) -> None:
84
86
  if max_concurrency < 1:
85
87
  raise ValueError("max_concurrency must be at least 1")
86
88
  self._parent_conversation = parent_conversation
87
89
  self._max_concurrency = max_concurrency
88
90
  if manager is None:
89
- task_manager = TaskManager()
91
+ task_manager = TaskManager(sub_agent_scope=sub_agent_scope)
90
92
  task_manager.attach_parent(parent_conversation)
91
93
  self._manager = task_manager
92
94
  else:
@@ -501,6 +503,9 @@ def _safe_globals() -> dict[str, Any]:
501
503
  class WorkflowExecutor(ToolExecutor["WorkflowAction", WorkflowObservation]):
502
504
  """Executor for the dynamic workflow tool."""
503
505
 
506
+ def __init__(self, sub_agent_scope: SubAgentScope | None = None) -> None:
507
+ self._sub_agent_scope = sub_agent_scope
508
+
504
509
  def __call__(
505
510
  self,
506
511
  action: WorkflowAction,
@@ -517,6 +522,7 @@ class WorkflowExecutor(ToolExecutor["WorkflowAction", WorkflowObservation]):
517
522
  context = WorkflowContext(
518
523
  parent_conversation=conversation,
519
524
  max_concurrency=action.max_concurrency,
525
+ sub_agent_scope=self._sub_agent_scope,
520
526
  )
521
527
  try:
522
528
  result = execute_workflow_script(action.script, context)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.50.1
3
+ Version: 1.52.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
@@ -13,7 +13,7 @@ Requires-Dist: tree-sitter-bash>=0.23
13
13
  Requires-Dist: binaryornot>=0.6.0
14
14
  Requires-Dist: cachetools
15
15
  Requires-Dist: libtmux>=0.53.0
16
- Requires-Dist: pydantic>=2.11.7
16
+ Requires-Dist: pydantic>=2.13.5
17
17
  Requires-Dist: browser-use>=0.8.0
18
18
  Requires-Dist: func-timeout>=4.3.5
19
19
  Requires-Dist: tom-swe>=1.0.3
@@ -4,7 +4,7 @@ tree-sitter-bash>=0.23
4
4
  binaryornot>=0.6.0
5
5
  cachetools
6
6
  libtmux>=0.53.0
7
- pydantic>=2.11.7
7
+ pydantic>=2.13.5
8
8
  browser-use>=0.8.0
9
9
  func-timeout>=4.3.5
10
10
  tom-swe>=1.0.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.50.1"
3
+ version = "1.52.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "binaryornot>=0.6.0",
12
12
  "cachetools",
13
13
  "libtmux>=0.53.0",
14
- "pydantic>=2.11.7",
14
+ "pydantic>=2.13.5",
15
15
  "browser-use>=0.8.0",
16
16
  "func-timeout>=4.3.5",
17
17
  "tom-swe>=1.0.3",