openhands-tools 1.28.1__tar.gz → 1.29.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/PKG-INFO +1 -1
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/editor.py +88 -31
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/default.py +3 -9
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task/manager.py +40 -3
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/subprocess_terminal.py +4 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/tmux_terminal.py +4 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/windows_terminal.py +7 -1
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/workflow/definition.py +6 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/workflow/impl.py +87 -10
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/PKG-INFO +1 -1
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/pyproject.toml +1 -1
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/apply_patch/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/apply_patch/core.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/apply_patch/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/event_storage.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/logging_fix.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/recording.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/server.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/delegate/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/delegate/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/delegate/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/delegate/templates/delegate_tool_description.j2 +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/delegate/visualizer.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/exceptions.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/config.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/constants.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/diff.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/encoding.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/file_cache.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/history.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/shell.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/edit/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/edit/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/edit/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/list_directory/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/list_directory/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/list_directory/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/read_file/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/read_file/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/read_file/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/write_file/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/write_file/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/write_file/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/glob/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/glob/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/glob/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/grep/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/grep/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/grep/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/gemini.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/gpt5.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/planning.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/bash_runner.md +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/code_explorer.md +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/default.md +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/web_researcher.md +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/py.typed +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task_tracker/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/task_tracker/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/constants.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/descriptions.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/impl.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/metadata.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/factory.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/interface.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/tmux_pane_pool.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/timeout_policy.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/utils/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/utils/command.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/utils/escape_filter.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/tom_consult/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/tom_consult/definition.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/tom_consult/executor.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/utils/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/utils/timeout.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/workflow/__init__.py +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/SOURCES.txt +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/dependency_links.txt +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/requires.txt +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/top_level.txt +0 -0
- {openhands_tools-1.28.1 → openhands_tools-1.29.2}/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.29.2
|
|
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
|
|
@@ -2,8 +2,9 @@ import base64
|
|
|
2
2
|
import mimetypes
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
-
import shutil
|
|
6
5
|
import tempfile
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import get_args
|
|
9
10
|
|
|
@@ -43,6 +44,15 @@ logger = get_logger(__name__)
|
|
|
43
44
|
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _is_encodable(text: str, encoding: str) -> bool:
|
|
48
|
+
"""Return True if text can be encoded with the given encoding."""
|
|
49
|
+
try:
|
|
50
|
+
text.encode(encoding)
|
|
51
|
+
except UnicodeEncodeError:
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
|
|
46
56
|
class FileEditor:
|
|
47
57
|
"""
|
|
48
58
|
An filesystem editor tool that allows the agent to
|
|
@@ -463,12 +473,61 @@ class FileEditor:
|
|
|
463
473
|
"""
|
|
464
474
|
self.validate_file(path)
|
|
465
475
|
try:
|
|
466
|
-
|
|
467
|
-
with open(path, "w", encoding=encoding) as f:
|
|
468
|
-
f.write(file_text)
|
|
476
|
+
self._atomic_write(path, file_text, encoding)
|
|
469
477
|
except Exception as e:
|
|
470
478
|
raise ToolError(f"Ran into {e} while trying to write to {path}") from None
|
|
471
479
|
|
|
480
|
+
def _atomic_write(self, path: Path, file_text: str, encoding: str) -> None:
|
|
481
|
+
"""Write file_text to path atomically, never leaving a truncated file.
|
|
482
|
+
|
|
483
|
+
The content is written to a temporary file in the same directory which is
|
|
484
|
+
then Path.replace'd into place, so a failed write can never destroy the
|
|
485
|
+
original file. If the file's detected encoding cannot represent the new
|
|
486
|
+
content, fall back to UTF-8 so an edit may add characters (arrows, emoji,
|
|
487
|
+
CJK, ...) the original single-byte encoding lacks, instead of failing and
|
|
488
|
+
truncating the file. Note that the fallback transcodes the whole file to
|
|
489
|
+
UTF-8.
|
|
490
|
+
"""
|
|
491
|
+
default = self._encoding_manager.default_encoding
|
|
492
|
+
if encoding != default and not _is_encodable(file_text, encoding):
|
|
493
|
+
logger.warning(
|
|
494
|
+
f"Detected encoding '{encoding}' cannot represent the new content "
|
|
495
|
+
f"for {path}; writing as '{default}' instead."
|
|
496
|
+
)
|
|
497
|
+
encoding = default
|
|
498
|
+
|
|
499
|
+
with self._temp_file(path, file_text, encoding) as tmp_path:
|
|
500
|
+
# Preserve the original file's permission bits when it already exists.
|
|
501
|
+
if path.exists():
|
|
502
|
+
os.chmod(tmp_path, os.stat(path).st_mode & 0o7777)
|
|
503
|
+
Path.replace(tmp_path, path)
|
|
504
|
+
|
|
505
|
+
@contextmanager
|
|
506
|
+
def _temp_file(self, path: Path, file_text: str, encoding: str) -> Iterator[Path]:
|
|
507
|
+
"""Write file_text to a fresh temp file beside path and yield its Path.
|
|
508
|
+
|
|
509
|
+
The temp file is removed on any failure (write, chmod or replace), so the
|
|
510
|
+
original file is never destroyed and no stray temp file is left behind. The
|
|
511
|
+
unlink runs after the file is closed because Windows cannot delete an open
|
|
512
|
+
file.
|
|
513
|
+
"""
|
|
514
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
515
|
+
mode="w",
|
|
516
|
+
dir=path.parent,
|
|
517
|
+
prefix=f".{path.name}.",
|
|
518
|
+
suffix=".tmp",
|
|
519
|
+
encoding=encoding,
|
|
520
|
+
delete=False,
|
|
521
|
+
)
|
|
522
|
+
tmp_path = Path(tmp.name)
|
|
523
|
+
try:
|
|
524
|
+
with tmp:
|
|
525
|
+
tmp.write(file_text)
|
|
526
|
+
yield tmp_path
|
|
527
|
+
except BaseException:
|
|
528
|
+
tmp_path.unlink(missing_ok=True)
|
|
529
|
+
raise
|
|
530
|
+
|
|
472
531
|
@with_encoding
|
|
473
532
|
def insert(
|
|
474
533
|
self,
|
|
@@ -501,33 +560,31 @@ class FileEditor:
|
|
|
501
560
|
|
|
502
561
|
new_str_lines = new_str.split("\n")
|
|
503
562
|
|
|
504
|
-
#
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
# Move temporary file to original location
|
|
530
|
-
shutil.move(temp_file.name, path)
|
|
563
|
+
# Build the new content in memory, then write it atomically. Routing the
|
|
564
|
+
# write through write_file reuses the same atomic, encoding-safe path as
|
|
565
|
+
# every other edit (no truncation, UTF-8 fallback for new characters).
|
|
566
|
+
new_lines: list[str] = []
|
|
567
|
+
history_lines: list[str] = []
|
|
568
|
+
with open(path, encoding=encoding) as f:
|
|
569
|
+
for i, line in enumerate(f, 1):
|
|
570
|
+
if i > insert_line:
|
|
571
|
+
break
|
|
572
|
+
new_lines.append(line)
|
|
573
|
+
history_lines.append(line)
|
|
574
|
+
|
|
575
|
+
# Insert new content
|
|
576
|
+
for line in new_str_lines:
|
|
577
|
+
new_lines.append(line + "\n")
|
|
578
|
+
|
|
579
|
+
# Copy remaining lines and save them for history
|
|
580
|
+
with open(path, encoding=encoding) as f:
|
|
581
|
+
for i, line in enumerate(f, 1):
|
|
582
|
+
if i <= insert_line:
|
|
583
|
+
continue
|
|
584
|
+
new_lines.append(line)
|
|
585
|
+
history_lines.append(line)
|
|
586
|
+
|
|
587
|
+
self.write_file(path, "".join(new_lines))
|
|
531
588
|
|
|
532
589
|
# Read just the snippet range
|
|
533
590
|
start_line = max(0, insert_line - SNIPPET_CONTEXT_WINDOW)
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from openhands.sdk import Agent, agent_definition_to_factory, load_agents_from_dir
|
|
6
|
-
from openhands.sdk.context.condenser import
|
|
7
|
-
LLMSummarizingCondenser,
|
|
8
|
-
)
|
|
6
|
+
from openhands.sdk.context.condenser import default_condenser
|
|
9
7
|
from openhands.sdk.context.condenser.base import CondenserBase
|
|
10
8
|
from openhands.sdk.llm.llm import LLM
|
|
11
9
|
from openhands.sdk.logger import get_logger
|
|
@@ -68,12 +66,8 @@ def get_default_tools(
|
|
|
68
66
|
|
|
69
67
|
|
|
70
68
|
def get_default_condenser(llm: LLM) -> CondenserBase:
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
# events with an LLM-generated summary.
|
|
74
|
-
condenser = LLMSummarizingCondenser(llm=llm, max_size=80, keep_first=4)
|
|
75
|
-
|
|
76
|
-
return condenser
|
|
69
|
+
# Shared with spawned sub-agents (see sdk default_condenser) so both stay in sync.
|
|
70
|
+
return default_condenser(llm)
|
|
77
71
|
|
|
78
72
|
|
|
79
73
|
def get_default_agent(
|
|
@@ -28,6 +28,7 @@ from openhands.sdk.conversation.state import (
|
|
|
28
28
|
ConversationExecutionStatus,
|
|
29
29
|
ConversationState,
|
|
30
30
|
)
|
|
31
|
+
from openhands.sdk.event.conversation_error import ConversationErrorEvent
|
|
31
32
|
from openhands.sdk.hooks.config import HookConfig
|
|
32
33
|
from openhands.sdk.logger import get_logger
|
|
33
34
|
from openhands.sdk.security import ConfirmationPolicyBase
|
|
@@ -251,6 +252,11 @@ class TaskManager:
|
|
|
251
252
|
if factory.definition.max_iteration_per_run
|
|
252
253
|
else self.parent_conversation.max_iteration_per_run
|
|
253
254
|
)
|
|
255
|
+
# Sub-agent budget: definition value, else inherit the parent's.
|
|
256
|
+
effective_max_budget = (
|
|
257
|
+
factory.definition.max_budget_per_run
|
|
258
|
+
or self.parent_conversation.max_budget_per_run
|
|
259
|
+
)
|
|
254
260
|
|
|
255
261
|
with self._tasks_lock:
|
|
256
262
|
task_id, conversation_id = self._generate_ids()
|
|
@@ -258,6 +264,7 @@ class TaskManager:
|
|
|
258
264
|
sub_conversation = self._get_conversation(
|
|
259
265
|
description=description,
|
|
260
266
|
max_iteration_per_run=effective_max_iter,
|
|
267
|
+
max_budget_per_run=effective_max_budget,
|
|
261
268
|
task_id=task_id,
|
|
262
269
|
worker_agent=worker_agent,
|
|
263
270
|
conversation_id=conversation_id,
|
|
@@ -285,6 +292,7 @@ class TaskManager:
|
|
|
285
292
|
conversation_id: uuid.UUID,
|
|
286
293
|
worker_agent: Agent,
|
|
287
294
|
hook_config: HookConfig | None = None,
|
|
295
|
+
max_budget_per_run: float | None = None,
|
|
288
296
|
) -> LocalConversation:
|
|
289
297
|
parent = self.parent_conversation
|
|
290
298
|
parent_visualizer = parent._visualizer
|
|
@@ -301,6 +309,7 @@ class TaskManager:
|
|
|
301
309
|
persistence_dir=self._persistence_dir,
|
|
302
310
|
conversation_id=conversation_id,
|
|
303
311
|
max_iteration_per_run=max_iteration_per_run,
|
|
312
|
+
max_budget_per_run=max_budget_per_run,
|
|
304
313
|
hook_config=hook_config,
|
|
305
314
|
delete_on_close=True,
|
|
306
315
|
)
|
|
@@ -346,9 +355,17 @@ class TaskManager:
|
|
|
346
355
|
try:
|
|
347
356
|
task.conversation.send_message(prompt, sender=parent_name)
|
|
348
357
|
self._run_until_finished(task.id, task.conversation)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
status = task.conversation.state.execution_status
|
|
359
|
+
if status == ConversationExecutionStatus.FINISHED:
|
|
360
|
+
result = get_agent_final_response(task.conversation.state.events)
|
|
361
|
+
task.set_result(result)
|
|
362
|
+
logger.info(f"Task '{task.id}' completed.")
|
|
363
|
+
else:
|
|
364
|
+
# Any non-FINISHED terminal status (run-limit, stuck, paused, ...)
|
|
365
|
+
# is surfaced as an error, not an empty "completed"; the detail
|
|
366
|
+
# keeps partial output so the parent can use/retry it.
|
|
367
|
+
task.set_error(self._run_stop_detail(task.conversation, status))
|
|
368
|
+
logger.warning(f"Task '{task.id}' stopped: status '{status.value}'.")
|
|
352
369
|
except Exception as e:
|
|
353
370
|
task.set_error(str(e))
|
|
354
371
|
logger.warning(f"Task {task.id} failed with error: {e}")
|
|
@@ -358,6 +375,26 @@ class TaskManager:
|
|
|
358
375
|
|
|
359
376
|
return task
|
|
360
377
|
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _run_stop_detail(
|
|
380
|
+
conversation: LocalConversation,
|
|
381
|
+
status: ConversationExecutionStatus,
|
|
382
|
+
) -> str:
|
|
383
|
+
"""Why a sub-agent stopped without finishing (run-limit, stuck, paused, ...),
|
|
384
|
+
plus any partial output so the parent isn't left with nothing to use."""
|
|
385
|
+
errors = [
|
|
386
|
+
e
|
|
387
|
+
for e in conversation.state.events
|
|
388
|
+
if isinstance(e, ConversationErrorEvent)
|
|
389
|
+
]
|
|
390
|
+
reason = (
|
|
391
|
+
errors[-1].detail
|
|
392
|
+
if errors
|
|
393
|
+
else f"Sub-agent stopped without finishing (status: {status.value})."
|
|
394
|
+
)
|
|
395
|
+
partial = get_agent_final_response(conversation.state.events)
|
|
396
|
+
return f"{reason}\nPartial result:\n{partial}" if partial else reason
|
|
397
|
+
|
|
361
398
|
def _run_until_finished(
|
|
362
399
|
self, task_id: str, conversation: LocalConversation
|
|
363
400
|
) -> None:
|
|
@@ -137,6 +137,10 @@ class SubprocessTerminal(TerminalInterface):
|
|
|
137
137
|
|
|
138
138
|
# Inherit environment variables from the parent process
|
|
139
139
|
env = sanitized_env()
|
|
140
|
+
# Disable interactive pagers (git, man, systemctl, ...) so commands that
|
|
141
|
+
# auto-launch `less` on a TTY don't capture the PTY and wedge the session.
|
|
142
|
+
env.setdefault("GIT_PAGER", "cat")
|
|
143
|
+
env.setdefault("PAGER", "cat")
|
|
140
144
|
env["PS1"] = self.PS1
|
|
141
145
|
env["PS2"] = ""
|
|
142
146
|
env["TERM"] = "xterm-256color"
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/tmux_terminal.py
RENAMED
|
@@ -67,6 +67,10 @@ class TmuxTerminal(TerminalInterface):
|
|
|
67
67
|
return
|
|
68
68
|
|
|
69
69
|
env = sanitized_env()
|
|
70
|
+
# Disable interactive pagers (git, man, systemctl, ...) so commands that
|
|
71
|
+
# auto-launch `less` on a TTY don't capture the pane and wedge the session.
|
|
72
|
+
env.setdefault("GIT_PAGER", "cat")
|
|
73
|
+
env.setdefault("PAGER", "cat")
|
|
70
74
|
# Use a dedicated socket to isolate OpenHands sessions from the user's tmux
|
|
71
75
|
self.server = libtmux.Server(socket_name=TMUX_SOCKET_NAME, environment=env)
|
|
72
76
|
_shell_command = "/bin/bash"
|
|
@@ -390,6 +390,12 @@ if ($toStop.Count -gt 0) {{ exit 0 }} else {{ exit 1 }}
|
|
|
390
390
|
if self.process is None or self.process.poll() is not None:
|
|
391
391
|
return False
|
|
392
392
|
|
|
393
|
+
# Kill descendants while they are still attached to the persistent
|
|
394
|
+
# PowerShell process. CTRL_BREAK can interrupt the waiting script first,
|
|
395
|
+
# leaving launched child processes alive but no longer discoverable as
|
|
396
|
+
# descendants of the shell.
|
|
397
|
+
terminated_children = self._terminate_child_processes()
|
|
398
|
+
|
|
393
399
|
sent_ctrl_break = False
|
|
394
400
|
ctrl_break_event = getattr(signal, "CTRL_BREAK_EVENT", None)
|
|
395
401
|
if platform.system() == "Windows" and ctrl_break_event is not None:
|
|
@@ -402,7 +408,7 @@ if ($toStop.Count -gt 0) {{ exit 0 }} else {{ exit 1 }}
|
|
|
402
408
|
if sent_ctrl_break:
|
|
403
409
|
time.sleep(_INTERRUPT_GRACE_SECONDS)
|
|
404
410
|
|
|
405
|
-
terminated_children = self._terminate_child_processes()
|
|
411
|
+
terminated_children = self._terminate_child_processes() or terminated_children
|
|
406
412
|
sent_ctrl_c_input = False
|
|
407
413
|
if not sent_ctrl_break and not terminated_children:
|
|
408
414
|
try:
|
|
@@ -79,6 +79,12 @@ Available `wf` methods:
|
|
|
79
79
|
max_concurrency=None, description=None)`
|
|
80
80
|
- `await wf.reduce_agent(items, prompt, subagent_type="general-purpose",
|
|
81
81
|
description=None)`
|
|
82
|
+
- `await wf.pipeline(items, *stages)` — run each item through all stages with no
|
|
83
|
+
barrier between stages (a fast item reaches a later stage while a slow item is
|
|
84
|
+
still in an earlier one). The first stage gets the item; each later stage gets
|
|
85
|
+
the previous result. Stages may be sync or async. A stage that raises drops that
|
|
86
|
+
item to `None`. Prefer this over chained `map_agents` calls when per-item stages
|
|
87
|
+
are independent, since `map_agents` fully drains each stage before the next.
|
|
82
88
|
- `wf.flatten(values)` — flatten one level of nesting (not recursive)
|
|
83
89
|
|
|
84
90
|
`subagent_type` must be a sub-agent type registered in the parent application.
|
|
@@ -23,6 +23,7 @@ logger = get_logger(__name__)
|
|
|
23
23
|
|
|
24
24
|
_MAX_SCRIPT_CHARS = 20_000
|
|
25
25
|
_MAX_REDUCE_INPUT_CHARS = 12_000
|
|
26
|
+
_TRUNCATION_MARKER = "\n... [truncated workflow intermediate results]"
|
|
26
27
|
_WORKFLOW_TIMEOUT_SECONDS = 3600.0 # 1 hour; prevents indefinitely hung workflows
|
|
27
28
|
_UNSAFE_CALLS = frozenset(
|
|
28
29
|
{
|
|
@@ -187,6 +188,40 @@ class WorkflowContext:
|
|
|
187
188
|
)
|
|
188
189
|
return [str(result) for result in results]
|
|
189
190
|
|
|
191
|
+
async def pipeline(
|
|
192
|
+
self,
|
|
193
|
+
items: Sequence[Any],
|
|
194
|
+
*stages: Callable[[Any], Any],
|
|
195
|
+
) -> list[Any]:
|
|
196
|
+
"""Run each item through all stages independently, with no barrier between
|
|
197
|
+
stages: a fast item can reach a later stage while a slow item is still in an
|
|
198
|
+
earlier one. The first stage receives the original item; each later stage
|
|
199
|
+
receives the previous stage's result. Stages may be sync or async. A stage
|
|
200
|
+
that raises drops that item to ``None`` and skips its remaining stages; other
|
|
201
|
+
items are unaffected.
|
|
202
|
+
|
|
203
|
+
Concurrency is bounded by the agent calls inside stages (which acquire the
|
|
204
|
+
shared semaphore), so pipeline adds no barrier of its own. Do not have a
|
|
205
|
+
stage re-acquire the semaphore directly; call ``run_agent``/``map_agents``.
|
|
206
|
+
"""
|
|
207
|
+
if not stages:
|
|
208
|
+
raise ValueError("pipeline requires at least one stage")
|
|
209
|
+
|
|
210
|
+
async def run_chain(index: int, item: Any) -> Any:
|
|
211
|
+
value: Any = item
|
|
212
|
+
try:
|
|
213
|
+
for stage in stages:
|
|
214
|
+
result = stage(value)
|
|
215
|
+
value = await result if inspect.isawaitable(result) else result
|
|
216
|
+
return value
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
logger.warning("pipeline: item %d failed: %s", index + 1, exc)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
return list(
|
|
222
|
+
await asyncio.gather(*(run_chain(i, item) for i, item in enumerate(items)))
|
|
223
|
+
)
|
|
224
|
+
|
|
190
225
|
async def reduce_agent(
|
|
191
226
|
self,
|
|
192
227
|
items: Any,
|
|
@@ -248,18 +283,60 @@ def _render_template(
|
|
|
248
283
|
|
|
249
284
|
|
|
250
285
|
def _format_value(value: Any) -> str:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
286
|
+
"""Serialize reduce input for the reducer prompt, bounded by
|
|
287
|
+
``_MAX_REDUCE_INPUT_CHARS``. For lists/dicts, drop whole trailing elements
|
|
288
|
+
(keeping valid JSON) and report how many were omitted, instead of slicing
|
|
289
|
+
mid-token; everything else falls back to character truncation."""
|
|
290
|
+
if isinstance(value, (list, dict)):
|
|
291
|
+
return _truncate_structured(value)
|
|
292
|
+
text = value if isinstance(value, str) else jsonlib.dumps(value, default=str)
|
|
293
|
+
return _truncate_text(text)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _truncate_text(text: str) -> str:
|
|
255
297
|
if len(text) <= _MAX_REDUCE_INPUT_CHARS:
|
|
256
298
|
return text
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
299
|
+
return text[:_MAX_REDUCE_INPUT_CHARS] + _TRUNCATION_MARKER
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _truncate_structured(value: list | dict) -> str:
|
|
303
|
+
"""Keep as many leading elements as fit, dropping whole elements so the JSON
|
|
304
|
+
stays valid. Always keeps at least one element; a single oversized element is
|
|
305
|
+
char-truncated as a last resort."""
|
|
306
|
+
full = jsonlib.dumps(value, indent=2, default=str)
|
|
307
|
+
if len(full) <= _MAX_REDUCE_INPUT_CHARS:
|
|
308
|
+
return full
|
|
309
|
+
|
|
310
|
+
is_dict = isinstance(value, dict)
|
|
311
|
+
items = list(value.items()) if is_dict else list(value)
|
|
312
|
+
total = len(items)
|
|
313
|
+
|
|
314
|
+
def render(kept: list) -> str:
|
|
315
|
+
body = jsonlib.dumps(dict(kept) if is_dict else kept, indent=2, default=str)
|
|
316
|
+
dropped = total - len(kept)
|
|
317
|
+
if not dropped:
|
|
318
|
+
return body
|
|
319
|
+
return (
|
|
320
|
+
f"{body}\n... [{dropped} of {total} items omitted to fit the "
|
|
321
|
+
"reduce input limit]"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Greedy fill by per-element estimate, then correct against the actual
|
|
325
|
+
# combined serialization (which is larger than the sum of parts) so the
|
|
326
|
+
# result fits without slicing mid-token.
|
|
327
|
+
kept: list = []
|
|
328
|
+
used = 0
|
|
329
|
+
for item in items:
|
|
330
|
+
chunk = jsonlib.dumps(dict([item]) if is_dict else item, default=str)
|
|
331
|
+
if kept and used + len(chunk) > _MAX_REDUCE_INPUT_CHARS:
|
|
332
|
+
break
|
|
333
|
+
kept.append(item)
|
|
334
|
+
used += len(chunk)
|
|
335
|
+
while len(kept) > 1 and len(render(kept)) > _MAX_REDUCE_INPUT_CHARS:
|
|
336
|
+
kept.pop()
|
|
337
|
+
|
|
338
|
+
# Safety net: a single oversized element can still exceed the budget.
|
|
339
|
+
return _truncate_text(render(kept))
|
|
263
340
|
|
|
264
341
|
|
|
265
342
|
def validate_workflow_script(script: str) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.29.2
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/browser_use/event_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/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
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/config.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/encoding.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/file_cache.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/history.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/file_editor/utils/shell.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/list_directory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/list_directory/impl.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/read_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/read_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/gemini/write_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/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.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/planning_file_editor/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/bash_runner.md
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/code_explorer.md
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/preset/subagents/default.md
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/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.28.1 → openhands_tools-1.29.2}/openhands/tools/task_tracker/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/factory.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/terminal/interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/timeout_policy.py
RENAMED
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands/tools/terminal/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/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
|
{openhands_tools-1.28.1 → openhands_tools-1.29.2}/openhands_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|