openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

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.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. openhands-0.0.0.dist-info/top_level.txt +0 -1
@@ -1,41 +0,0 @@
1
- class ToolError(Exception):
2
- """Raised when a tool encounters an error."""
3
-
4
- def __init__(self, message):
5
- self.message = message
6
- super().__init__(message)
7
-
8
- def __str__(self):
9
- return self.message
10
-
11
-
12
- class EditorToolParameterMissingError(ToolError):
13
- """Raised when a required parameter is missing for a tool command."""
14
-
15
- def __init__(self, command, parameter):
16
- self.command = command
17
- self.parameter = parameter
18
- self.message = f"Parameter `{parameter}` is required for command: {command}."
19
-
20
-
21
- class EditorToolParameterInvalidError(ToolError):
22
- """Raised when a parameter is invalid for a tool command."""
23
-
24
- def __init__(self, parameter, value, hint=None):
25
- self.parameter = parameter
26
- self.value = value
27
- self.message = (
28
- f"Invalid `{parameter}` parameter: {value}. {hint}"
29
- if hint
30
- else f"Invalid `{parameter}` parameter: {value}."
31
- )
32
-
33
-
34
- class FileValidationError(ToolError):
35
- """Raised when a file fails validation checks (size, type, etc.)."""
36
-
37
- def __init__(self, path: str, reason: str):
38
- self.path = path
39
- self.reason = reason
40
- self.message = f"File validation failed for {path}: {reason}"
41
- super().__init__(self.message)
@@ -1,66 +0,0 @@
1
- from openhands.sdk.tool import ToolExecutor
2
- from openhands.tools.str_replace_editor.definition import (
3
- CommandLiteral,
4
- StrReplaceEditorAction,
5
- StrReplaceEditorObservation,
6
- )
7
- from openhands.tools.str_replace_editor.editor import FileEditor
8
- from openhands.tools.str_replace_editor.exceptions import ToolError
9
-
10
-
11
- # Module-global editor instance (lazily initialized in file_editor)
12
- _GLOBAL_EDITOR: FileEditor | None = None
13
-
14
-
15
- class FileEditorExecutor(ToolExecutor):
16
- def __init__(self):
17
- self.editor = FileEditor()
18
-
19
- def __call__(self, action: StrReplaceEditorAction) -> StrReplaceEditorObservation:
20
- result: StrReplaceEditorObservation | None = None
21
- try:
22
- result = self.editor(
23
- command=action.command,
24
- path=action.path,
25
- file_text=action.file_text,
26
- view_range=action.view_range,
27
- old_str=action.old_str,
28
- new_str=action.new_str,
29
- insert_line=action.insert_line,
30
- )
31
- except ToolError as e:
32
- result = StrReplaceEditorObservation(error=e.message)
33
- assert result is not None, "file_editor should always return a result"
34
- return result
35
-
36
-
37
- def file_editor(
38
- command: CommandLiteral,
39
- path: str,
40
- file_text: str | None = None,
41
- view_range: list[int] | None = None,
42
- old_str: str | None = None,
43
- new_str: str | None = None,
44
- insert_line: int | None = None,
45
- ) -> StrReplaceEditorObservation:
46
- """A global FileEditor instance to be used by the tool."""
47
-
48
- global _GLOBAL_EDITOR
49
- if _GLOBAL_EDITOR is None:
50
- _GLOBAL_EDITOR = FileEditor()
51
-
52
- result: StrReplaceEditorObservation | None = None
53
- try:
54
- result = _GLOBAL_EDITOR(
55
- command=command,
56
- path=path,
57
- file_text=file_text,
58
- view_range=view_range,
59
- old_str=old_str,
60
- new_str=new_str,
61
- insert_line=insert_line,
62
- )
63
- except ToolError as e:
64
- result = StrReplaceEditorObservation(error=e.message)
65
- assert result is not None, "file_editor should always return a result"
66
- return result
File without changes
@@ -1,2 +0,0 @@
1
- MAX_RESPONSE_LEN_CHAR: int = 16000
2
- SNIPPET_CONTEXT_WINDOW: int = 4
@@ -1,9 +0,0 @@
1
- MAX_RESPONSE_LEN_CHAR: int = 16000
2
-
3
- CONTENT_TRUNCATED_NOTICE = "<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>" # noqa: E501
4
-
5
- TEXT_FILE_CONTENT_TRUNCATED_NOTICE: str = "<response clipped><NOTE>Due to the max output limit, only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>" # noqa: E501
6
-
7
- BINARY_FILE_CONTENT_TRUNCATED_NOTICE: str = "<response clipped><NOTE>Due to the max output limit, only part of this file has been shown to you. Please use Python libraries to view the entire file or search for specific content within the file.</NOTE>" # noqa: E501
8
-
9
- DIRECTORY_CONTENT_TRUNCATED_NOTICE: str = "<response clipped><NOTE>Due to the max output limit, only part of this directory has been shown to you. You should use `ls -la` instead to view large directories incrementally.</NOTE>" # noqa: E501
@@ -1,135 +0,0 @@
1
- """Encoding management for file operations."""
2
-
3
- import functools
4
- import inspect
5
- import os
6
- from pathlib import Path
7
- from typing import TYPE_CHECKING, Tuple
8
-
9
- import charset_normalizer
10
- from cachetools import LRUCache
11
-
12
-
13
- if TYPE_CHECKING:
14
- from openhands.tools.str_replace_editor.impl import FileEditor
15
-
16
-
17
- class EncodingManager:
18
- """Manages file encodings across multiple operations to ensure consistency."""
19
-
20
- # Default maximum number of entries in the cache
21
- DEFAULT_MAX_CACHE_SIZE = 1000 # ~= 300 KB
22
-
23
- def __init__(self, max_cache_size=None):
24
- # Cache detected encodings to avoid repeated detection on the same file
25
- # Format: {path_str: (encoding, mtime)}
26
- self._encoding_cache: LRUCache[str, Tuple[str, float]] = LRUCache(
27
- maxsize=max_cache_size or self.DEFAULT_MAX_CACHE_SIZE
28
- )
29
- # Default fallback encoding
30
- self.default_encoding = "utf-8"
31
- # Confidence threshold for encoding detection
32
- self.confidence_threshold = 0.9
33
-
34
- def detect_encoding(self, path: Path) -> str:
35
- """Detect the encoding of a file without handling caching logic.
36
- Args:
37
- path: Path to the file
38
- Returns:
39
- The detected encoding or default encoding if detection fails
40
- """
41
- # Handle non-existent files
42
- if not path.exists():
43
- return self.default_encoding
44
-
45
- # Read a sample of the file to detect encoding
46
- sample_size = min(os.path.getsize(path), 1024 * 1024) # Max 1MB sample
47
- with open(path, "rb") as f:
48
- raw_data = f.read(sample_size)
49
-
50
- # Use charset_normalizer instead of chardet
51
- results = charset_normalizer.detect(raw_data)
52
-
53
- # Get the best match if any exists
54
- if (
55
- results
56
- and results["confidence"]
57
- and results["confidence"] > self.confidence_threshold
58
- and results["encoding"]
59
- ):
60
- encoding = results["encoding"]
61
- # Always use utf-8 instead of ascii for text files to support
62
- # non-ASCII characters. This ensures files initially containing only
63
- # ASCII can later accept non-ASCII content
64
- if encoding.lower() == "ascii":
65
- encoding = self.default_encoding
66
- else:
67
- encoding = self.default_encoding
68
-
69
- return encoding
70
-
71
- def get_encoding(self, path: Path) -> str:
72
- """Get encoding for a file, using cache or detecting if necessary.
73
- Args:
74
- path: Path to the file
75
- Returns:
76
- The encoding for the file
77
- """
78
- path_str = str(path)
79
- # If file doesn't exist, return default encoding
80
- if not path.exists():
81
- return self.default_encoding
82
-
83
- # Get current modification time
84
- current_mtime = os.path.getmtime(path)
85
-
86
- # Check cache for valid entry
87
- if path_str in self._encoding_cache:
88
- cached_encoding, cached_mtime = self._encoding_cache[path_str]
89
- if cached_mtime == current_mtime:
90
- return cached_encoding
91
-
92
- # No valid cache entry, detect encoding
93
- encoding = self.detect_encoding(path)
94
-
95
- # Cache the result with current modification time
96
- self._encoding_cache[path_str] = (encoding, current_mtime)
97
- return encoding
98
-
99
-
100
- def with_encoding(method):
101
- """Decorator to handle file encoding for file operations.
102
- This decorator automatically detects and applies the correct encoding
103
- for file operations, ensuring consistency between read and write operations.
104
- Args:
105
- method: The method to decorate
106
- Returns:
107
- The decorated method
108
- """
109
-
110
- @functools.wraps(method)
111
- def wrapper(self: "FileEditor", path: Path, *args, **kwargs):
112
- # Skip encoding handling for directories
113
- if path.is_dir():
114
- return method(self, path, *args, **kwargs)
115
-
116
- # Check if the method accepts an encoding parameter
117
- sig = inspect.signature(method)
118
- accepts_encoding = "encoding" in sig.parameters
119
-
120
- if accepts_encoding:
121
- # For files that don't exist yet (like in 'create' command),
122
- # use the default encoding
123
- if not path.exists():
124
- if "encoding" not in kwargs:
125
- kwargs["encoding"] = self._encoding_manager.default_encoding
126
- else:
127
- # Get encoding from the encoding manager for existing files
128
- encoding = self._encoding_manager.get_encoding(path)
129
- # Add encoding to kwargs if the method accepts it
130
- if "encoding" not in kwargs:
131
- kwargs["encoding"] = encoding
132
-
133
- return method(self, path, *args, **kwargs)
134
-
135
- return wrapper
@@ -1,154 +0,0 @@
1
- import hashlib
2
- import json
3
- import os
4
- import time
5
- from pathlib import Path
6
- from typing import Any, Optional
7
-
8
- from openhands.sdk.logger import get_logger
9
-
10
-
11
- logger = get_logger(__name__)
12
-
13
-
14
- class FileCache:
15
- def __init__(self, directory: str, size_limit: Optional[int] = None):
16
- self.directory = Path(directory)
17
- self.directory.mkdir(parents=True, exist_ok=True)
18
- self.size_limit = size_limit
19
- self.current_size = 0
20
- self._update_current_size()
21
- logger.debug(
22
- f"FileCache initialized with directory: {self.directory}, "
23
- f"size_limit: {self.size_limit}, current_size: {self.current_size}"
24
- )
25
-
26
- def _get_file_path(self, key: str) -> Path:
27
- hashed_key = hashlib.sha256(key.encode()).hexdigest()
28
- return self.directory / f"{hashed_key}.json"
29
-
30
- def _update_current_size(self):
31
- self.current_size = sum(
32
- f.stat().st_size for f in self.directory.glob("*.json") if f.is_file()
33
- )
34
- logger.debug(f"Current size updated: {self.current_size}")
35
-
36
- def set(self, key: str, value: Any) -> None:
37
- file_path = self._get_file_path(key)
38
- content = json.dumps({"key": key, "value": value})
39
- content_size = len(content.encode("utf-8"))
40
- logger.debug(f"Setting key: {key}, content_size: {content_size}")
41
-
42
- if self.size_limit is not None:
43
- if file_path.exists():
44
- old_size = file_path.stat().st_size
45
- size_diff = content_size - old_size
46
- logger.debug(
47
- f"Existing file: old_size: {old_size}, size_diff: {size_diff}"
48
- )
49
- if size_diff > 0:
50
- while (
51
- self.current_size + size_diff > self.size_limit
52
- and len(self) > 1
53
- ):
54
- logger.debug(
55
- f"Evicting oldest (existing file case): "
56
- f"current_size: {self.current_size}, "
57
- f"size_limit: {self.size_limit}"
58
- )
59
- self._evict_oldest(file_path)
60
- else:
61
- while (
62
- self.current_size + content_size > self.size_limit and len(self) > 1
63
- ):
64
- logger.debug(
65
- f"Evicting oldest (new file case): "
66
- f"current_size: {self.current_size}, "
67
- f"size_limit: {self.size_limit}"
68
- )
69
- self._evict_oldest(file_path)
70
-
71
- if file_path.exists():
72
- self.current_size -= file_path.stat().st_size
73
- logger.debug(
74
- f"Existing file removed from current_size: {self.current_size}"
75
- )
76
-
77
- with open(file_path, "w") as f:
78
- f.write(content)
79
-
80
- self.current_size += content_size
81
- logger.debug(f"File written, new current_size: {self.current_size}")
82
- os.utime(
83
- file_path, (time.time(), time.time())
84
- ) # Update access and modification time
85
-
86
- def _evict_oldest(self, exclude_path: Optional[Path] = None):
87
- oldest_file = min(
88
- (
89
- f
90
- for f in self.directory.glob("*.json")
91
- if f.is_file() and f != exclude_path
92
- ),
93
- key=os.path.getmtime,
94
- )
95
- evicted_size = oldest_file.stat().st_size
96
- self.current_size -= evicted_size
97
- os.remove(oldest_file)
98
- logger.debug(
99
- f"Evicted file: {oldest_file}, size: {evicted_size}, "
100
- f"new current_size: {self.current_size}"
101
- )
102
-
103
- def get(self, key: str, default: Any = None) -> Any:
104
- file_path = self._get_file_path(key)
105
- if not file_path.exists():
106
- logger.debug(f"Get: Key not found: {key}")
107
- return default
108
- with open(file_path, "r") as f:
109
- data = json.load(f)
110
- os.utime(file_path, (time.time(), time.time())) # Update access time
111
- logger.debug(f"Get: Key found: {key}")
112
- return data["value"]
113
-
114
- def delete(self, key: str) -> None:
115
- file_path = self._get_file_path(key)
116
- if file_path.exists():
117
- deleted_size = file_path.stat().st_size
118
- self.current_size -= deleted_size
119
- os.remove(file_path)
120
- logger.debug(
121
- f"Deleted key: {key}, size: {deleted_size}, "
122
- f"new current_size: {self.current_size}"
123
- )
124
-
125
- def clear(self) -> None:
126
- for item in self.directory.glob("*.json"):
127
- if item.is_file():
128
- os.remove(item)
129
- self.current_size = 0
130
- logger.debug("Cache cleared")
131
-
132
- def __contains__(self, key: str) -> bool:
133
- exists = self._get_file_path(key).exists()
134
- logger.debug(f"Contains check: {key}, result: {exists}")
135
- return exists
136
-
137
- def __len__(self) -> int:
138
- length = sum(1 for _ in self.directory.glob("*.json") if _.is_file())
139
- logger.debug(f"Cache length: {length}")
140
- return length
141
-
142
- def __iter__(self):
143
- for file in self.directory.glob("*.json"):
144
- if file.is_file():
145
- with open(file, "r") as f:
146
- data = json.load(f)
147
- logger.debug(f"Yielding key: {data['key']}")
148
- yield data["key"]
149
-
150
- def __getitem__(self, key: str) -> Any:
151
- return self.get(key)
152
-
153
- def __setitem__(self, key: str, value: Any) -> None:
154
- self.set(key, value)
@@ -1,122 +0,0 @@
1
- """History management for file edits with disk-based storage and memory constraints."""
2
-
3
- import logging
4
- import tempfile
5
- from pathlib import Path
6
- from typing import List, Optional
7
-
8
- from openhands.tools.str_replace_editor.utils.file_cache import FileCache
9
-
10
-
11
- class FileHistoryManager:
12
- """Manages file edit history with disk-based storage and memory constraints."""
13
-
14
- def __init__(
15
- self, max_history_per_file: int = 5, history_dir: Optional[Path] = None
16
- ):
17
- """Initialize the history manager.
18
-
19
- Args:
20
- max_history_per_file: Maximum number of history entries to keep per
21
- file (default: 5)
22
- history_dir: Directory to store history files. If None, uses a temp
23
- directory
24
-
25
- Notes:
26
- - Each file's history is limited to the last N entries to conserve
27
- memory
28
- - The file cache is limited to prevent excessive disk usage
29
- - Older entries are automatically removed when limits are exceeded
30
- """
31
- self.max_history_per_file = max_history_per_file
32
- if history_dir is None:
33
- history_dir = Path(tempfile.mkdtemp(prefix="oh_editor_history_"))
34
- self.cache = FileCache(str(history_dir))
35
- self.logger = logging.getLogger(__name__)
36
-
37
- def _get_metadata_key(self, file_path: Path) -> str:
38
- return f"{file_path}.metadata"
39
-
40
- def _get_history_key(self, file_path: Path, counter: int) -> str:
41
- return f"{file_path}.{counter}"
42
-
43
- def add_history(self, file_path: Path, content: str):
44
- """Add a new history entry for a file."""
45
- metadata_key = self._get_metadata_key(file_path)
46
- metadata = self.cache.get(metadata_key, {"entries": [], "counter": 0})
47
- counter = metadata["counter"]
48
-
49
- # Add new entry
50
- history_key = self._get_history_key(file_path, counter)
51
- self.cache.set(history_key, content)
52
-
53
- metadata["entries"].append(counter)
54
- metadata["counter"] += 1
55
-
56
- # Keep only last N entries
57
- while len(metadata["entries"]) > self.max_history_per_file:
58
- old_counter = metadata["entries"].pop(0)
59
- old_history_key = self._get_history_key(file_path, old_counter)
60
- self.cache.delete(old_history_key)
61
-
62
- self.cache.set(metadata_key, metadata)
63
-
64
- def pop_last_history(self, file_path: Path) -> Optional[str]:
65
- """Pop and return the most recent history entry for a file."""
66
- metadata_key = self._get_metadata_key(file_path)
67
- metadata = self.cache.get(metadata_key, {"entries": [], "counter": 0})
68
- entries = metadata["entries"]
69
-
70
- if not entries:
71
- return None
72
-
73
- # Pop and remove the last entry
74
- last_counter = entries.pop()
75
- history_key = self._get_history_key(file_path, last_counter)
76
- content = self.cache.get(history_key)
77
-
78
- if content is None:
79
- self.logger.warning(f"History entry not found for {file_path}")
80
- else:
81
- # Remove the entry from the cache
82
- self.cache.delete(history_key)
83
-
84
- # Update metadata
85
- metadata["entries"] = entries
86
- self.cache.set(metadata_key, metadata)
87
-
88
- return content
89
-
90
- def get_metadata(self, file_path: Path):
91
- """Get metadata for a file (for testing purposes)."""
92
- metadata_key = self._get_metadata_key(file_path)
93
- metadata = self.cache.get(metadata_key, {"entries": [], "counter": 0})
94
- return metadata # Return the actual metadata, not a copy
95
-
96
- def clear_history(self, file_path: Path):
97
- """Clear history for a given file."""
98
- metadata_key = self._get_metadata_key(file_path)
99
- metadata = self.cache.get(metadata_key, {"entries": [], "counter": 0})
100
-
101
- # Delete all history entries
102
- for counter in metadata["entries"]:
103
- history_key = self._get_history_key(file_path, counter)
104
- self.cache.delete(history_key)
105
-
106
- # Clear metadata
107
- self.cache.set(metadata_key, {"entries": [], "counter": 0})
108
-
109
- def get_all_history(self, file_path: Path) -> List[str]:
110
- """Get all history entries for a file."""
111
- metadata_key = self._get_metadata_key(file_path)
112
- metadata = self.cache.get(metadata_key, {"entries": [], "counter": 0})
113
- entries = metadata["entries"]
114
-
115
- history = []
116
- for counter in entries:
117
- history_key = self._get_history_key(file_path, counter)
118
- content = self.cache.get(history_key)
119
- if content is not None:
120
- history.append(content)
121
-
122
- return history
@@ -1,72 +0,0 @@
1
- import os
2
- import subprocess
3
- import time
4
-
5
- from openhands.sdk.utils.truncate import maybe_truncate
6
- from openhands.tools.str_replace_editor.utils.constants import (
7
- CONTENT_TRUNCATED_NOTICE,
8
- MAX_RESPONSE_LEN_CHAR,
9
- )
10
-
11
-
12
- def run_shell_cmd(
13
- cmd: str,
14
- timeout: float | None = 120.0, # seconds
15
- truncate_after: int | None = MAX_RESPONSE_LEN_CHAR,
16
- truncate_notice: str = CONTENT_TRUNCATED_NOTICE,
17
- ) -> tuple[int, str, str]:
18
- """Run a shell command synchronously with a timeout.
19
-
20
- Args:
21
- cmd: The shell command to run.
22
- timeout: The maximum time to wait for the command to complete.
23
- truncate_after: The maximum number of characters to return for stdout
24
- and stderr.
25
-
26
- Returns:
27
- A tuple containing the return code, stdout, and stderr.
28
- """
29
-
30
- start_time = time.time()
31
-
32
- process: subprocess.Popen | None = None
33
- try:
34
- process = subprocess.Popen(
35
- cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
36
- )
37
-
38
- stdout, stderr = process.communicate(timeout=timeout)
39
-
40
- return (
41
- process.returncode or 0,
42
- maybe_truncate(
43
- stdout, truncate_after=truncate_after, truncate_notice=truncate_notice
44
- ),
45
- maybe_truncate(
46
- stderr,
47
- truncate_after=truncate_after,
48
- truncate_notice=CONTENT_TRUNCATED_NOTICE,
49
- ), # Use generic notice for stderr
50
- )
51
- except subprocess.TimeoutExpired:
52
- if process:
53
- process.kill()
54
- elapsed_time = time.time() - start_time
55
- raise TimeoutError(
56
- f"Command '{cmd}' timed out after {elapsed_time:.2f} seconds"
57
- )
58
-
59
-
60
- def check_tool_installed(tool_name: str) -> bool:
61
- """Check if a tool is installed."""
62
- try:
63
- subprocess.run(
64
- [tool_name, "--version"],
65
- check=True,
66
- cwd=os.getcwd(),
67
- stdout=subprocess.PIPE,
68
- stderr=subprocess.PIPE,
69
- )
70
- return True
71
- except (subprocess.CalledProcessError, FileNotFoundError):
72
- return False
@@ -1,16 +0,0 @@
1
- from .definition import (
2
- TaskTrackerAction,
3
- TaskTrackerExecutor,
4
- TaskTrackerObservation,
5
- TaskTrackerTool,
6
- task_tracker_tool,
7
- )
8
-
9
-
10
- __all__ = [
11
- "TaskTrackerAction",
12
- "TaskTrackerExecutor",
13
- "TaskTrackerObservation",
14
- "TaskTrackerTool",
15
- "task_tracker_tool",
16
- ]