openhands-tools 1.29.2__tar.gz → 1.30.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.
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/PKG-INFO +1 -1
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/apply_patch/core.py +34 -11
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/apply_patch/definition.py +1 -1
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/editor.py +4 -2
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/impl.py +1 -6
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/__init__.py +6 -1
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/default.py +32 -12
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task/manager.py +1 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/definition.py +7 -1
- openhands_tools-1.30.0/openhands/tools/terminal/env.py +39 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/impl.py +12 -6
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/factory.py +17 -5
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +8 -2
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +13 -4
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/tmux_terminal.py +8 -2
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/windows_terminal.py +8 -2
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/PKG-INFO +1 -1
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/SOURCES.txt +2 -2
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/pyproject.toml +1 -1
- openhands_tools-1.29.2/openhands/tools/delegate/templates/delegate_tool_description.j2 +0 -27
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/apply_patch/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/event_storage.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/logging_fix.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/recording.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/server.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/delegate/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/delegate/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/delegate/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/delegate/visualizer.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/exceptions.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/config.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/constants.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/diff.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/history.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/shell.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/edit/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/glob/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/glob/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/glob/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/grep/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/grep/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/grep/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/gemini.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/gpt5.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/planning.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/default.md +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/py.typed +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task_tracker/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task_tracker/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/constants.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/descriptions.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/metadata.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/interface.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/timeout_policy.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/command.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/tom_consult/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/tom_consult/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/tom_consult/executor.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/utils/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/utils/timeout.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/workflow/__init__.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/workflow/definition.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/workflow/impl.py +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/requires.txt +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/top_level.txt +0 -0
- {openhands_tools-1.29.2 → openhands_tools-1.30.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: OpenHands Tools - Runtime tools for AI agents
|
|
5
5
|
Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
|
|
6
6
|
Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
|
|
@@ -59,7 +59,9 @@ def assemble_changes(
|
|
|
59
59
|
old_content=old_content,
|
|
60
60
|
)
|
|
61
61
|
else:
|
|
62
|
-
|
|
62
|
+
raise DiffError(
|
|
63
|
+
f"Unexpected state: path '{path}' exists in neither orig nor dest"
|
|
64
|
+
)
|
|
63
65
|
return commit
|
|
64
66
|
|
|
65
67
|
|
|
@@ -95,13 +97,15 @@ class Parser(BaseModel):
|
|
|
95
97
|
return False
|
|
96
98
|
|
|
97
99
|
def startswith(self, prefix: str | tuple[str, ...]) -> bool:
|
|
98
|
-
|
|
100
|
+
if self.index >= len(self.lines):
|
|
101
|
+
raise DiffError(f"Unexpected end of patch at index {self.index}")
|
|
99
102
|
if self.lines[self.index].startswith(prefix):
|
|
100
103
|
return True
|
|
101
104
|
return False
|
|
102
105
|
|
|
103
106
|
def read_str(self, prefix: str = "", return_everything: bool = False) -> str:
|
|
104
|
-
|
|
107
|
+
if self.index >= len(self.lines):
|
|
108
|
+
raise DiffError(f"Unexpected end of patch at index {self.index}")
|
|
105
109
|
line = self.lines[self.index]
|
|
106
110
|
if line.startswith(prefix):
|
|
107
111
|
text = line if return_everything else line[len(prefix) :]
|
|
@@ -116,11 +120,15 @@ class Parser(BaseModel):
|
|
|
116
120
|
if path in self.patch.actions:
|
|
117
121
|
raise DiffError(f"Update File Error: Duplicate Path: {path}")
|
|
118
122
|
move_to = self.read_str("*** Move to: ")
|
|
123
|
+
if move_to and (".." in move_to.split("/") or move_to.startswith("/")):
|
|
124
|
+
raise DiffError(
|
|
125
|
+
f"Update File Error: Invalid move path '{move_to}': "
|
|
126
|
+
"must be a relative path without '..' components"
|
|
127
|
+
)
|
|
119
128
|
if path not in self.current_files:
|
|
120
129
|
raise DiffError(f"Update File Error: Missing File: {path}")
|
|
121
130
|
text = self.current_files[path]
|
|
122
131
|
action = self.parse_update_file(text)
|
|
123
|
-
# TODO: Check move_to is valid
|
|
124
132
|
action.move_path = move_to
|
|
125
133
|
self.patch.actions[path] = action
|
|
126
134
|
continue
|
|
@@ -359,7 +367,10 @@ def identify_files_needed(text: str) -> list[str]:
|
|
|
359
367
|
|
|
360
368
|
|
|
361
369
|
def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
|
|
362
|
-
|
|
370
|
+
if action.type != ActionType.UPDATE:
|
|
371
|
+
raise DiffError(
|
|
372
|
+
f"_get_updated_file: expected UPDATE action for '{path}', got {action.type}"
|
|
373
|
+
)
|
|
363
374
|
orig_lines = text.split("\n")
|
|
364
375
|
dest_lines = []
|
|
365
376
|
orig_index = 0
|
|
@@ -375,7 +386,6 @@ def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
|
|
|
375
386
|
f"_get_updated_file: {path}: orig_index {orig_index} > "
|
|
376
387
|
f"chunk.orig_index {chunk.orig_index}"
|
|
377
388
|
)
|
|
378
|
-
assert orig_index <= chunk.orig_index
|
|
379
389
|
dest_lines.extend(orig_lines[orig_index : chunk.orig_index])
|
|
380
390
|
delta = chunk.orig_index - orig_index
|
|
381
391
|
orig_index += delta
|
|
@@ -389,8 +399,16 @@ def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
|
|
|
389
399
|
delta = len(orig_lines) - orig_index
|
|
390
400
|
orig_index += delta
|
|
391
401
|
dest_index += delta
|
|
392
|
-
|
|
393
|
-
|
|
402
|
+
if orig_index != len(orig_lines):
|
|
403
|
+
raise DiffError(
|
|
404
|
+
f"_get_updated_file: {path}: did not consume all original lines "
|
|
405
|
+
f"(orig_index={orig_index}, len={len(orig_lines)})"
|
|
406
|
+
)
|
|
407
|
+
if dest_index != len(dest_lines):
|
|
408
|
+
raise DiffError(
|
|
409
|
+
f"_get_updated_file: {path}: dest line count mismatch "
|
|
410
|
+
f"(dest_index={dest_index}, len={len(dest_lines)})"
|
|
411
|
+
)
|
|
394
412
|
return "\n".join(dest_lines)
|
|
395
413
|
|
|
396
414
|
|
|
@@ -449,10 +467,14 @@ def apply_commit(
|
|
|
449
467
|
if change.type == ActionType.DELETE:
|
|
450
468
|
remove_fn(path)
|
|
451
469
|
elif change.type == ActionType.ADD:
|
|
452
|
-
|
|
470
|
+
if change.new_content is None:
|
|
471
|
+
raise DiffError(f"apply_commit: ADD change for '{path}' has no content")
|
|
453
472
|
write_fn(path, change.new_content)
|
|
454
473
|
elif change.type == ActionType.UPDATE:
|
|
455
|
-
|
|
474
|
+
if change.new_content is None:
|
|
475
|
+
raise DiffError(
|
|
476
|
+
f"apply_commit: UPDATE change for '{path}' has no content"
|
|
477
|
+
)
|
|
456
478
|
if change.move_path:
|
|
457
479
|
write_fn(change.move_path, change.new_content)
|
|
458
480
|
remove_fn(path)
|
|
@@ -470,7 +492,8 @@ def process_patch(
|
|
|
470
492
|
|
|
471
493
|
Returns (message, fuzz, commit)
|
|
472
494
|
"""
|
|
473
|
-
|
|
495
|
+
if not text.startswith("*** Begin Patch"):
|
|
496
|
+
raise DiffError("Invalid patch: must start with '*** Begin Patch'")
|
|
474
497
|
paths = identify_files_needed(text)
|
|
475
498
|
orig = load_files(paths, open_fn)
|
|
476
499
|
patch, fuzz = text_to_patch(text, orig)
|
|
@@ -77,7 +77,7 @@ class ApplyPatchExecutor(ToolExecutor[ApplyPatchAction, ApplyPatchObservation]):
|
|
|
77
77
|
if not p.startswith("/")
|
|
78
78
|
else Path(p).resolve()
|
|
79
79
|
)
|
|
80
|
-
if not
|
|
80
|
+
if not pth.is_relative_to(self.workspace_root):
|
|
81
81
|
raise DiffError("Absolute or escaping paths are not allowed")
|
|
82
82
|
return pth
|
|
83
83
|
|
|
@@ -214,9 +214,11 @@ class FileEditor:
|
|
|
214
214
|
if not occurrences:
|
|
215
215
|
# We found no occurrences, possibly because of extra white spaces at
|
|
216
216
|
# either the front or back of the string.
|
|
217
|
-
#
|
|
217
|
+
# Strip old_str to retry the *match* only. Do NOT strip new_str: it
|
|
218
|
+
# is the replacement content, and stripping it would silently drop
|
|
219
|
+
# meaningful leading/trailing whitespace (e.g. a Markdown hard line
|
|
220
|
+
# break or intentional indentation) the caller asked to write.
|
|
218
221
|
old_str = old_str.strip()
|
|
219
|
-
new_str = new_str.strip()
|
|
220
222
|
pattern = re.escape(old_str)
|
|
221
223
|
occurrences = [
|
|
222
224
|
(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Edit tool executor implementation."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
@@ -43,11 +42,7 @@ class EditExecutor(ToolExecutor[EditAction, EditObservation]):
|
|
|
43
42
|
new_string = action.new_string
|
|
44
43
|
expected_replacements = action.expected_replacements
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
if not os.path.isabs(file_path):
|
|
48
|
-
resolved_path = self.workspace_root / file_path
|
|
49
|
-
else:
|
|
50
|
-
resolved_path = Path(file_path)
|
|
45
|
+
resolved_path = (self.workspace_root / file_path).resolve()
|
|
51
46
|
|
|
52
47
|
# Handle file creation (old_string is empty)
|
|
53
48
|
if old_string == "":
|
|
@@ -18,13 +18,18 @@ Notes:
|
|
|
18
18
|
setups.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
-
from .default import
|
|
21
|
+
from .default import (
|
|
22
|
+
discover_builtin_agents,
|
|
23
|
+
get_default_agent,
|
|
24
|
+
register_builtins_agents,
|
|
25
|
+
)
|
|
22
26
|
from .gemini import get_gemini_agent, get_gemini_tools
|
|
23
27
|
from .gpt5 import get_gpt5_agent
|
|
24
28
|
from .planning import get_planning_agent
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
__all__ = [
|
|
32
|
+
"discover_builtin_agents",
|
|
28
33
|
"get_default_agent",
|
|
29
34
|
"get_gemini_agent",
|
|
30
35
|
"get_gemini_tools",
|
|
@@ -7,7 +7,7 @@ from openhands.sdk.context.condenser import default_condenser
|
|
|
7
7
|
from openhands.sdk.context.condenser.base import CondenserBase
|
|
8
8
|
from openhands.sdk.llm.llm import LLM
|
|
9
9
|
from openhands.sdk.logger import get_logger
|
|
10
|
-
from openhands.sdk.subagent import register_agent_if_absent
|
|
10
|
+
from openhands.sdk.subagent import AgentDefinition, register_agent_if_absent
|
|
11
11
|
from openhands.sdk.tool import Tool
|
|
12
12
|
|
|
13
13
|
|
|
@@ -89,20 +89,18 @@ def get_default_agent(
|
|
|
89
89
|
return agent
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
93
|
-
"""Load
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
def discover_builtin_agents(enable_browser: bool = True) -> list[AgentDefinition]:
|
|
93
|
+
"""Load builtin agent definitions (``level='builtin'``) without registering them.
|
|
94
|
+
|
|
95
|
+
Non-mutating counterpart to ``register_builtins_agents``. Browser-only agents
|
|
96
|
+
are skipped when ``enable_browser`` is False.
|
|
97
|
+
|
|
97
98
|
Args:
|
|
98
|
-
enable_browser:
|
|
99
|
-
|
|
100
|
-
skipped.
|
|
99
|
+
enable_browser: When False, skip agents needing browser tools (web researcher).
|
|
100
|
+
|
|
101
101
|
Returns:
|
|
102
|
-
|
|
102
|
+
Builtin agent definitions with ``level="builtin"``.
|
|
103
103
|
"""
|
|
104
|
-
register_default_tools(enable_browser=enable_browser)
|
|
105
|
-
|
|
106
104
|
subagent_dir = Path(__file__).parent / "subagents"
|
|
107
105
|
builtins_agents_def = load_agents_from_dir(subagent_dir)
|
|
108
106
|
|
|
@@ -115,6 +113,28 @@ def register_builtins_agents(enable_browser: bool = True) -> list[str]:
|
|
|
115
113
|
if agent.name not in _browser_only_agents
|
|
116
114
|
]
|
|
117
115
|
|
|
116
|
+
return [
|
|
117
|
+
agent_def.model_copy(update={"level": "builtin"})
|
|
118
|
+
for agent_def in builtins_agents_def
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def register_builtins_agents(enable_browser: bool = True) -> list[str]:
|
|
123
|
+
"""Load and register builtin agents from ``subagent/*.md``.
|
|
124
|
+
They are registered via `register_agent_if_absent` and will not
|
|
125
|
+
overwrite agents already registered by programmatic calls, plugins,
|
|
126
|
+
or project/user-level file-based definitions.
|
|
127
|
+
Args:
|
|
128
|
+
enable_browser: Whether browser tools are available. When False,
|
|
129
|
+
agents that require browser tools (e.g. web researcher) are
|
|
130
|
+
skipped.
|
|
131
|
+
Returns:
|
|
132
|
+
List of agents which were actually registered.
|
|
133
|
+
"""
|
|
134
|
+
register_default_tools(enable_browser=enable_browser)
|
|
135
|
+
|
|
136
|
+
builtins_agents_def = discover_builtin_agents(enable_browser=enable_browser)
|
|
137
|
+
|
|
118
138
|
registered: list[str] = []
|
|
119
139
|
for agent_def in builtins_agents_def:
|
|
120
140
|
factory = agent_definition_to_factory(agent_def)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
|
-
from collections.abc import Sequence
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
6
|
from typing import TYPE_CHECKING, Literal
|
|
7
7
|
|
|
8
8
|
from pydantic import Field
|
|
@@ -288,6 +288,8 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
|
|
|
288
288
|
terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
|
|
289
289
|
shell_path: str | None = None,
|
|
290
290
|
executor: ToolExecutor | None = None,
|
|
291
|
+
*,
|
|
292
|
+
env: Mapping[str, str] | None = None,
|
|
291
293
|
) -> Sequence["TerminalTool"]:
|
|
292
294
|
"""Initialize TerminalTool with executor parameters.
|
|
293
295
|
|
|
@@ -305,6 +307,9 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
|
|
|
305
307
|
shell_path: Path to the shell binary. On Unix this applies to the
|
|
306
308
|
subprocess backend; on Windows it can point to a
|
|
307
309
|
PowerShell executable.
|
|
310
|
+
env: Extra environment variables to add to the terminal session.
|
|
311
|
+
These are client-controlled session settings and are not part
|
|
312
|
+
of the LLM-facing TerminalAction schema.
|
|
308
313
|
"""
|
|
309
314
|
# Import here to avoid circular imports
|
|
310
315
|
from openhands.tools.terminal.impl import TerminalExecutor
|
|
@@ -321,6 +326,7 @@ class TerminalTool(ToolDefinition[TerminalAction, TerminalObservation]):
|
|
|
321
326
|
no_change_timeout_seconds=no_change_timeout_seconds,
|
|
322
327
|
terminal_type=terminal_type,
|
|
323
328
|
shell_path=shell_path,
|
|
329
|
+
env=env,
|
|
324
330
|
full_output_save_dir=conv_state.env_observation_persistence_dir,
|
|
325
331
|
)
|
|
326
332
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Environment helpers for terminal backends."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
|
|
6
|
+
from openhands.sdk.utils import sanitized_env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_terminal_env(
|
|
13
|
+
extra_env: Mapping[str, str] | None,
|
|
14
|
+
) -> dict[str, str] | None:
|
|
15
|
+
"""Validate and copy client-provided terminal environment variables."""
|
|
16
|
+
if extra_env is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
normalized: dict[str, str] = {}
|
|
20
|
+
for key, value in extra_env.items():
|
|
21
|
+
if not isinstance(key, str) or not ENV_VAR_NAME_RE.match(key):
|
|
22
|
+
raise ValueError(f"Invalid terminal environment variable name: {key!r}")
|
|
23
|
+
if not isinstance(value, str):
|
|
24
|
+
raise TypeError(
|
|
25
|
+
"Terminal environment variable values must be strings: "
|
|
26
|
+
f"{key!r}={value!r}"
|
|
27
|
+
)
|
|
28
|
+
normalized[key] = value
|
|
29
|
+
return normalized
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_terminal_env(extra_env: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
33
|
+
"""Return the sanitized process environment plus client-provided overrides."""
|
|
34
|
+
env = sanitized_env()
|
|
35
|
+
normalized = normalize_terminal_env(extra_env)
|
|
36
|
+
if normalized:
|
|
37
|
+
env.update(normalized)
|
|
38
|
+
env = sanitized_env(env)
|
|
39
|
+
return env
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import re
|
|
2
1
|
import threading
|
|
3
2
|
import time
|
|
3
|
+
from collections.abc import Mapping
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from typing import TYPE_CHECKING, Literal
|
|
6
6
|
|
|
@@ -20,6 +20,10 @@ from openhands.tools.terminal.definition import (
|
|
|
20
20
|
TerminalObservation,
|
|
21
21
|
looks_like_python_literal_argument,
|
|
22
22
|
)
|
|
23
|
+
from openhands.tools.terminal.env import (
|
|
24
|
+
ENV_VAR_NAME_RE,
|
|
25
|
+
normalize_terminal_env,
|
|
26
|
+
)
|
|
23
27
|
from openhands.tools.terminal.terminal.factory import (
|
|
24
28
|
_is_tmux_available,
|
|
25
29
|
create_terminal_session,
|
|
@@ -55,10 +59,6 @@ _TMUX_RECOVERABLE_ERROR_MARKERS = (
|
|
|
55
59
|
|
|
56
60
|
logger = get_logger(__name__)
|
|
57
61
|
|
|
58
|
-
# Environment variable names must be alphanumeric + underscores, starting with
|
|
59
|
-
# a letter or underscore. This guards against shell injection via key names.
|
|
60
|
-
_ENV_VAR_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
|
61
|
-
|
|
62
62
|
|
|
63
63
|
class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
64
64
|
shell_path: str | None
|
|
@@ -70,6 +70,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
70
70
|
no_change_timeout_seconds: int | None = None,
|
|
71
71
|
terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
|
|
72
72
|
shell_path: str | None = None,
|
|
73
|
+
env: Mapping[str, str] | None = None,
|
|
73
74
|
full_output_save_dir: str | None = None,
|
|
74
75
|
max_panes: int = DEFAULT_MAX_PANES,
|
|
75
76
|
):
|
|
@@ -85,6 +86,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
85
86
|
shell_path: Path to the shell binary. On Unix this applies to the
|
|
86
87
|
subprocess backend; on Windows it can point to a
|
|
87
88
|
PowerShell executable.
|
|
89
|
+
env: Extra environment variables to add to the terminal session.
|
|
88
90
|
full_output_save_dir: Path to directory to save full output
|
|
89
91
|
logs and files, used when truncation is needed.
|
|
90
92
|
max_panes: Maximum number of concurrent panes in pool mode.
|
|
@@ -94,6 +96,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
94
96
|
self._username = username
|
|
95
97
|
self._no_change_timeout_seconds = no_change_timeout_seconds
|
|
96
98
|
self._terminal_type = terminal_type
|
|
99
|
+
self._env = normalize_terminal_env(env)
|
|
97
100
|
self._max_panes = max_panes
|
|
98
101
|
self.full_output_save_dir: str | None = full_output_save_dir
|
|
99
102
|
|
|
@@ -115,6 +118,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
115
118
|
no_change_timeout_seconds=no_change_timeout_seconds,
|
|
116
119
|
terminal_type=terminal_type,
|
|
117
120
|
shell_path=shell_path,
|
|
121
|
+
env=self._env,
|
|
118
122
|
)
|
|
119
123
|
self._session.initialize()
|
|
120
124
|
logger.info(
|
|
@@ -134,6 +138,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
134
138
|
self._pool = TmuxPanePool(
|
|
135
139
|
self._working_dir,
|
|
136
140
|
self._username,
|
|
141
|
+
env=self._env,
|
|
137
142
|
max_panes=self._max_panes,
|
|
138
143
|
)
|
|
139
144
|
self._pool.initialize()
|
|
@@ -288,7 +293,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
288
293
|
) -> str:
|
|
289
294
|
valid: dict[str, str] = {}
|
|
290
295
|
for key, value in env_vars.items():
|
|
291
|
-
if
|
|
296
|
+
if ENV_VAR_NAME_RE.match(key):
|
|
292
297
|
valid[key] = value
|
|
293
298
|
else:
|
|
294
299
|
logger.warning("Skipping secret with invalid env var name: %r", key)
|
|
@@ -400,6 +405,7 @@ class TerminalExecutor(ToolExecutor[TerminalAction, TerminalObservation]):
|
|
|
400
405
|
no_change_timeout_seconds=original_no_change_timeout,
|
|
401
406
|
terminal_type=None,
|
|
402
407
|
shell_path=self.shell_path,
|
|
408
|
+
env=self._env,
|
|
403
409
|
)
|
|
404
410
|
self._session.initialize()
|
|
405
411
|
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/factory.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import platform
|
|
4
4
|
import subprocess
|
|
5
5
|
import warnings
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from typing import Literal
|
|
7
8
|
|
|
8
9
|
from openhands.sdk.logger import get_logger
|
|
@@ -64,6 +65,7 @@ def _create_windows_terminal(
|
|
|
64
65
|
username: str | None,
|
|
65
66
|
no_change_timeout_seconds: int | None,
|
|
66
67
|
shell_path: str | None,
|
|
68
|
+
env: Mapping[str, str] | None,
|
|
67
69
|
) -> TerminalSession:
|
|
68
70
|
from openhands.tools.terminal.terminal.windows_terminal import WindowsTerminal
|
|
69
71
|
|
|
@@ -71,7 +73,12 @@ def _create_windows_terminal(
|
|
|
71
73
|
if resolved_shell_path is None:
|
|
72
74
|
raise RuntimeError("PowerShell is not available on this system")
|
|
73
75
|
|
|
74
|
-
terminal = WindowsTerminal(
|
|
76
|
+
terminal = WindowsTerminal(
|
|
77
|
+
work_dir,
|
|
78
|
+
username,
|
|
79
|
+
shell_path=resolved_shell_path,
|
|
80
|
+
env=env,
|
|
81
|
+
)
|
|
75
82
|
return TerminalSession(terminal, no_change_timeout_seconds)
|
|
76
83
|
|
|
77
84
|
|
|
@@ -81,6 +88,7 @@ def create_terminal_session(
|
|
|
81
88
|
no_change_timeout_seconds: int | None = None,
|
|
82
89
|
terminal_type: Literal["tmux", "subprocess", "powershell"] | None = None,
|
|
83
90
|
shell_path: str | None = None,
|
|
91
|
+
env: Mapping[str, str] | None = None,
|
|
84
92
|
) -> TerminalSession:
|
|
85
93
|
"""Create an appropriate terminal session based on system capabilities.
|
|
86
94
|
|
|
@@ -92,6 +100,7 @@ def create_terminal_session(
|
|
|
92
100
|
or 'powershell'). If None, auto-detect based on system capabilities.
|
|
93
101
|
shell_path: Path to the shell binary. On Unix this is used for the
|
|
94
102
|
subprocess backend; on Windows it can point to a PowerShell binary.
|
|
103
|
+
env: Extra environment variables to add to the terminal session.
|
|
95
104
|
|
|
96
105
|
Returns:
|
|
97
106
|
TerminalSession instance
|
|
@@ -106,7 +115,7 @@ def create_terminal_session(
|
|
|
106
115
|
from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
|
|
107
116
|
|
|
108
117
|
logger.info("Using forced TmuxTerminal")
|
|
109
|
-
terminal = TmuxTerminal(work_dir, username)
|
|
118
|
+
terminal = TmuxTerminal(work_dir, username, env=env)
|
|
110
119
|
return TerminalSession(terminal, no_change_timeout_seconds)
|
|
111
120
|
|
|
112
121
|
if terminal_type == "powershell":
|
|
@@ -116,6 +125,7 @@ def create_terminal_session(
|
|
|
116
125
|
username,
|
|
117
126
|
no_change_timeout_seconds,
|
|
118
127
|
shell_path,
|
|
128
|
+
env,
|
|
119
129
|
)
|
|
120
130
|
|
|
121
131
|
if terminal_type == "subprocess":
|
|
@@ -130,13 +140,14 @@ def create_terminal_session(
|
|
|
130
140
|
username,
|
|
131
141
|
no_change_timeout_seconds,
|
|
132
142
|
shell_path,
|
|
143
|
+
env,
|
|
133
144
|
)
|
|
134
145
|
from openhands.tools.terminal.terminal.subprocess_terminal import (
|
|
135
146
|
SubprocessTerminal,
|
|
136
147
|
)
|
|
137
148
|
|
|
138
149
|
logger.info("Using forced SubprocessTerminal")
|
|
139
|
-
terminal = SubprocessTerminal(work_dir, username, shell_path)
|
|
150
|
+
terminal = SubprocessTerminal(work_dir, username, shell_path, env=env)
|
|
140
151
|
return TerminalSession(terminal, no_change_timeout_seconds)
|
|
141
152
|
|
|
142
153
|
raise ValueError(f"Unknown session type: {terminal_type}")
|
|
@@ -148,13 +159,14 @@ def create_terminal_session(
|
|
|
148
159
|
username,
|
|
149
160
|
no_change_timeout_seconds,
|
|
150
161
|
shell_path,
|
|
162
|
+
env,
|
|
151
163
|
)
|
|
152
164
|
|
|
153
165
|
if _is_tmux_available():
|
|
154
166
|
from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
|
|
155
167
|
|
|
156
168
|
logger.info("Auto-detected: Using TmuxTerminal (tmux available)")
|
|
157
|
-
terminal = TmuxTerminal(work_dir, username)
|
|
169
|
+
terminal = TmuxTerminal(work_dir, username, env=env)
|
|
158
170
|
return TerminalSession(terminal, no_change_timeout_seconds)
|
|
159
171
|
|
|
160
172
|
from openhands.tools.terminal.terminal.subprocess_terminal import (
|
|
@@ -168,5 +180,5 @@ def create_terminal_session(
|
|
|
168
180
|
)
|
|
169
181
|
logger.warning(_tmux_warning)
|
|
170
182
|
warnings.warn(_tmux_warning, stacklevel=2)
|
|
171
|
-
terminal = SubprocessTerminal(work_dir, username, shell_path)
|
|
183
|
+
terminal = SubprocessTerminal(work_dir, username, shell_path, env=env)
|
|
172
184
|
return TerminalSession(terminal, no_change_timeout_seconds)
|
|
@@ -9,6 +9,7 @@ import subprocess
|
|
|
9
9
|
import threading
|
|
10
10
|
import time
|
|
11
11
|
from collections import deque
|
|
12
|
+
from collections.abc import Mapping
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
if platform.system() == "Windows":
|
|
@@ -22,12 +23,15 @@ import pty
|
|
|
22
23
|
import select
|
|
23
24
|
|
|
24
25
|
from openhands.sdk.logger import get_logger
|
|
25
|
-
from openhands.sdk.utils import sanitized_env
|
|
26
26
|
from openhands.tools.terminal.constants import (
|
|
27
27
|
CMD_OUTPUT_PS1_BEGIN,
|
|
28
28
|
CMD_OUTPUT_PS1_END,
|
|
29
29
|
HISTORY_LIMIT,
|
|
30
30
|
)
|
|
31
|
+
from openhands.tools.terminal.env import (
|
|
32
|
+
build_terminal_env,
|
|
33
|
+
normalize_terminal_env,
|
|
34
|
+
)
|
|
31
35
|
from openhands.tools.terminal.metadata import CmdOutputMetadata
|
|
32
36
|
from openhands.tools.terminal.terminal import TerminalInterface
|
|
33
37
|
from openhands.tools.terminal.terminal.interface import parse_ctrl_key
|
|
@@ -84,6 +88,7 @@ class SubprocessTerminal(TerminalInterface):
|
|
|
84
88
|
work_dir: str,
|
|
85
89
|
username: str | None = None,
|
|
86
90
|
shell_path: str | None = None,
|
|
91
|
+
env: Mapping[str, str] | None = None,
|
|
87
92
|
):
|
|
88
93
|
super().__init__(work_dir, username)
|
|
89
94
|
self.PS1 = CmdOutputMetadata.to_ps1_prompt()
|
|
@@ -96,6 +101,7 @@ class SubprocessTerminal(TerminalInterface):
|
|
|
96
101
|
self.reader_thread = None
|
|
97
102
|
self._current_command_running = False
|
|
98
103
|
self.shell_path = shell_path
|
|
104
|
+
self._env = normalize_terminal_env(env)
|
|
99
105
|
|
|
100
106
|
# ------------------------- Lifecycle -------------------------
|
|
101
107
|
|
|
@@ -136,7 +142,7 @@ class SubprocessTerminal(TerminalInterface):
|
|
|
136
142
|
logger.info(f"Using shell: {resolved_shell_path}")
|
|
137
143
|
|
|
138
144
|
# Inherit environment variables from the parent process
|
|
139
|
-
env =
|
|
145
|
+
env = build_terminal_env(self._env)
|
|
140
146
|
# Disable interactive pagers (git, man, systemctl, ...) so commands that
|
|
141
147
|
# auto-launch `less` on a TTY don't capture the PTY and wedge the session.
|
|
142
148
|
env.setdefault("GIT_PAGER", "cat")
|
|
@@ -10,7 +10,7 @@ import threading
|
|
|
10
10
|
import time
|
|
11
11
|
import uuid
|
|
12
12
|
from collections import deque
|
|
13
|
-
from collections.abc import Iterator
|
|
13
|
+
from collections.abc import Iterator, Mapping
|
|
14
14
|
from contextlib import contextmanager, suppress
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
16
|
from typing import Final
|
|
@@ -18,13 +18,16 @@ from typing import Final
|
|
|
18
18
|
import libtmux
|
|
19
19
|
|
|
20
20
|
from openhands.sdk.logger import get_logger
|
|
21
|
-
from openhands.sdk.utils import sanitized_env
|
|
22
21
|
from openhands.tools.terminal.constants import (
|
|
23
22
|
HISTORY_LIMIT,
|
|
24
23
|
TMUX_SESSION_HEIGHT,
|
|
25
24
|
TMUX_SESSION_WIDTH,
|
|
26
25
|
TMUX_SOCKET_NAME,
|
|
27
26
|
)
|
|
27
|
+
from openhands.tools.terminal.env import (
|
|
28
|
+
build_terminal_env,
|
|
29
|
+
normalize_terminal_env,
|
|
30
|
+
)
|
|
28
31
|
from openhands.tools.terminal.terminal.tmux_terminal import TmuxTerminal
|
|
29
32
|
|
|
30
33
|
|
|
@@ -80,6 +83,7 @@ class TmuxPanePool:
|
|
|
80
83
|
|
|
81
84
|
work_dir: str
|
|
82
85
|
username: str | None = None
|
|
86
|
+
env: Mapping[str, str] | None = None
|
|
83
87
|
max_panes: int = DEFAULT_MAX_PANES
|
|
84
88
|
|
|
85
89
|
# tmux handles
|
|
@@ -105,6 +109,7 @@ class TmuxPanePool:
|
|
|
105
109
|
def __post_init__(self) -> None:
|
|
106
110
|
if self.max_panes < 1:
|
|
107
111
|
raise ValueError(f"max_panes must be >= 1, but got {self.max_panes}.")
|
|
112
|
+
self.env = normalize_terminal_env(self.env)
|
|
108
113
|
self._semaphore = threading.Semaphore(self.max_panes)
|
|
109
114
|
|
|
110
115
|
def initialize(self) -> None:
|
|
@@ -112,7 +117,7 @@ class TmuxPanePool:
|
|
|
112
117
|
if self._initialized:
|
|
113
118
|
return
|
|
114
119
|
|
|
115
|
-
env =
|
|
120
|
+
env = build_terminal_env(self.env)
|
|
116
121
|
self._server = libtmux.Server(socket_name=TMUX_SOCKET_NAME, environment=env)
|
|
117
122
|
session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
|
|
118
123
|
self._session = self._server.new_session(
|
|
@@ -182,7 +187,11 @@ class TmuxPanePool:
|
|
|
182
187
|
|
|
183
188
|
# Use PooledTmuxTerminal which overrides close() to only kill
|
|
184
189
|
# this terminal's window instead of the entire shared tmux session.
|
|
185
|
-
terminal = PooledTmuxTerminal(
|
|
190
|
+
terminal = PooledTmuxTerminal(
|
|
191
|
+
work_dir=self.work_dir,
|
|
192
|
+
username=self.username,
|
|
193
|
+
env=self.env,
|
|
194
|
+
)
|
|
186
195
|
terminal.server = self._server # type: ignore[assignment]
|
|
187
196
|
terminal.session = self._session
|
|
188
197
|
terminal.window = window
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/tmux_terminal.py
RENAMED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
|
|
6
7
|
import libtmux
|
|
7
8
|
|
|
8
9
|
from openhands.sdk.logger import get_logger
|
|
9
|
-
from openhands.sdk.utils import sanitized_env
|
|
10
10
|
from openhands.tools.terminal.constants import (
|
|
11
11
|
HISTORY_LIMIT,
|
|
12
12
|
TMUX_SESSION_HEIGHT,
|
|
13
13
|
TMUX_SESSION_WIDTH,
|
|
14
14
|
TMUX_SOCKET_NAME,
|
|
15
15
|
)
|
|
16
|
+
from openhands.tools.terminal.env import (
|
|
17
|
+
build_terminal_env,
|
|
18
|
+
normalize_terminal_env,
|
|
19
|
+
)
|
|
16
20
|
from openhands.tools.terminal.metadata import CmdOutputMetadata
|
|
17
21
|
from openhands.tools.terminal.terminal import TerminalInterface
|
|
18
22
|
from openhands.tools.terminal.terminal.interface import parse_ctrl_key
|
|
@@ -57,16 +61,18 @@ class TmuxTerminal(TerminalInterface):
|
|
|
57
61
|
self,
|
|
58
62
|
work_dir: str,
|
|
59
63
|
username: str | None = None,
|
|
64
|
+
env: Mapping[str, str] | None = None,
|
|
60
65
|
):
|
|
61
66
|
super().__init__(work_dir, username)
|
|
62
67
|
self.PS1 = CmdOutputMetadata.to_ps1_prompt()
|
|
68
|
+
self._env = normalize_terminal_env(env)
|
|
63
69
|
|
|
64
70
|
def initialize(self) -> None:
|
|
65
71
|
"""Initialize the tmux terminal session."""
|
|
66
72
|
if self._initialized:
|
|
67
73
|
return
|
|
68
74
|
|
|
69
|
-
env =
|
|
75
|
+
env = build_terminal_env(self._env)
|
|
70
76
|
# Disable interactive pagers (git, man, systemctl, ...) so commands that
|
|
71
77
|
# auto-launch `less` on a TTY don't capture the pane and wedge the session.
|
|
72
78
|
env.setdefault("GIT_PAGER", "cat")
|
|
@@ -10,14 +10,18 @@ import subprocess
|
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
12
|
from collections import deque
|
|
13
|
+
from collections.abc import Mapping
|
|
13
14
|
|
|
14
15
|
from openhands.sdk.logger import get_logger
|
|
15
|
-
from openhands.sdk.utils import sanitized_env
|
|
16
16
|
from openhands.tools.terminal.constants import (
|
|
17
17
|
CMD_OUTPUT_PS1_BEGIN,
|
|
18
18
|
CMD_OUTPUT_PS1_END,
|
|
19
19
|
HISTORY_LIMIT,
|
|
20
20
|
)
|
|
21
|
+
from openhands.tools.terminal.env import (
|
|
22
|
+
build_terminal_env,
|
|
23
|
+
normalize_terminal_env,
|
|
24
|
+
)
|
|
21
25
|
from openhands.tools.terminal.terminal.interface import (
|
|
22
26
|
TerminalInterface,
|
|
23
27
|
parse_ctrl_key,
|
|
@@ -70,6 +74,7 @@ class WindowsTerminal(TerminalInterface):
|
|
|
70
74
|
work_dir: str,
|
|
71
75
|
username: str | None = None,
|
|
72
76
|
shell_path: str = "powershell.exe",
|
|
77
|
+
env: Mapping[str, str] | None = None,
|
|
73
78
|
):
|
|
74
79
|
super().__init__(work_dir, username)
|
|
75
80
|
self.process = None
|
|
@@ -77,6 +82,7 @@ class WindowsTerminal(TerminalInterface):
|
|
|
77
82
|
self.output_lock = threading.Lock()
|
|
78
83
|
self.reader_thread = None
|
|
79
84
|
self.shell_path = shell_path
|
|
85
|
+
self._env = normalize_terminal_env(env)
|
|
80
86
|
self._command_running_event = threading.Event()
|
|
81
87
|
self._stop_reader = threading.Event()
|
|
82
88
|
self._decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
@@ -96,7 +102,7 @@ class WindowsTerminal(TerminalInterface):
|
|
|
96
102
|
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
97
103
|
creationflags |= getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
98
104
|
|
|
99
|
-
env =
|
|
105
|
+
env = build_terminal_env(self._env)
|
|
100
106
|
env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
101
107
|
env.setdefault("PYTHONUTF8", "1")
|
|
102
108
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary: OpenHands Tools - Runtime tools for AI agents
|
|
5
5
|
Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
|
|
6
6
|
Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
|
|
@@ -15,7 +15,6 @@ pyproject.toml
|
|
|
15
15
|
./openhands/tools/delegate/definition.py
|
|
16
16
|
./openhands/tools/delegate/impl.py
|
|
17
17
|
./openhands/tools/delegate/visualizer.py
|
|
18
|
-
./openhands/tools/delegate/templates/delegate_tool_description.j2
|
|
19
18
|
./openhands/tools/file_editor/__init__.py
|
|
20
19
|
./openhands/tools/file_editor/definition.py
|
|
21
20
|
./openhands/tools/file_editor/editor.py
|
|
@@ -70,6 +69,7 @@ pyproject.toml
|
|
|
70
69
|
./openhands/tools/terminal/constants.py
|
|
71
70
|
./openhands/tools/terminal/definition.py
|
|
72
71
|
./openhands/tools/terminal/descriptions.py
|
|
72
|
+
./openhands/tools/terminal/env.py
|
|
73
73
|
./openhands/tools/terminal/impl.py
|
|
74
74
|
./openhands/tools/terminal/metadata.py
|
|
75
75
|
./openhands/tools/terminal/timeout_policy.py
|
|
@@ -108,7 +108,6 @@ openhands/tools/delegate/__init__.py
|
|
|
108
108
|
openhands/tools/delegate/definition.py
|
|
109
109
|
openhands/tools/delegate/impl.py
|
|
110
110
|
openhands/tools/delegate/visualizer.py
|
|
111
|
-
openhands/tools/delegate/templates/delegate_tool_description.j2
|
|
112
111
|
openhands/tools/file_editor/__init__.py
|
|
113
112
|
openhands/tools/file_editor/definition.py
|
|
114
113
|
openhands/tools/file_editor/editor.py
|
|
@@ -163,6 +162,7 @@ openhands/tools/terminal/__init__.py
|
|
|
163
162
|
openhands/tools/terminal/constants.py
|
|
164
163
|
openhands/tools/terminal/definition.py
|
|
165
164
|
openhands/tools/terminal/descriptions.py
|
|
165
|
+
openhands/tools/terminal/env.py
|
|
166
166
|
openhands/tools/terminal/impl.py
|
|
167
167
|
openhands/tools/terminal/metadata.py
|
|
168
168
|
openhands/tools/terminal/timeout_policy.py
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Delegation tool for spawning sub-agents and delegating tasks to them.
|
|
2
|
-
|
|
3
|
-
This tool provides two commands:
|
|
4
|
-
|
|
5
|
-
**spawn**: Initialize sub-agents with meaningful identifiers and optional types
|
|
6
|
-
- Use descriptive identifiers that make sense for your use case (e.g., 'refactoring', 'run_tests', 'research')
|
|
7
|
-
- Optionally specify agent types for specialized capabilities
|
|
8
|
-
- Each identifier creates a separate sub-agent conversation
|
|
9
|
-
- Examples:
|
|
10
|
-
{% raw %} - Default agents: {"command": "spawn", "ids": ["research", "implementation"]}
|
|
11
|
-
- Specialized agents: {"command": "spawn", "ids": ["research", "code"], "agent_types": ["researcher", "programmer"]}
|
|
12
|
-
- Mixed types: {"command": "spawn", "ids": ["research", "generic"], "agent_types": ["researcher"]} # unspecified entries fall back to the default agent{% endraw %}
|
|
13
|
-
|
|
14
|
-
**delegate**: Send tasks to specific sub-agents and wait for results
|
|
15
|
-
- Use a dictionary mapping sub-agent identifiers to task descriptions
|
|
16
|
-
- This is a blocking operation - waits for all sub-agents to complete
|
|
17
|
-
- Returns a single observation containing results from all sub-agents
|
|
18
|
-
- Example: {% raw %}{"command": "delegate", "tasks": {"research": "Find best practices for async code", "implementation": "Refactor the MyClass class"}}{% endraw %}
|
|
19
|
-
|
|
20
|
-
**Available agent types:**
|
|
21
|
-
{{ agent_types_info }}
|
|
22
|
-
|
|
23
|
-
**Important Notes:**
|
|
24
|
-
- Identifiers used in delegate must match those used in spawn
|
|
25
|
-
- All operations are blocking and return comprehensive results
|
|
26
|
-
- Sub-agents work in the same workspace as the main agent: {{ workspace_path }}
|
|
27
|
-
- If you omit an agent type for an ID, a default general-purpose agent is used
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/event_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/browser_use/logging_fix.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/config.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/encoding.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/file_cache.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/history.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/file_editor/utils/shell.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/list_directory/impl.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/read_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/gemini/write_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/planning_file_editor/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/bash_runner.md
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/code_explorer.md
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/default.md
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/preset/subagents/web_researcher.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/task_tracker/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/terminal/interface.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/timeout_policy.py
RENAMED
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands/tools/terminal/utils/escape_filter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.29.2 → openhands_tools-1.30.0}/openhands_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|