openhands-tools 1.7.1__tar.gz → 1.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/PKG-INFO +1 -1
  2. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/browser_use/__init__.py +8 -0
  3. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/browser_use/definition.py +128 -0
  4. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/browser_use/impl.py +16 -0
  5. openhands_tools-1.7.2/openhands/tools/browser_use/server.py +190 -0
  6. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/tmux_terminal.py +5 -3
  7. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands_tools.egg-info/PKG-INFO +1 -1
  8. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/pyproject.toml +1 -1
  9. openhands_tools-1.7.1/openhands/tools/browser_use/server.py +0 -100
  10. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/__init__.py +0 -0
  11. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/apply_patch/__init__.py +0 -0
  12. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/apply_patch/core.py +0 -0
  13. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/apply_patch/definition.py +0 -0
  14. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/browser_use/impl_windows.py +0 -0
  15. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/browser_use/logging_fix.py +0 -0
  16. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/__init__.py +0 -0
  17. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/definition.py +0 -0
  18. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/impl.py +0 -0
  19. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/registration.py +0 -0
  20. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/templates/delegate_tool_description.j2 +0 -0
  21. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/delegate/visualizer.py +0 -0
  22. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/__init__.py +0 -0
  23. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/definition.py +0 -0
  24. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/editor.py +0 -0
  25. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/exceptions.py +0 -0
  26. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/impl.py +0 -0
  27. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/__init__.py +0 -0
  28. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/config.py +0 -0
  29. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/constants.py +0 -0
  30. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/diff.py +0 -0
  31. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/encoding.py +0 -0
  32. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  33. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/history.py +0 -0
  34. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/file_editor/utils/shell.py +0 -0
  35. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/__init__.py +0 -0
  36. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/__init__.py +0 -0
  37. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/definition.py +0 -0
  38. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/edit/impl.py +0 -0
  39. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  40. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/definition.py +0 -0
  41. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/list_directory/impl.py +0 -0
  42. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/__init__.py +0 -0
  43. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/definition.py +0 -0
  44. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/read_file/impl.py +0 -0
  45. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/__init__.py +0 -0
  46. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/definition.py +0 -0
  47. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/gemini/write_file/impl.py +0 -0
  48. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/glob/__init__.py +0 -0
  49. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/glob/definition.py +0 -0
  50. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/glob/impl.py +0 -0
  51. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/grep/__init__.py +0 -0
  52. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/grep/definition.py +0 -0
  53. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/grep/impl.py +0 -0
  54. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/__init__.py +0 -0
  55. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/definition.py +0 -0
  56. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/planning_file_editor/impl.py +0 -0
  57. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/preset/__init__.py +0 -0
  58. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/preset/default.py +0 -0
  59. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/preset/gemini.py +0 -0
  60. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/preset/planning.py +0 -0
  61. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/py.typed +0 -0
  62. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/task_tracker/__init__.py +0 -0
  63. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/task_tracker/definition.py +0 -0
  64. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/__init__.py +0 -0
  65. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/constants.py +0 -0
  66. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/definition.py +0 -0
  67. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/impl.py +0 -0
  68. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/metadata.py +0 -0
  69. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/__init__.py +0 -0
  70. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/factory.py +0 -0
  71. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/interface.py +0 -0
  72. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  73. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  74. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/terminal/utils/command.py +0 -0
  75. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/tom_consult/__init__.py +0 -0
  76. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/tom_consult/definition.py +0 -0
  77. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/tom_consult/executor.py +0 -0
  78. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/utils/__init__.py +0 -0
  79. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands/tools/utils/timeout.py +0 -0
  80. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands_tools.egg-info/SOURCES.txt +0 -0
  81. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands_tools.egg-info/dependency_links.txt +0 -0
  82. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands_tools.egg-info/requires.txt +0 -0
  83. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/openhands_tools.egg-info/top_level.txt +0 -0
  84. {openhands_tools-1.7.1 → openhands_tools-1.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.7.1
3
+ Version: 1.7.2
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: openhands-sdk
@@ -9,6 +9,8 @@ from openhands.tools.browser_use.definition import (
9
9
  BrowserGetContentTool,
10
10
  BrowserGetStateAction,
11
11
  BrowserGetStateTool,
12
+ BrowserGetStorageAction,
13
+ BrowserGetStorageTool,
12
14
  BrowserGoBackAction,
13
15
  BrowserGoBackTool,
14
16
  BrowserListTabsAction,
@@ -18,6 +20,8 @@ from openhands.tools.browser_use.definition import (
18
20
  BrowserObservation,
19
21
  BrowserScrollAction,
20
22
  BrowserScrollTool,
23
+ BrowserSetStorageAction,
24
+ BrowserSetStorageTool,
21
25
  BrowserSwitchTabAction,
22
26
  BrowserSwitchTabTool,
23
27
  BrowserToolSet,
@@ -38,6 +42,8 @@ __all__ = [
38
42
  "BrowserListTabsTool",
39
43
  "BrowserSwitchTabTool",
40
44
  "BrowserCloseTabTool",
45
+ "BrowserGetStorageTool",
46
+ "BrowserSetStorageTool",
41
47
  # Actions
42
48
  "BrowserNavigateAction",
43
49
  "BrowserClickAction",
@@ -49,6 +55,8 @@ __all__ = [
49
55
  "BrowserListTabsAction",
50
56
  "BrowserSwitchTabAction",
51
57
  "BrowserCloseTabAction",
58
+ "BrowserGetStorageAction",
59
+ "BrowserSetStorageAction",
52
60
  # Observations
53
61
  "BrowserObservation",
54
62
  "BrowserToolSet",
@@ -1,6 +1,9 @@
1
1
  """Browser-use tool implementation for web automation."""
2
2
 
3
+ import base64
4
+ import hashlib
3
5
  from collections.abc import Sequence
6
+ from pathlib import Path
4
7
  from typing import TYPE_CHECKING, Literal, Self
5
8
 
6
9
  from pydantic import Field
@@ -57,6 +60,29 @@ class BrowserObservation(Observation):
57
60
  description="Directory where full output files are saved",
58
61
  )
59
62
 
63
+ def _save_screenshot(self, base64_data: str, save_dir: str) -> str | None:
64
+ try:
65
+ save_dir_path = Path(save_dir)
66
+ save_dir_path.mkdir(parents=True, exist_ok=True)
67
+
68
+ mime_type = detect_image_mime_type(base64_data)
69
+ ext = mime_type.split("/")[-1]
70
+ if ext == "jpeg":
71
+ ext = "jpg"
72
+
73
+ # Generate hash for filename
74
+ content_hash = hashlib.sha256(base64_data.encode("utf-8")).hexdigest()[:8]
75
+ filename = f"browser_screenshot_{content_hash}.{ext}"
76
+ file_path = save_dir_path / filename
77
+
78
+ if not file_path.exists():
79
+ image_data = base64.b64decode(base64_data)
80
+ file_path.write_bytes(image_data)
81
+
82
+ return str(file_path)
83
+ except Exception:
84
+ return None
85
+
60
86
  @property
61
87
  def to_llm_content(self) -> Sequence[TextContent | ImageContent]:
62
88
  llm_content: list[TextContent | ImageContent] = []
@@ -81,6 +107,17 @@ class BrowserObservation(Observation):
81
107
 
82
108
  if self.screenshot_data:
83
109
  mime_type = detect_image_mime_type(self.screenshot_data)
110
+
111
+ # Save screenshot if directory is available
112
+ if self.full_output_save_dir:
113
+ saved_path = self._save_screenshot(
114
+ self.screenshot_data, self.full_output_save_dir
115
+ )
116
+ if saved_path:
117
+ llm_content.append(
118
+ TextContent(text=f"Screenshot saved to: {saved_path}")
119
+ )
120
+
84
121
  # Convert base64 to data URL format for ImageContent
85
122
  data_url = f"data:{mime_type};base64,{self.screenshot_data}"
86
123
  llm_content.append(ImageContent(image_urls=[data_url]))
@@ -542,6 +579,95 @@ class BrowserCloseTabTool(ToolDefinition[BrowserCloseTabAction, BrowserObservati
542
579
  ]
543
580
 
544
581
 
582
+ # ============================================
583
+ # `browser_get_storage`
584
+ # ============================================
585
+ class BrowserGetStorageAction(BrowserAction):
586
+ """Schema for getting browser storage (cookies, local storage, session storage)."""
587
+
588
+ pass
589
+
590
+
591
+ BROWSER_GET_STORAGE_DESCRIPTION = """Get browser storage data including cookies,
592
+ local storage, and session storage.
593
+
594
+ This tool extracts all cookies and storage data from the current browser session.
595
+ Useful for debugging, session management, or extracting authentication tokens.
596
+ """
597
+
598
+
599
+ class BrowserGetStorageTool(
600
+ ToolDefinition[BrowserGetStorageAction, BrowserObservation]
601
+ ):
602
+ """Tool for getting browser storage."""
603
+
604
+ @classmethod
605
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
606
+ return [
607
+ cls(
608
+ description=BROWSER_GET_STORAGE_DESCRIPTION,
609
+ action_type=BrowserGetStorageAction,
610
+ observation_type=BrowserObservation,
611
+ annotations=ToolAnnotations(
612
+ title="browser_get_storage",
613
+ readOnlyHint=True,
614
+ destructiveHint=False,
615
+ idempotentHint=True,
616
+ openWorldHint=False,
617
+ ),
618
+ executor=executor,
619
+ )
620
+ ]
621
+
622
+
623
+ # ============================================
624
+ # `browser_set_storage`
625
+ # ============================================
626
+ class BrowserSetStorageAction(BrowserAction):
627
+ """Schema for setting browser storage (cookies, local storage, session storage)."""
628
+
629
+ storage_state: dict = Field(
630
+ description="Storage state dictionary containing 'cookies' and 'origins' (from browser_get_storage)" # noqa: E501
631
+ )
632
+
633
+
634
+ BROWSER_SET_STORAGE_DESCRIPTION = """Set browser storage data including cookies,
635
+ local storage, and session storage.
636
+
637
+ This tool allows you to restore or set the browser's storage state. You can use the
638
+ output from browser_get_storage to restore a previous session.
639
+
640
+ Parameters:
641
+ - storage_state: A dictionary containing 'cookies' and 'origins'.
642
+ - cookies: List of cookie objects
643
+ - origins: List of origin objects containing 'localStorage' and 'sessionStorage'
644
+ """
645
+
646
+
647
+ class BrowserSetStorageTool(
648
+ ToolDefinition[BrowserSetStorageAction, BrowserObservation]
649
+ ):
650
+ """Tool for setting browser storage."""
651
+
652
+ @classmethod
653
+ def create(cls, executor: "BrowserToolExecutor") -> Sequence[Self]:
654
+ return [
655
+ cls(
656
+ description=BROWSER_SET_STORAGE_DESCRIPTION,
657
+ action_type=BrowserSetStorageAction,
658
+ observation_type=BrowserObservation,
659
+ annotations=ToolAnnotations(
660
+ title="browser_set_storage",
661
+ readOnlyHint=False,
662
+ destructiveHint=True,
663
+ idempotentHint=False,
664
+ openWorldHint=False,
665
+ ),
666
+ executor=executor,
667
+ )
668
+ ]
669
+
670
+
545
671
  class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
546
672
  """A set of all browser tools.
547
673
 
@@ -593,6 +719,8 @@ class BrowserToolSet(ToolDefinition[BrowserAction, BrowserObservation]):
593
719
  BrowserListTabsTool,
594
720
  BrowserSwitchTabTool,
595
721
  BrowserCloseTabTool,
722
+ BrowserGetStorageTool,
723
+ BrowserSetStorageTool,
596
724
  ]:
597
725
  tools.extend(tool_class.create(executor))
598
726
  return tools
@@ -214,11 +214,13 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
214
214
  BrowserCloseTabAction,
215
215
  BrowserGetContentAction,
216
216
  BrowserGetStateAction,
217
+ BrowserGetStorageAction,
217
218
  BrowserGoBackAction,
218
219
  BrowserListTabsAction,
219
220
  BrowserNavigateAction,
220
221
  BrowserObservation,
221
222
  BrowserScrollAction,
223
+ BrowserSetStorageAction,
222
224
  BrowserSwitchTabAction,
223
225
  BrowserTypeAction,
224
226
  )
@@ -234,6 +236,10 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
234
236
  result = await self.type_text(action.index, action.text)
235
237
  elif isinstance(action, BrowserGetStateAction):
236
238
  return await self.get_state(action.include_screenshot)
239
+ elif isinstance(action, BrowserGetStorageAction):
240
+ result = await self.get_storage()
241
+ elif isinstance(action, BrowserSetStorageAction):
242
+ result = await self.set_storage(action.storage_state)
237
243
  elif isinstance(action, BrowserGetContentAction):
238
244
  result = await self.get_content(
239
245
  action.extract_links, action.start_from_char
@@ -334,6 +340,16 @@ class BrowserToolExecutor(ToolExecutor[BrowserAction, BrowserObservation]):
334
340
  full_output_save_dir=self.full_output_save_dir,
335
341
  )
336
342
 
343
+ async def get_storage(self) -> str:
344
+ """Get browser storage (cookies, local storage, session storage)."""
345
+ await self._ensure_initialized()
346
+ return await self._server._get_storage()
347
+
348
+ async def set_storage(self, storage_state: dict) -> str:
349
+ """Set browser storage (cookies, local storage, session storage)."""
350
+ await self._ensure_initialized()
351
+ return await self._server._set_storage(storage_state)
352
+
337
353
  # Tab Management
338
354
  async def list_tabs(self) -> str:
339
355
  """List all open tabs."""
@@ -0,0 +1,190 @@
1
+ from browser_use.dom.markdown_extractor import extract_clean_markdown
2
+
3
+ from openhands.sdk import get_logger
4
+ from openhands.tools.browser_use.logging_fix import LogSafeBrowserUseServer
5
+
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class CustomBrowserUseServer(LogSafeBrowserUseServer):
11
+ """
12
+ Custom BrowserUseServer with a new tool for extracting web
13
+ page's content in markdown.
14
+ """
15
+
16
+ async def _get_storage(self) -> str:
17
+ """Get browser storage (cookies, local storage, session storage)."""
18
+ import json
19
+
20
+ if not self.browser_session:
21
+ return "Error: No browser session active"
22
+
23
+ try:
24
+ # Use the private method from BrowserSession to get storage state
25
+ # This returns a dict with 'cookies' and 'origins'
26
+ # (localStorage/sessionStorage)
27
+ storage_state = await self.browser_session._cdp_get_storage_state()
28
+ return json.dumps(storage_state, indent=2)
29
+ except Exception as e:
30
+ logger.exception("Error getting storage state", exc_info=e)
31
+ return f"Error getting storage state: {str(e)}"
32
+
33
+ async def _set_storage(self, storage_state: dict) -> str:
34
+ """Set browser storage (cookies, local storage, session storage)."""
35
+ if not self.browser_session:
36
+ return "Error: No browser session active"
37
+
38
+ try:
39
+ # 1. Set cookies
40
+ cookies = storage_state.get("cookies", [])
41
+ if cookies:
42
+ await self.browser_session._cdp_set_cookies(cookies)
43
+
44
+ # 2. Set local/session storage
45
+ origins = storage_state.get("origins", [])
46
+ if origins:
47
+ cdp_session = await self.browser_session.get_or_create_cdp_session()
48
+
49
+ # Enable DOMStorage
50
+ await cdp_session.cdp_client.send.DOMStorage.enable(
51
+ session_id=cdp_session.session_id
52
+ )
53
+
54
+ try:
55
+ for origin_data in origins:
56
+ origin = origin_data.get("origin")
57
+ if not origin:
58
+ continue
59
+
60
+ dom_storage = cdp_session.cdp_client.send.DOMStorage
61
+
62
+ # Set localStorage
63
+ for item in origin_data.get("localStorage", []):
64
+ key = item.get("key") or item.get("name")
65
+ if not key:
66
+ continue
67
+ await dom_storage.setDOMStorageItem(
68
+ params={
69
+ "storageId": {
70
+ "securityOrigin": origin,
71
+ "isLocalStorage": True,
72
+ },
73
+ "key": key,
74
+ "value": item["value"],
75
+ },
76
+ session_id=cdp_session.session_id,
77
+ )
78
+
79
+ # Set sessionStorage
80
+ for item in origin_data.get("sessionStorage", []):
81
+ key = item.get("key") or item.get("name")
82
+ if not key:
83
+ continue
84
+ await dom_storage.setDOMStorageItem(
85
+ params={
86
+ "storageId": {
87
+ "securityOrigin": origin,
88
+ "isLocalStorage": False,
89
+ },
90
+ "key": key,
91
+ "value": item["value"],
92
+ },
93
+ session_id=cdp_session.session_id,
94
+ )
95
+ finally:
96
+ # Disable DOMStorage
97
+ await cdp_session.cdp_client.send.DOMStorage.disable(
98
+ session_id=cdp_session.session_id
99
+ )
100
+
101
+ return "Storage set successfully"
102
+ except Exception as e:
103
+ logger.exception("Error setting storage state", exc_info=e)
104
+ return f"Error setting storage state: {str(e)}"
105
+
106
+ async def _get_content(self, extract_links=False, start_from_char: int = 0) -> str:
107
+ MAX_CHAR_LIMIT = 30000
108
+
109
+ if not self.browser_session:
110
+ return "Error: No browser session active"
111
+
112
+ # Extract clean markdown using the new method
113
+ try:
114
+ content, content_stats = await extract_clean_markdown(
115
+ browser_session=self.browser_session, extract_links=extract_links
116
+ )
117
+ except Exception as e:
118
+ logger.exception(
119
+ "Error extracting clean markdown", exc_info=e, stack_info=True
120
+ )
121
+ return f"Could not extract clean markdown: {type(e).__name__}"
122
+
123
+ # Original content length for processing
124
+ final_filtered_length = content_stats["final_filtered_chars"]
125
+
126
+ if start_from_char > 0:
127
+ if start_from_char >= len(content):
128
+ return f"start_from_char ({start_from_char}) exceeds content length ({len(content)}). Content has {final_filtered_length} characters after filtering." # noqa: E501
129
+
130
+ content = content[start_from_char:]
131
+ content_stats["started_from_char"] = start_from_char
132
+
133
+ # Smart truncation with context preservation
134
+ truncated = False
135
+ if len(content) > MAX_CHAR_LIMIT:
136
+ # Try to truncate at a natural break point (paragraph, sentence)
137
+ truncate_at = MAX_CHAR_LIMIT
138
+
139
+ # Look for paragraph break within last 500 chars of limit
140
+ paragraph_break = content.rfind(
141
+ "\n\n", MAX_CHAR_LIMIT - 500, MAX_CHAR_LIMIT
142
+ )
143
+ if paragraph_break > 0:
144
+ truncate_at = paragraph_break
145
+ else:
146
+ # Look for sentence break within last 200 chars of limit
147
+ sentence_break = content.rfind(
148
+ ".", MAX_CHAR_LIMIT - 200, MAX_CHAR_LIMIT
149
+ )
150
+ if sentence_break > 0:
151
+ truncate_at = sentence_break + 1
152
+
153
+ content = content[:truncate_at]
154
+ truncated = True
155
+ next_start = (start_from_char or 0) + truncate_at
156
+ content_stats["truncated_at_char"] = truncate_at
157
+ content_stats["next_start_char"] = next_start
158
+
159
+ # Add content statistics to the result
160
+ original_html_length = content_stats["original_html_chars"]
161
+ initial_markdown_length = content_stats["initial_markdown_chars"]
162
+ chars_filtered = content_stats["filtered_chars_removed"]
163
+
164
+ stats_summary = (
165
+ f"Content processed: {original_html_length:,}"
166
+ + f" HTML chars → {initial_markdown_length:,}"
167
+ + f" initial markdown → {final_filtered_length:,} filtered markdown"
168
+ )
169
+ if start_from_char > 0:
170
+ stats_summary += f" (started from char {start_from_char:,})"
171
+ if truncated:
172
+ stats_summary += f" → {len(content):,} final chars (truncated, use start_from_char={content_stats['next_start_char']} to continue)" # noqa: E501
173
+ elif chars_filtered > 0:
174
+ stats_summary += f" (filtered {chars_filtered:,} chars of noise)"
175
+
176
+ prompt = f"""<content_stats>
177
+ {stats_summary}
178
+ </content_stats>
179
+
180
+ <webpage_content>
181
+ {content}
182
+ </webpage_content>"""
183
+ current_url = await self.browser_session.get_current_page_url()
184
+
185
+ return f"""<url>
186
+ {current_url}
187
+ </url>
188
+ <content>
189
+ {prompt}
190
+ </content>"""
@@ -97,9 +97,11 @@ class TmuxTerminal(TerminalInterface):
97
97
  try:
98
98
  if hasattr(self, "session"):
99
99
  self.session.kill()
100
- except ImportError:
101
- # Python is shutting down, let the OS handle cleanup
102
- pass
100
+ except Exception as e:
101
+ # Session might already be dead/killed externally
102
+ # (e.g., "can't find session" error from tmux)
103
+ # Also handles ImportError during Python shutdown
104
+ logger.debug(f"Error closing tmux session (may already be dead): {e}")
103
105
  self._closed: bool = True
104
106
 
105
107
  def send_keys(self, text: str, enter: bool = True) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.7.1
3
+ Version: 1.7.2
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: openhands-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.7.1"
3
+ version = "1.7.2"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -1,100 +0,0 @@
1
- from browser_use.dom.markdown_extractor import extract_clean_markdown
2
-
3
- from openhands.sdk import get_logger
4
- from openhands.tools.browser_use.logging_fix import LogSafeBrowserUseServer
5
-
6
-
7
- logger = get_logger(__name__)
8
-
9
-
10
- class CustomBrowserUseServer(LogSafeBrowserUseServer):
11
- """
12
- Custom BrowserUseServer with a new tool for extracting web
13
- page's content in markdown.
14
- """
15
-
16
- async def _get_content(self, extract_links=False, start_from_char: int = 0) -> str:
17
- MAX_CHAR_LIMIT = 30000
18
-
19
- if not self.browser_session:
20
- return "Error: No browser session active"
21
-
22
- # Extract clean markdown using the new method
23
- try:
24
- content, content_stats = await extract_clean_markdown(
25
- browser_session=self.browser_session, extract_links=extract_links
26
- )
27
- except Exception as e:
28
- logger.exception(
29
- "Error extracting clean markdown", exc_info=e, stack_info=True
30
- )
31
- return f"Could not extract clean markdown: {type(e).__name__}"
32
-
33
- # Original content length for processing
34
- final_filtered_length = content_stats["final_filtered_chars"]
35
-
36
- if start_from_char > 0:
37
- if start_from_char >= len(content):
38
- return f"start_from_char ({start_from_char}) exceeds content length ({len(content)}). Content has {final_filtered_length} characters after filtering." # noqa: E501
39
-
40
- content = content[start_from_char:]
41
- content_stats["started_from_char"] = start_from_char
42
-
43
- # Smart truncation with context preservation
44
- truncated = False
45
- if len(content) > MAX_CHAR_LIMIT:
46
- # Try to truncate at a natural break point (paragraph, sentence)
47
- truncate_at = MAX_CHAR_LIMIT
48
-
49
- # Look for paragraph break within last 500 chars of limit
50
- paragraph_break = content.rfind(
51
- "\n\n", MAX_CHAR_LIMIT - 500, MAX_CHAR_LIMIT
52
- )
53
- if paragraph_break > 0:
54
- truncate_at = paragraph_break
55
- else:
56
- # Look for sentence break within last 200 chars of limit
57
- sentence_break = content.rfind(
58
- ".", MAX_CHAR_LIMIT - 200, MAX_CHAR_LIMIT
59
- )
60
- if sentence_break > 0:
61
- truncate_at = sentence_break + 1
62
-
63
- content = content[:truncate_at]
64
- truncated = True
65
- next_start = (start_from_char or 0) + truncate_at
66
- content_stats["truncated_at_char"] = truncate_at
67
- content_stats["next_start_char"] = next_start
68
-
69
- # Add content statistics to the result
70
- original_html_length = content_stats["original_html_chars"]
71
- initial_markdown_length = content_stats["initial_markdown_chars"]
72
- chars_filtered = content_stats["filtered_chars_removed"]
73
-
74
- stats_summary = (
75
- f"Content processed: {original_html_length:,}"
76
- + f" HTML chars → {initial_markdown_length:,}"
77
- + f" initial markdown → {final_filtered_length:,} filtered markdown"
78
- )
79
- if start_from_char > 0:
80
- stats_summary += f" (started from char {start_from_char:,})"
81
- if truncated:
82
- stats_summary += f" → {len(content):,} final chars (truncated, use start_from_char={content_stats['next_start_char']} to continue)" # noqa: E501
83
- elif chars_filtered > 0:
84
- stats_summary += f" (filtered {chars_filtered:,} chars of noise)"
85
-
86
- prompt = f"""<content_stats>
87
- {stats_summary}
88
- </content_stats>
89
-
90
- <webpage_content>
91
- {content}
92
- </webpage_content>"""
93
- current_url = await self.browser_session.get_current_page_url()
94
-
95
- return f"""<url>
96
- {current_url}
97
- </url>
98
- <content>
99
- {prompt}
100
- </content>"""