openhands-tools 1.54.0__tar.gz → 1.55.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.54.0 → openhands_tools-1.55.0}/PKG-INFO +2 -1
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/impl.py +6 -1
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/definition.py +4 -4
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/manager.py +10 -2
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/definition.py +21 -2
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/constants.py +15 -1
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/terminal_session.py +4 -2
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +7 -8
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_terminal.py +75 -7
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/PKG-INFO +2 -1
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/requires.txt +1 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/pyproject.toml +2 -1
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/core.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/event_storage.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/logging_fix.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/recording.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/server.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/delegate/visualizer.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/editor.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/exceptions.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/config.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/constants.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/diff.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/history.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/shell.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/file_change.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/glob/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/grep/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/default.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/gemini.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/gpt5.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/planning.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/default.md +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/task_outcome.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/py.typed +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/descriptions.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/env.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/metadata.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/factory.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/interface.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/timeout_policy.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/command.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/executor.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/utils/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/utils/timeout.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/__init__.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/definition.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/workflow/impl.py +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/top_level.txt +0 -0
- {openhands_tools-1.54.0 → openhands_tools-1.55.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.55.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
|
|
@@ -12,6 +12,7 @@ Requires-Dist: tree-sitter>=0.24
|
|
|
12
12
|
Requires-Dist: tree-sitter-bash>=0.23
|
|
13
13
|
Requires-Dist: binaryornot>=0.6.0
|
|
14
14
|
Requires-Dist: cachetools
|
|
15
|
+
Requires-Dist: filelock>=3.20.1
|
|
15
16
|
Requires-Dist: libtmux>=0.53.0
|
|
16
17
|
Requires-Dist: pydantic>=2.13.5
|
|
17
18
|
Requires-Dist: browser-use>=0.8.0
|
|
@@ -13,6 +13,7 @@ from openhands.sdk.conversation.state import (
|
|
|
13
13
|
)
|
|
14
14
|
from openhands.sdk.logger import get_logger
|
|
15
15
|
from openhands.sdk.subagent import get_agent_factory
|
|
16
|
+
from openhands.sdk.subagent.scope import inherit_mcp_config
|
|
16
17
|
from openhands.sdk.tool.tool import ToolExecutor
|
|
17
18
|
from openhands.tools.delegate.definition import DelegateObservation
|
|
18
19
|
|
|
@@ -188,7 +189,9 @@ class DelegateExecutor(ToolExecutor):
|
|
|
188
189
|
worker_agent = factory.factory_func(sub_agent_llm)
|
|
189
190
|
|
|
190
191
|
# ensuring that the sub-agent LLM has stream deactivated
|
|
191
|
-
worker_agent =
|
|
192
|
+
worker_agent = inherit_mcp_config(
|
|
193
|
+
parent_conversation.agent, worker_agent
|
|
194
|
+
).model_copy(
|
|
192
195
|
update={
|
|
193
196
|
"llm": worker_agent.llm.model_copy(update={"stream": False})
|
|
194
197
|
}
|
|
@@ -223,6 +226,8 @@ class DelegateExecutor(ToolExecutor):
|
|
|
223
226
|
"_parent_llm_call_context": (
|
|
224
227
|
parent_conversation.get_llm_call_context()
|
|
225
228
|
),
|
|
229
|
+
"mcp_tool_provider": parent_conversation._mcp_tool_provider,
|
|
230
|
+
"_mcp_config_from_parent": True,
|
|
226
231
|
}
|
|
227
232
|
|
|
228
233
|
if factory.definition.max_iteration_per_run is not None:
|
|
@@ -244,8 +244,9 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
|
|
|
244
244
|
confirmation policy requires user approval. Receives
|
|
245
245
|
`(task_id, pending_actions)` and must return `True` to
|
|
246
246
|
approve or `False` to reject.
|
|
247
|
-
sub_agent_scope:
|
|
248
|
-
|
|
247
|
+
sub_agent_scope: Whether the sub-agents it offers and starts must
|
|
248
|
+
stay within this agent's tools. Sub-agents always inherit this
|
|
249
|
+
agent's MCP servers.
|
|
249
250
|
|
|
250
251
|
Returns:
|
|
251
252
|
List containing a single TaskTool.
|
|
@@ -264,8 +265,7 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
|
|
|
264
265
|
agent_types_info = get_factory_info(lambda d: d.name in registered)
|
|
265
266
|
else:
|
|
266
267
|
agent_types_info = (
|
|
267
|
-
"- None: every registered agent uses tools
|
|
268
|
-
"this agent does not have."
|
|
268
|
+
"- None: every registered agent uses tools this agent does not have."
|
|
269
269
|
)
|
|
270
270
|
|
|
271
271
|
task_tool_examples = "\n".join(
|
|
@@ -35,7 +35,11 @@ from openhands.sdk.logger import get_logger
|
|
|
35
35
|
from openhands.sdk.observability.laminar import detached_delegate_context
|
|
36
36
|
from openhands.sdk.security import ConfirmationPolicyBase
|
|
37
37
|
from openhands.sdk.subagent.registry import AgentFactory, get_agent_factory
|
|
38
|
-
from openhands.sdk.subagent.scope import
|
|
38
|
+
from openhands.sdk.subagent.scope import (
|
|
39
|
+
SubAgentScope,
|
|
40
|
+
inherit_mcp_config,
|
|
41
|
+
scope_delegation_tools,
|
|
42
|
+
)
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
if TYPE_CHECKING:
|
|
@@ -225,6 +229,8 @@ class TaskManager:
|
|
|
225
229
|
_parent_llm_call_context=(
|
|
226
230
|
self.parent_conversation.get_llm_call_context()
|
|
227
231
|
),
|
|
232
|
+
mcp_tool_provider=self.parent_conversation._mcp_tool_provider,
|
|
233
|
+
_mcp_config_from_parent=True,
|
|
228
234
|
delete_on_close=True,
|
|
229
235
|
observability_metadata=self._delegate_observability_metadata(
|
|
230
236
|
task_id=resume, subagent_type=subagent_type, link=link
|
|
@@ -328,6 +334,8 @@ class TaskManager:
|
|
|
328
334
|
max_budget_per_run=max_budget_per_run,
|
|
329
335
|
hook_config=hook_config,
|
|
330
336
|
_parent_llm_call_context=parent.get_llm_call_context(),
|
|
337
|
+
mcp_tool_provider=parent._mcp_tool_provider,
|
|
338
|
+
_mcp_config_from_parent=True,
|
|
331
339
|
delete_on_close=True,
|
|
332
340
|
prompt_cache_key=str(parent.state.id),
|
|
333
341
|
observability_metadata=self._delegate_observability_metadata(
|
|
@@ -391,7 +399,7 @@ class TaskManager:
|
|
|
391
399
|
)
|
|
392
400
|
|
|
393
401
|
# ensuring that the sub-agent LLM has stream deactivated
|
|
394
|
-
sub_agent = sub_agent.model_copy(
|
|
402
|
+
sub_agent = inherit_mcp_config(parent.agent, sub_agent).model_copy(
|
|
395
403
|
update={
|
|
396
404
|
"llm": sub_agent.llm.model_copy(update={"stream": False}),
|
|
397
405
|
"tools": scope_delegation_tools(sub_agent.tools, self._sub_agent_scope),
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/definition.py
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections.abc import Sequence
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Literal, get_args
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, Field, ValidationError
|
|
6
|
+
from pydantic import BaseModel, Field, ValidationError, field_validator
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
@@ -28,6 +28,14 @@ logger = get_logger(__name__)
|
|
|
28
28
|
# Type alias for task tracker status
|
|
29
29
|
TaskTrackerStatusType = Literal["todo", "in_progress", "done"]
|
|
30
30
|
|
|
31
|
+
# Models trained on other agents' todo tools send these names for our statuses.
|
|
32
|
+
_STATUS_ALIASES: dict[str, TaskTrackerStatusType] = {
|
|
33
|
+
"pending": "todo",
|
|
34
|
+
"to_do": "todo",
|
|
35
|
+
"completed": "done",
|
|
36
|
+
"complete": "done",
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
|
|
32
40
|
class TaskItem(BaseModel):
|
|
33
41
|
title: str = Field(..., description="A brief title for the task.")
|
|
@@ -38,6 +46,16 @@ class TaskItem(BaseModel):
|
|
|
38
46
|
"One of 'todo', 'in_progress', or 'done'.",
|
|
39
47
|
)
|
|
40
48
|
|
|
49
|
+
@field_validator("status", mode="before")
|
|
50
|
+
@classmethod
|
|
51
|
+
def _normalize_status(cls, value: object) -> object:
|
|
52
|
+
if not isinstance(value, str):
|
|
53
|
+
return value
|
|
54
|
+
normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
|
|
55
|
+
if normalized in get_args(TaskTrackerStatusType):
|
|
56
|
+
return normalized
|
|
57
|
+
return _STATUS_ALIASES.get(normalized, value)
|
|
58
|
+
|
|
41
59
|
|
|
42
60
|
class TaskTrackerAction(Action):
|
|
43
61
|
"""An action where the agent writes or updates a task list for task management."""
|
|
@@ -406,6 +424,7 @@ class TaskTrackerTool(ToolDefinition[TaskTrackerAction, TaskTrackerObservation])
|
|
|
406
424
|
catalog_description: ClassVar[str] = (
|
|
407
425
|
"Keep a running task list to organise multi-step work."
|
|
408
426
|
)
|
|
427
|
+
conversation_local: ClassVar[bool] = True
|
|
409
428
|
|
|
410
429
|
@classmethod
|
|
411
430
|
def create(cls, conv_state: "ConversationState") -> Sequence["TaskTrackerTool"]:
|
|
@@ -30,12 +30,26 @@ TIMEOUT_MESSAGE_TEMPLATE: Final[str] = (
|
|
|
30
30
|
# How long to wait with no new output before considering it a no-change timeout
|
|
31
31
|
NO_CHANGE_TIMEOUT_SECONDS: Final[int] = 30
|
|
32
32
|
|
|
33
|
-
# How often to poll for new output in seconds
|
|
33
|
+
# How often to poll for new output in seconds, once a command has run for a while
|
|
34
34
|
POLL_INTERVAL: Final[float] = 0.5
|
|
35
|
+
# Shortest poll interval, used right after a command is sent
|
|
36
|
+
MIN_POLL_INTERVAL: Final[float] = 0.02
|
|
35
37
|
HISTORY_LIMIT: Final[int] = 10_000
|
|
36
38
|
|
|
37
39
|
TMUX_SOCKET_NAME: Final[str] = "openhands"
|
|
38
40
|
|
|
41
|
+
# tmux user option recording the PID of the process that created a socket.
|
|
42
|
+
# Startup cleanup reads it so it does not kill the live sessions of a sibling
|
|
43
|
+
# agent-server that shares one externally supplied TMUX_TMPDIR.
|
|
44
|
+
TMUX_SOCKET_OWNER_OPTION: Final[str] = "@openhands_socket_owner_pid"
|
|
45
|
+
|
|
46
|
+
# Cross-process lock file, inside the tmux socket directory, that serializes
|
|
47
|
+
# socket creation with the agent-server's startup cleanup. A socket only exists
|
|
48
|
+
# between creation and the recording of its owner, so both operations take this
|
|
49
|
+
# lock to keep cleanup from reclaiming a socket that a live process is still
|
|
50
|
+
# setting up.
|
|
51
|
+
TMUX_SOCKET_CREATION_LOCK: Final[str] = ".openhands-socket-creation.lock"
|
|
52
|
+
|
|
39
53
|
# Tmux session dimensions (columns x rows). Keep the viewport wide enough for
|
|
40
54
|
# common command output while leaving scrollback retention to HISTORY_LIMIT.
|
|
41
55
|
# Output wider than TMUX_SESSION_WIDTH columns will wrap; this is an accepted
|
|
@@ -10,6 +10,7 @@ from openhands.sdk.utils import maybe_truncate
|
|
|
10
10
|
from openhands.tools.terminal.constants import (
|
|
11
11
|
CMD_OUTPUT_PS1_END,
|
|
12
12
|
MAX_CMD_OUTPUT_SIZE,
|
|
13
|
+
MIN_POLL_INTERVAL,
|
|
13
14
|
NO_CHANGE_TIMEOUT_SECONDS,
|
|
14
15
|
POLL_INTERVAL,
|
|
15
16
|
TIMEOUT_MESSAGE_TEMPLATE,
|
|
@@ -769,5 +770,6 @@ class TerminalSession(TerminalSessionBase):
|
|
|
769
770
|
)
|
|
770
771
|
return obs
|
|
771
772
|
|
|
772
|
-
#
|
|
773
|
-
|
|
773
|
+
# Poll in proportion to the command's run time so a short command is
|
|
774
|
+
# noticed within milliseconds, not at the next POLL_INTERVAL tick.
|
|
775
|
+
time.sleep(min(POLL_INTERVAL, max(MIN_POLL_INTERVAL, elapsed_time / 4)))
|
|
@@ -22,13 +22,15 @@ from openhands.tools.terminal.constants import (
|
|
|
22
22
|
HISTORY_LIMIT,
|
|
23
23
|
TMUX_SESSION_HEIGHT,
|
|
24
24
|
TMUX_SESSION_WIDTH,
|
|
25
|
-
TMUX_SOCKET_NAME,
|
|
26
25
|
)
|
|
27
26
|
from openhands.tools.terminal.env import (
|
|
28
27
|
build_terminal_env,
|
|
29
28
|
normalize_terminal_env,
|
|
30
29
|
)
|
|
31
|
-
from openhands.tools.terminal.terminal.tmux_terminal import
|
|
30
|
+
from openhands.tools.terminal.terminal.tmux_terminal import (
|
|
31
|
+
TmuxTerminal,
|
|
32
|
+
_new_owned_session,
|
|
33
|
+
)
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
logger = get_logger(__name__)
|
|
@@ -118,15 +120,12 @@ class TmuxPanePool:
|
|
|
118
120
|
return
|
|
119
121
|
|
|
120
122
|
env = build_terminal_env(self.env)
|
|
121
|
-
# Stale handles must not reach another pool after tmux restarts.
|
|
122
|
-
self._server = libtmux.Server(
|
|
123
|
-
socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
|
|
124
|
-
)
|
|
125
123
|
session_name = f"openhands-pool-{self.username}-{uuid.uuid4()}"
|
|
126
|
-
|
|
124
|
+
# Stale handles must not reach another pool after tmux restarts.
|
|
125
|
+
self._server, self._session = _new_owned_session(
|
|
126
|
+
env,
|
|
127
127
|
session_name=session_name,
|
|
128
128
|
start_directory=self.work_dir,
|
|
129
|
-
kill_session=True,
|
|
130
129
|
x=TMUX_SESSION_WIDTH,
|
|
131
130
|
y=TMUX_SESSION_HEIGHT,
|
|
132
131
|
)
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_terminal.py
RENAMED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"""Tmux-based terminal backend implementation."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import time
|
|
5
6
|
import uuid
|
|
6
|
-
from collections.abc import Mapping
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from contextlib import contextmanager, suppress
|
|
9
|
+
from pathlib import Path
|
|
7
10
|
|
|
8
11
|
import libtmux
|
|
12
|
+
from filelock import FileLock
|
|
9
13
|
|
|
10
14
|
from openhands.sdk.logger import get_logger
|
|
11
15
|
from openhands.sdk.utils.redact import redact_api_key_literals
|
|
@@ -13,7 +17,9 @@ from openhands.tools.terminal.constants import (
|
|
|
13
17
|
HISTORY_LIMIT,
|
|
14
18
|
TMUX_SESSION_HEIGHT,
|
|
15
19
|
TMUX_SESSION_WIDTH,
|
|
20
|
+
TMUX_SOCKET_CREATION_LOCK,
|
|
16
21
|
TMUX_SOCKET_NAME,
|
|
22
|
+
TMUX_SOCKET_OWNER_OPTION,
|
|
17
23
|
)
|
|
18
24
|
from openhands.tools.terminal.env import (
|
|
19
25
|
build_terminal_env,
|
|
@@ -68,6 +74,71 @@ def _install_libtmux_redaction_filter() -> None:
|
|
|
68
74
|
libtmux_logger.addFilter(_SecretRedactFilter())
|
|
69
75
|
|
|
70
76
|
|
|
77
|
+
@contextmanager
|
|
78
|
+
def _socket_creation_lock() -> Iterator[None]:
|
|
79
|
+
"""Serialize socket creation with the agent-server's startup cleanup.
|
|
80
|
+
|
|
81
|
+
A tmux socket exists between ``new-session`` and the recording of its owner
|
|
82
|
+
option. Startup cleanup in a sibling agent-server treats an ownerless socket
|
|
83
|
+
as reclaimable, so without this lock it can kill a socket whose live owner
|
|
84
|
+
has not finished initializing. The lock file lives in the tmux socket
|
|
85
|
+
directory, which is the one path every process sharing ``TMUX_TMPDIR``
|
|
86
|
+
agrees on even while tmux has not created any socket yet.
|
|
87
|
+
"""
|
|
88
|
+
socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
|
|
89
|
+
# tmux refuses a socket directory that is group/world accessible, and
|
|
90
|
+
# FileLock would create it with the umask default, so create it 0700 first.
|
|
91
|
+
socket_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
with suppress(OSError):
|
|
93
|
+
socket_dir.chmod(0o700)
|
|
94
|
+
with FileLock(socket_dir / TMUX_SOCKET_CREATION_LOCK):
|
|
95
|
+
yield
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mark_socket_owner(server: libtmux.Server) -> None:
|
|
99
|
+
"""Record this process as the owner of *server*'s socket.
|
|
100
|
+
|
|
101
|
+
The agent-server's startup cleanup reads this to tell a live sibling's
|
|
102
|
+
socket apart from a stale one when a shared ``TMUX_TMPDIR`` is in use.
|
|
103
|
+
Best-effort: a failure here only loses the ownership hint, so it must
|
|
104
|
+
never break terminal setup.
|
|
105
|
+
"""
|
|
106
|
+
with suppress(Exception):
|
|
107
|
+
server.set_option(
|
|
108
|
+
TMUX_SOCKET_OWNER_OPTION, str(os.getpid()), global_=True, scope=None
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _new_owned_session(
|
|
113
|
+
env: Mapping[str, str],
|
|
114
|
+
*,
|
|
115
|
+
session_name: str,
|
|
116
|
+
start_directory: str,
|
|
117
|
+
x: int,
|
|
118
|
+
y: int,
|
|
119
|
+
) -> tuple[libtmux.Server, libtmux.Session]:
|
|
120
|
+
"""Create a tmux session on a fresh socket owned by this process.
|
|
121
|
+
|
|
122
|
+
The socket only starts to exist once the session is created, so the session
|
|
123
|
+
is created and its owner recorded while holding the cross-process creation
|
|
124
|
+
lock. Startup cleanup in another process takes the same lock, so it can
|
|
125
|
+
never observe the socket before its owner is published.
|
|
126
|
+
"""
|
|
127
|
+
with _socket_creation_lock():
|
|
128
|
+
server = libtmux.Server(
|
|
129
|
+
socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=dict(env)
|
|
130
|
+
)
|
|
131
|
+
session = server.new_session(
|
|
132
|
+
session_name=session_name,
|
|
133
|
+
start_directory=start_directory,
|
|
134
|
+
kill_session=True,
|
|
135
|
+
x=x,
|
|
136
|
+
y=y,
|
|
137
|
+
)
|
|
138
|
+
mark_socket_owner(server)
|
|
139
|
+
return server, session
|
|
140
|
+
|
|
141
|
+
|
|
71
142
|
# Map normalized special key names to tmux key names.
|
|
72
143
|
_TMUX_SPECIALS: dict[str, str] = {
|
|
73
144
|
"ENTER": "Enter",
|
|
@@ -122,10 +193,6 @@ class TmuxTerminal(TerminalInterface):
|
|
|
122
193
|
# auto-launch `less` on a TTY don't capture the pane and wedge the session.
|
|
123
194
|
env.setdefault("GIT_PAGER", "cat")
|
|
124
195
|
env.setdefault("PAGER", "cat")
|
|
125
|
-
# Never reuse a socket: tmux numeric IDs restart at zero after server exit.
|
|
126
|
-
self.server = libtmux.Server(
|
|
127
|
-
socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=env
|
|
128
|
-
)
|
|
129
196
|
_shell_command = "/bin/bash"
|
|
130
197
|
if self.username in ["root", "openhands"]:
|
|
131
198
|
# This starts a non-login (new) shell for the given user
|
|
@@ -134,11 +201,12 @@ class TmuxTerminal(TerminalInterface):
|
|
|
134
201
|
window_command = _shell_command
|
|
135
202
|
|
|
136
203
|
logger.debug(f"Initializing tmux terminal with command: {window_command}")
|
|
204
|
+
# Never reuse a socket: tmux numeric IDs restart at zero after server exit.
|
|
137
205
|
session_name = f"openhands-{self.username}-{uuid.uuid4()}"
|
|
138
|
-
self.session =
|
|
206
|
+
self.server, self.session = _new_owned_session(
|
|
207
|
+
env,
|
|
139
208
|
session_name=session_name,
|
|
140
209
|
start_directory=self.work_dir,
|
|
141
|
-
kill_session=True,
|
|
142
210
|
x=TMUX_SESSION_WIDTH,
|
|
143
211
|
y=TMUX_SESSION_HEIGHT,
|
|
144
212
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.55.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
|
|
@@ -12,6 +12,7 @@ Requires-Dist: tree-sitter>=0.24
|
|
|
12
12
|
Requires-Dist: tree-sitter-bash>=0.23
|
|
13
13
|
Requires-Dist: binaryornot>=0.6.0
|
|
14
14
|
Requires-Dist: cachetools
|
|
15
|
+
Requires-Dist: filelock>=3.20.1
|
|
15
16
|
Requires-Dist: libtmux>=0.53.0
|
|
16
17
|
Requires-Dist: pydantic>=2.13.5
|
|
17
18
|
Requires-Dist: browser-use>=0.8.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openhands-tools"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.55.0"
|
|
4
4
|
description = "OpenHands Tools - Runtime tools for AI agents"
|
|
5
5
|
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
"tree-sitter-bash>=0.23",
|
|
11
11
|
"binaryornot>=0.6.0",
|
|
12
12
|
"cachetools",
|
|
13
|
+
"filelock>=3.20.1",
|
|
13
14
|
"libtmux>=0.53.0",
|
|
14
15
|
"pydantic>=2.13.5",
|
|
15
16
|
"browser-use>=0.8.0",
|
|
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.54.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/event_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.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.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/config.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/encoding.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/file_cache.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/history.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/shell.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/impl.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.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.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/bash_runner.md
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/code_explorer.md
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/default.md
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/factory.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/timeout_policy.py
RENAMED
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.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
|
|
File without changes
|
{openhands_tools-1.54.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|