openhands-tools 1.0.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 (58) hide show
  1. openhands_tools-1.0.0/PKG-INFO +13 -0
  2. openhands_tools-1.0.0/openhands/tools/__init__.py +9 -0
  3. openhands_tools-1.0.0/openhands/tools/browser_use/__init__.py +55 -0
  4. openhands_tools-1.0.0/openhands/tools/browser_use/definition.py +559 -0
  5. openhands_tools-1.0.0/openhands/tools/browser_use/impl.py +354 -0
  6. openhands_tools-1.0.0/openhands/tools/browser_use/server.py +100 -0
  7. openhands_tools-1.0.0/openhands/tools/delegate/__init__.py +16 -0
  8. openhands_tools-1.0.0/openhands/tools/delegate/definition.py +116 -0
  9. openhands_tools-1.0.0/openhands/tools/delegate/impl.py +254 -0
  10. openhands_tools-1.0.0/openhands/tools/file_editor/__init__.py +15 -0
  11. openhands_tools-1.0.0/openhands/tools/file_editor/definition.py +241 -0
  12. openhands_tools-1.0.0/openhands/tools/file_editor/editor.py +704 -0
  13. openhands_tools-1.0.0/openhands/tools/file_editor/exceptions.py +54 -0
  14. openhands_tools-1.0.0/openhands/tools/file_editor/impl.py +107 -0
  15. openhands_tools-1.0.0/openhands/tools/file_editor/utils/__init__.py +0 -0
  16. openhands_tools-1.0.0/openhands/tools/file_editor/utils/config.py +2 -0
  17. openhands_tools-1.0.0/openhands/tools/file_editor/utils/constants.py +9 -0
  18. openhands_tools-1.0.0/openhands/tools/file_editor/utils/diff.py +124 -0
  19. openhands_tools-1.0.0/openhands/tools/file_editor/utils/encoding.py +137 -0
  20. openhands_tools-1.0.0/openhands/tools/file_editor/utils/file_cache.py +158 -0
  21. openhands_tools-1.0.0/openhands/tools/file_editor/utils/history.py +123 -0
  22. openhands_tools-1.0.0/openhands/tools/file_editor/utils/shell.py +71 -0
  23. openhands_tools-1.0.0/openhands/tools/glob/__init__.py +15 -0
  24. openhands_tools-1.0.0/openhands/tools/glob/definition.py +115 -0
  25. openhands_tools-1.0.0/openhands/tools/glob/impl.py +278 -0
  26. openhands_tools-1.0.0/openhands/tools/grep/__init__.py +16 -0
  27. openhands_tools-1.0.0/openhands/tools/grep/definition.py +117 -0
  28. openhands_tools-1.0.0/openhands/tools/grep/impl.py +250 -0
  29. openhands_tools-1.0.0/openhands/tools/planning_file_editor/__init__.py +6 -0
  30. openhands_tools-1.0.0/openhands/tools/planning_file_editor/definition.py +120 -0
  31. openhands_tools-1.0.0/openhands/tools/planning_file_editor/impl.py +66 -0
  32. openhands_tools-1.0.0/openhands/tools/preset/__init__.py +25 -0
  33. openhands_tools-1.0.0/openhands/tools/preset/default.py +88 -0
  34. openhands_tools-1.0.0/openhands/tools/preset/planning.py +171 -0
  35. openhands_tools-1.0.0/openhands/tools/py.typed +0 -0
  36. openhands_tools-1.0.0/openhands/tools/task_tracker/__init__.py +14 -0
  37. openhands_tools-1.0.0/openhands/tools/task_tracker/definition.py +432 -0
  38. openhands_tools-1.0.0/openhands/tools/terminal/__init__.py +28 -0
  39. openhands_tools-1.0.0/openhands/tools/terminal/constants.py +31 -0
  40. openhands_tools-1.0.0/openhands/tools/terminal/definition.py +283 -0
  41. openhands_tools-1.0.0/openhands/tools/terminal/impl.py +184 -0
  42. openhands_tools-1.0.0/openhands/tools/terminal/metadata.py +101 -0
  43. openhands_tools-1.0.0/openhands/tools/terminal/terminal/__init__.py +24 -0
  44. openhands_tools-1.0.0/openhands/tools/terminal/terminal/factory.py +119 -0
  45. openhands_tools-1.0.0/openhands/tools/terminal/terminal/interface.py +229 -0
  46. openhands_tools-1.0.0/openhands/tools/terminal/terminal/subprocess_terminal.py +418 -0
  47. openhands_tools-1.0.0/openhands/tools/terminal/terminal/terminal_session.py +502 -0
  48. openhands_tools-1.0.0/openhands/tools/terminal/terminal/tmux_terminal.py +177 -0
  49. openhands_tools-1.0.0/openhands/tools/terminal/utils/command.py +150 -0
  50. openhands_tools-1.0.0/openhands/tools/utils/__init__.py +45 -0
  51. openhands_tools-1.0.0/openhands/tools/utils/timeout.py +14 -0
  52. openhands_tools-1.0.0/openhands_tools.egg-info/PKG-INFO +13 -0
  53. openhands_tools-1.0.0/openhands_tools.egg-info/SOURCES.txt +106 -0
  54. openhands_tools-1.0.0/openhands_tools.egg-info/dependency_links.txt +1 -0
  55. openhands_tools-1.0.0/openhands_tools.egg-info/requires.txt +8 -0
  56. openhands_tools-1.0.0/openhands_tools.egg-info/top_level.txt +1 -0
  57. openhands_tools-1.0.0/pyproject.toml +30 -0
  58. openhands_tools-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: openhands-tools
3
+ Version: 1.0.0
4
+ Summary: OpenHands Tools - Runtime tools for AI agents
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: openhands-sdk
7
+ Requires-Dist: bashlex>=0.18
8
+ Requires-Dist: binaryornot>=0.4.4
9
+ Requires-Dist: cachetools
10
+ Requires-Dist: libtmux>=0.46.2
11
+ Requires-Dist: pydantic>=2.11.7
12
+ Requires-Dist: browser-use>=0.8.0
13
+ Requires-Dist: func-timeout>=4.3.5
@@ -0,0 +1,9 @@
1
+ """Runtime tools package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+
6
+ try:
7
+ __version__ = version("openhands-tools")
8
+ except PackageNotFoundError:
9
+ __version__ = "0.0.0" # fallback for editable/unbuilt environments
@@ -0,0 +1,55 @@
1
+ """Browser tools using browser-use integration."""
2
+
3
+ from openhands.tools.browser_use.definition import (
4
+ BrowserClickAction,
5
+ BrowserClickTool,
6
+ BrowserCloseTabAction,
7
+ BrowserCloseTabTool,
8
+ BrowserGetContentAction,
9
+ BrowserGetContentTool,
10
+ BrowserGetStateAction,
11
+ BrowserGetStateTool,
12
+ BrowserGoBackAction,
13
+ BrowserGoBackTool,
14
+ BrowserListTabsAction,
15
+ BrowserListTabsTool,
16
+ BrowserNavigateAction,
17
+ BrowserNavigateTool,
18
+ BrowserObservation,
19
+ BrowserScrollAction,
20
+ BrowserScrollTool,
21
+ BrowserSwitchTabAction,
22
+ BrowserSwitchTabTool,
23
+ BrowserToolSet,
24
+ BrowserTypeAction,
25
+ BrowserTypeTool,
26
+ )
27
+
28
+
29
+ __all__ = [
30
+ # Tool classes
31
+ "BrowserNavigateTool",
32
+ "BrowserClickTool",
33
+ "BrowserTypeTool",
34
+ "BrowserGetStateTool",
35
+ "BrowserGetContentTool",
36
+ "BrowserScrollTool",
37
+ "BrowserGoBackTool",
38
+ "BrowserListTabsTool",
39
+ "BrowserSwitchTabTool",
40
+ "BrowserCloseTabTool",
41
+ # Actions
42
+ "BrowserNavigateAction",
43
+ "BrowserClickAction",
44
+ "BrowserTypeAction",
45
+ "BrowserGetStateAction",
46
+ "BrowserGetContentAction",
47
+ "BrowserScrollAction",
48
+ "BrowserGoBackAction",
49
+ "BrowserListTabsAction",
50
+ "BrowserSwitchTabAction",
51
+ "BrowserCloseTabAction",
52
+ # Observations
53
+ "BrowserObservation",
54
+ "BrowserToolSet",
55
+ ]
@@ -0,0 +1,559 @@
1
+ """Browser-use tool implementation for web automation."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING, Literal, Self
5
+
6
+ from pydantic import Field
7
+
8
+ from openhands.sdk.llm import ImageContent, TextContent
9
+ from openhands.sdk.tool import (
10
+ Action,
11
+ Observation,
12
+ ToolAnnotations,
13
+ ToolDefinition,
14
+ register_tool,
15
+ )
16
+ from openhands.sdk.utils import maybe_truncate
17
+
18
+
19
+ # Lazy import to avoid hanging during module import
20
+ if TYPE_CHECKING:
21
+ from openhands.tools.browser_use.impl import BrowserToolExecutor
22
+
23
+
24
+ # Maximum output size for browser observations
25
+ MAX_BROWSER_OUTPUT_SIZE = 50000
26
+
27
+
28
+ class BrowserObservation(Observation):
29
+ """Base observation for browser operations."""
30
+
31
+ screenshot_data: str | None = Field(
32
+ default=None, description="Base64 screenshot data if available"
33
+ )
34
+
35
+ @property
36
+ def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
37
+ llm_content: list[TextContent | ImageContent] = []
38
+
39
+ # If is_error is true, prepend error message
40
+ if self.is_error:
41
+ llm_content.append(TextContent(text=self.ERROR_MESSAGE_HEADER))
42
+
43
+ # Get text content and truncate if needed
44
+ content_text = self.text
45
+ if content_text:
46
+ llm_content.append(
47
+ TextContent(text=maybe_truncate(content_text, MAX_BROWSER_OUTPUT_SIZE))
48
+ )
49
+
50
+ if self.screenshot_data:
51
+ mime_type = "image/png"
52
+ if self.screenshot_data.startswith("/9j/"):
53
+ mime_type = "image/jpeg"
54
+ elif self.screenshot_data.startswith("iVBORw0KGgo"):
55
+ mime_type = "image/png"
56
+ elif self.screenshot_data.startswith("R0lGODlh"):
57
+ mime_type = "image/gif"
58
+ elif self.screenshot_data.startswith("UklGR"):
59
+ mime_type = "image/webp"
60
+ # Convert base64 to data URL format for ImageContent
61
+ data_url = f"data:{mime_type};base64,{self.screenshot_data}"
62
+ llm_content.append(ImageContent(image_urls=[data_url]))
63
+
64
+ return llm_content
65
+
66
+
67
+ # ============================================
68
+ # Base Browser Action
69
+ # ============================================
70
+ class BrowserAction(Action):
71
+ """Base class for all browser actions.
72
+
73
+ This base class serves as the parent for all browser-related actions,
74
+ enabling proper type hierarchy and eliminating the need for union types.
75
+ """
76
+
77
+ pass
78
+
79
+
80
+ # ============================================
81
+ # `go_to_url`
82
+ # ============================================
83
+ class BrowserNavigateAction(BrowserAction):
84
+ """Schema for browser navigation."""
85
+
86
+ url: str = Field(description="The URL to navigate to")
87
+ new_tab: bool = Field(
88
+ default=False, description="Whether to open in a new tab. Default: False"
89
+ )
90
+
91
+
92
+ BROWSER_NAVIGATE_DESCRIPTION = """Navigate to a URL in the browser.
93
+
94
+ This tool allows you to navigate to any web page. You can optionally open the URL in a new tab.
95
+
96
+ Parameters:
97
+ - url: The URL to navigate to (required)
98
+ - new_tab: Whether to open in a new tab (optional, default: False)
99
+
100
+ Examples:
101
+ - Navigate to Google: url="https://www.google.com"
102
+ - Open GitHub in new tab: url="https://github.com", new_tab=True
103
+ """ # noqa: E501
104
+
105
+
106
+ class BrowserNavigateTool(ToolDefinition[BrowserNavigateAction, BrowserObservation]):
107
+ """Tool for browser navigation."""
108
+
109
+ @classmethod
110
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
111
+ return [
112
+ cls(
113
+ description=BROWSER_NAVIGATE_DESCRIPTION,
114
+ action_type=BrowserNavigateAction,
115
+ observation_type=BrowserObservation,
116
+ annotations=ToolAnnotations(
117
+ title="browser_navigate",
118
+ readOnlyHint=False,
119
+ destructiveHint=False,
120
+ idempotentHint=False,
121
+ openWorldHint=True,
122
+ ),
123
+ executor=executor,
124
+ )
125
+ ]
126
+
127
+
128
+ # ============================================
129
+ # `browser_click`
130
+ # ============================================
131
+ class BrowserClickAction(BrowserAction):
132
+ """Schema for clicking elements."""
133
+
134
+ index: int = Field(
135
+ ge=0, description="The index of the element to click (from browser_get_state)"
136
+ )
137
+ new_tab: bool = Field(
138
+ default=False,
139
+ description="Whether to open any resulting navigation in a new tab. Default: False", # noqa: E501
140
+ )
141
+
142
+
143
+ BROWSER_CLICK_DESCRIPTION = """Click an element on the page by its index.
144
+
145
+ Use this tool to click on interactive elements like buttons, links, or form controls.
146
+ The index comes from the browser_get_state tool output.
147
+
148
+ Parameters:
149
+ - index: The index of the element to click (from browser_get_state)
150
+ - new_tab: Whether to open any resulting navigation in a new tab (optional)
151
+
152
+ Important: Only use indices that appear in your current browser_get_state output.
153
+ """ # noqa: E501
154
+
155
+
156
+ class BrowserClickTool(ToolDefinition[BrowserClickAction, BrowserObservation]):
157
+ """Tool for clicking browser elements."""
158
+
159
+ @classmethod
160
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
161
+ return [
162
+ cls(
163
+ description=BROWSER_CLICK_DESCRIPTION,
164
+ action_type=BrowserClickAction,
165
+ observation_type=BrowserObservation,
166
+ annotations=ToolAnnotations(
167
+ title="browser_click",
168
+ readOnlyHint=False,
169
+ destructiveHint=False,
170
+ idempotentHint=False,
171
+ openWorldHint=True,
172
+ ),
173
+ executor=executor,
174
+ )
175
+ ]
176
+
177
+
178
+ # ============================================
179
+ # `browser_type`
180
+ # ============================================
181
+ class BrowserTypeAction(BrowserAction):
182
+ """Schema for typing text into elements."""
183
+
184
+ index: int = Field(
185
+ ge=0, description="The index of the input element (from browser_get_state)"
186
+ )
187
+ text: str = Field(description="The text to type")
188
+
189
+
190
+ BROWSER_TYPE_DESCRIPTION = """Type text into an input field.
191
+
192
+ Use this tool to enter text into form fields, search boxes, or other text input elements.
193
+ The index comes from the browser_get_state tool output.
194
+
195
+ Parameters:
196
+ - index: The index of the input element (from browser_get_state)
197
+ - text: The text to type
198
+
199
+ Important: Only use indices that appear in your current browser_get_state output.
200
+ """ # noqa: E501
201
+
202
+
203
+ class BrowserTypeTool(ToolDefinition[BrowserTypeAction, BrowserObservation]):
204
+ """Tool for typing text into browser elements."""
205
+
206
+ @classmethod
207
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
208
+ return [
209
+ cls(
210
+ description=BROWSER_TYPE_DESCRIPTION,
211
+ action_type=BrowserTypeAction,
212
+ observation_type=BrowserObservation,
213
+ annotations=ToolAnnotations(
214
+ title="browser_type",
215
+ readOnlyHint=False,
216
+ destructiveHint=False,
217
+ idempotentHint=False,
218
+ openWorldHint=True,
219
+ ),
220
+ executor=executor,
221
+ )
222
+ ]
223
+
224
+
225
+ # ============================================
226
+ # `browser_get_state`
227
+ # ============================================
228
+ class BrowserGetStateAction(BrowserAction):
229
+ """Schema for getting browser state."""
230
+
231
+ include_screenshot: bool = Field(
232
+ default=False,
233
+ description="Whether to include a screenshot of the current page. Default: False", # noqa: E501
234
+ )
235
+
236
+
237
+ BROWSER_GET_STATE_DESCRIPTION = """Get the current state of the page including all interactive elements.
238
+
239
+ This tool returns the current page content with numbered interactive elements that you can
240
+ click or type into. Use this frequently to understand what's available on the page.
241
+
242
+ Parameters:
243
+ - include_screenshot: Whether to include a screenshot (optional, default: False)
244
+ """ # noqa: E501
245
+
246
+
247
+ class BrowserGetStateTool(ToolDefinition[BrowserGetStateAction, BrowserObservation]):
248
+ """Tool for getting browser state."""
249
+
250
+ @classmethod
251
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
252
+ return [
253
+ cls(
254
+ description=BROWSER_GET_STATE_DESCRIPTION,
255
+ action_type=BrowserGetStateAction,
256
+ observation_type=BrowserObservation,
257
+ annotations=ToolAnnotations(
258
+ title="browser_get_state",
259
+ readOnlyHint=True,
260
+ destructiveHint=False,
261
+ idempotentHint=True,
262
+ openWorldHint=True,
263
+ ),
264
+ executor=executor,
265
+ )
266
+ ]
267
+
268
+
269
+ # ============================================
270
+ # `browser_get_content`
271
+ # ============================================
272
+ class BrowserGetContentAction(BrowserAction):
273
+ """Schema for getting page content in markdown."""
274
+
275
+ extract_links: bool = Field(
276
+ default=False,
277
+ description="Whether to include links in the content (default: False)",
278
+ )
279
+ start_from_char: int = Field(
280
+ default=0,
281
+ ge=0,
282
+ description="Character index to start from in the page content (default: 0)",
283
+ )
284
+
285
+
286
+ BROWSER_GET_CONTENT_DESCRIPTION = """Extract the main content of the current page in clean markdown format. It has been filtered to remove noise and advertising content.
287
+
288
+ If the content was truncated and you need more information, use start_from_char parameter to continue from where truncation occurred.
289
+ """ # noqa: E501
290
+
291
+
292
+ class BrowserGetContentTool(
293
+ ToolDefinition[BrowserGetContentAction, BrowserObservation]
294
+ ):
295
+ """Tool for getting page content in markdown."""
296
+
297
+ @classmethod
298
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
299
+ return [
300
+ cls(
301
+ description=BROWSER_GET_CONTENT_DESCRIPTION,
302
+ action_type=BrowserGetContentAction,
303
+ observation_type=BrowserObservation,
304
+ annotations=ToolAnnotations(
305
+ title="browser_get_content",
306
+ readOnlyHint=True,
307
+ destructiveHint=False,
308
+ idempotentHint=True,
309
+ openWorldHint=True,
310
+ ),
311
+ executor=executor,
312
+ )
313
+ ]
314
+
315
+
316
+ # ============================================
317
+ # `browser_scroll`
318
+ # ============================================
319
+ class BrowserScrollAction(BrowserAction):
320
+ """Schema for scrolling the page."""
321
+
322
+ direction: Literal["up", "down"] = Field(
323
+ default="down",
324
+ description="Direction to scroll. Options: 'up', 'down'. Default: 'down'",
325
+ )
326
+
327
+
328
+ BROWSER_SCROLL_DESCRIPTION = """Scroll the page up or down.
329
+
330
+ Use this tool to scroll through page content when elements are not visible or when you need
331
+ to see more content.
332
+
333
+ Parameters:
334
+ - direction: Direction to scroll - "up" or "down" (optional, default: "down")
335
+ """ # noqa: E501
336
+
337
+
338
+ class BrowserScrollTool(ToolDefinition[BrowserScrollAction, BrowserObservation]):
339
+ """Tool for scrolling the browser page."""
340
+
341
+ @classmethod
342
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
343
+ return [
344
+ cls(
345
+ description=BROWSER_SCROLL_DESCRIPTION,
346
+ action_type=BrowserScrollAction,
347
+ observation_type=BrowserObservation,
348
+ annotations=ToolAnnotations(
349
+ title="browser_scroll",
350
+ readOnlyHint=False,
351
+ destructiveHint=False,
352
+ idempotentHint=False,
353
+ openWorldHint=True,
354
+ ),
355
+ executor=executor,
356
+ )
357
+ ]
358
+
359
+
360
+ # ============================================
361
+ # `browser_go_back`
362
+ # ============================================
363
+ class BrowserGoBackAction(BrowserAction):
364
+ """Schema for going back in browser history."""
365
+
366
+ pass
367
+
368
+
369
+ BROWSER_GO_BACK_DESCRIPTION = """Go back to the previous page in browser history.
370
+
371
+ Use this tool to navigate back to the previously visited page, similar to clicking the
372
+ browser's back button.
373
+ """ # noqa: E501
374
+
375
+
376
+ class BrowserGoBackTool(ToolDefinition[BrowserGoBackAction, BrowserObservation]):
377
+ """Tool for going back in browser history."""
378
+
379
+ @classmethod
380
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
381
+ return [
382
+ cls(
383
+ description=BROWSER_GO_BACK_DESCRIPTION,
384
+ action_type=BrowserGoBackAction,
385
+ observation_type=BrowserObservation,
386
+ annotations=ToolAnnotations(
387
+ title="browser_go_back",
388
+ readOnlyHint=False,
389
+ destructiveHint=False,
390
+ idempotentHint=False,
391
+ openWorldHint=True,
392
+ ),
393
+ executor=executor,
394
+ )
395
+ ]
396
+
397
+
398
+ # ============================================
399
+ # `browser_list_tabs`
400
+ # ============================================
401
+ class BrowserListTabsAction(BrowserAction):
402
+ """Schema for listing browser tabs."""
403
+
404
+ pass
405
+
406
+
407
+ BROWSER_LIST_TABS_DESCRIPTION = """List all open browser tabs.
408
+
409
+ This tool shows all currently open tabs with their IDs, titles, and URLs. Use the tab IDs
410
+ with browser_switch_tab or browser_close_tab.
411
+ """ # noqa: E501
412
+
413
+
414
+ class BrowserListTabsTool(ToolDefinition[BrowserListTabsAction, BrowserObservation]):
415
+ """Tool for listing browser tabs."""
416
+
417
+ @classmethod
418
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
419
+ return [
420
+ cls(
421
+ description=BROWSER_LIST_TABS_DESCRIPTION,
422
+ action_type=BrowserListTabsAction,
423
+ observation_type=BrowserObservation,
424
+ annotations=ToolAnnotations(
425
+ title="browser_list_tabs",
426
+ readOnlyHint=True,
427
+ destructiveHint=False,
428
+ idempotentHint=True,
429
+ openWorldHint=False,
430
+ ),
431
+ executor=executor,
432
+ )
433
+ ]
434
+
435
+
436
+ # ============================================
437
+ # `browser_switch_tab`
438
+ # ============================================
439
+ class BrowserSwitchTabAction(BrowserAction):
440
+ """Schema for switching browser tabs."""
441
+
442
+ tab_id: str = Field(
443
+ description="4 Character Tab ID of the tab to switch"
444
+ + " to (from browser_list_tabs)"
445
+ )
446
+
447
+
448
+ BROWSER_SWITCH_TAB_DESCRIPTION = """Switch to a different browser tab.
449
+
450
+ Use this tool to switch between open tabs. Get the tab_id from browser_list_tabs.
451
+
452
+ Parameters:
453
+ - tab_id: 4 Character Tab ID of the tab to switch to
454
+ """
455
+
456
+
457
+ class BrowserSwitchTabTool(ToolDefinition[BrowserSwitchTabAction, BrowserObservation]):
458
+ """Tool for switching browser tabs."""
459
+
460
+ @classmethod
461
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
462
+ return [
463
+ cls(
464
+ description=BROWSER_SWITCH_TAB_DESCRIPTION,
465
+ action_type=BrowserSwitchTabAction,
466
+ observation_type=BrowserObservation,
467
+ annotations=ToolAnnotations(
468
+ title="browser_switch_tab",
469
+ readOnlyHint=False,
470
+ destructiveHint=False,
471
+ idempotentHint=False,
472
+ openWorldHint=False,
473
+ ),
474
+ executor=executor,
475
+ )
476
+ ]
477
+
478
+
479
+ # ============================================
480
+ # `browser_close_tab`
481
+ # ============================================
482
+ class BrowserCloseTabAction(BrowserAction):
483
+ """Schema for closing browser tabs."""
484
+
485
+ tab_id: str = Field(
486
+ description="4 Character Tab ID of the tab to close (from browser_list_tabs)"
487
+ )
488
+
489
+
490
+ BROWSER_CLOSE_TAB_DESCRIPTION = """Close a specific browser tab.
491
+
492
+ Use this tool to close tabs you no longer need. Get the tab_id from browser_list_tabs.
493
+
494
+ Parameters:
495
+ - tab_id: 4 Character Tab ID of the tab to close
496
+ """
497
+
498
+
499
+ class BrowserCloseTabTool(ToolDefinition[BrowserCloseTabAction, BrowserObservation]):
500
+ """Tool for closing browser tabs."""
501
+
502
+ @classmethod
503
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
504
+ return [
505
+ cls(
506
+ description=BROWSER_CLOSE_TAB_DESCRIPTION,
507
+ action_type=BrowserCloseTabAction,
508
+ observation_type=BrowserObservation,
509
+ annotations=ToolAnnotations(
510
+ title="browser_close_tab",
511
+ readOnlyHint=False,
512
+ destructiveHint=True,
513
+ idempotentHint=False,
514
+ openWorldHint=False,
515
+ ),
516
+ executor=executor,
517
+ )
518
+ ]
519
+
520
+
521
+ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
522
+ """A set of all browser tools.
523
+
524
+ This tool set includes all available browser-related tools
525
+ for interacting with web pages.
526
+
527
+ The toolset automatically checks for Chromium availability
528
+ when created and automatically installs it if missing.
529
+ """
530
+
531
+ @classmethod
532
+ def create(
533
+ cls,
534
+ **executor_config,
535
+ ) -> list[ToolDefinition[BrowserAction, BrowserObservation]]:
536
+ # Import executor only when actually needed to
537
+ # avoid hanging during module import
538
+ from openhands.tools.browser_use.impl import BrowserToolExecutor
539
+
540
+ executor = BrowserToolExecutor(**executor_config)
541
+ # Each tool.create() returns a Sequence[Self], so we flatten the results
542
+ tools: list[ToolDefinition[BrowserAction, BrowserObservation]] = []
543
+ for tool_class in [
544
+ BrowserNavigateTool,
545
+ BrowserClickTool,
546
+ BrowserGetStateTool,
547
+ BrowserGetContentTool,
548
+ BrowserTypeTool,
549
+ BrowserScrollTool,
550
+ BrowserGoBackTool,
551
+ BrowserListTabsTool,
552
+ BrowserSwitchTabTool,
553
+ BrowserCloseTabTool,
554
+ ]:
555
+ tools.extend(tool_class.create(executor))
556
+ return tools
557
+
558
+
559
+ register_tool(BrowserToolSet.name, BrowserToolSet)