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.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- 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,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
|
-
]
|