openhands-tools 1.55.0__tar.gz → 1.56.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.55.0 → openhands_tools-1.56.0}/PKG-INFO +1 -1
- openhands_tools-1.56.0/openhands/tools/child_conversation/__init__.py +36 -0
- openhands_tools-1.56.0/openhands/tools/child_conversation/definition.py +162 -0
- openhands_tools-1.56.0/openhands/tools/child_conversation/impl.py +155 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/tmux_terminal.py +23 -4
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/PKG-INFO +1 -1
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/SOURCES.txt +6 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/pyproject.toml +1 -1
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/core.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/event_storage.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/logging_fix.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/recording.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/server.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/visualizer.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/editor.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/exceptions.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/config.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/constants.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/diff.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/history.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/shell.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/file_change.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/default.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/gemini.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/gpt5.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/planning.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/default.md +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/task_outcome.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/py.typed +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/manager.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task_tracker/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task_tracker/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/constants.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/descriptions.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/env.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/metadata.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/factory.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/interface.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/timeout_policy.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/command.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/executor.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/utils/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/utils/timeout.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/__init__.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/definition.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/impl.py +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/requires.txt +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/top_level.txt +0 -0
- {openhands_tools-1.55.0 → openhands_tools-1.56.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.56.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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Child-conversation tool package.
|
|
2
|
+
|
|
3
|
+
Provides the ``start_child_conversation`` tool: a server-executed tool that
|
|
4
|
+
starts a child conversation on the same backend as its parent and returns the
|
|
5
|
+
child's id and launch status to the parent agent. The launch itself is
|
|
6
|
+
delegated over HTTP to a child-launch endpoint, which defaults to the
|
|
7
|
+
agent-server running the tool (``POST /api/conversations/{id}/children``) and
|
|
8
|
+
can be pointed elsewhere via the ``launch_url`` tool parameter so a hosting
|
|
9
|
+
backend can run its own provisioning lifecycle.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from openhands.tools.child_conversation import StartChildConversationTool
|
|
13
|
+
|
|
14
|
+
agent = Agent(
|
|
15
|
+
llm=llm,
|
|
16
|
+
tools=[Tool(name=StartChildConversationTool.name)],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
The tool is registered by the agent-server; it is not part of the default
|
|
20
|
+
tool preset because it requires a server to create the child on.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from openhands.tools.child_conversation.definition import (
|
|
24
|
+
StartChildConversationAction,
|
|
25
|
+
StartChildConversationObservation,
|
|
26
|
+
StartChildConversationTool,
|
|
27
|
+
)
|
|
28
|
+
from openhands.tools.child_conversation.impl import StartChildConversationExecutor
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"StartChildConversationAction",
|
|
33
|
+
"StartChildConversationExecutor",
|
|
34
|
+
"StartChildConversationObservation",
|
|
35
|
+
"StartChildConversationTool",
|
|
36
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Action, observation, and tool definitions for start_child_conversation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Literal, Self
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from openhands.sdk.tool.registry import register_tool
|
|
10
|
+
from openhands.sdk.tool.tool import (
|
|
11
|
+
Action,
|
|
12
|
+
Observation,
|
|
13
|
+
ToolAnnotations,
|
|
14
|
+
ToolDefinition,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from openhands.sdk.conversation.state import ConversationState
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StartChildConversationAction(Action):
|
|
23
|
+
"""Action that starts a child conversation on the parent's backend."""
|
|
24
|
+
|
|
25
|
+
task: str = Field(
|
|
26
|
+
description=(
|
|
27
|
+
"Self-contained brief for the child: the goal, relevant files or "
|
|
28
|
+
"paths, constraints, and the expected deliverable. The child cannot "
|
|
29
|
+
"see this conversation's history."
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
title: str | None = Field(
|
|
33
|
+
default=None, description="Optional short title for the child conversation."
|
|
34
|
+
)
|
|
35
|
+
isolation: Literal["worktree", "shared"] = Field(
|
|
36
|
+
default="worktree",
|
|
37
|
+
description=(
|
|
38
|
+
"'worktree' gives the child its own git worktree and branch when the "
|
|
39
|
+
"workspace is a git repository; 'shared' runs it in the same directory."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def visualize(self) -> Text:
|
|
45
|
+
content = Text()
|
|
46
|
+
content.append("Start child conversation", style="bold cyan")
|
|
47
|
+
if self.title:
|
|
48
|
+
content.append(f": {self.title}")
|
|
49
|
+
content.append(f" [{self.isolation}]\n")
|
|
50
|
+
content.append(self.task)
|
|
51
|
+
return content
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StartChildConversationObservation(Observation):
|
|
55
|
+
"""Result of a child conversation launch."""
|
|
56
|
+
|
|
57
|
+
conversation_id: str | None = None
|
|
58
|
+
parent_conversation_id: str | None = None
|
|
59
|
+
status: str | None = None
|
|
60
|
+
title: str | None = None
|
|
61
|
+
url: str | None = None
|
|
62
|
+
workspace: str | None = None
|
|
63
|
+
isolation: Literal["worktree", "shared"] | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def visualize(self) -> Text:
|
|
67
|
+
content = Text()
|
|
68
|
+
if self.is_error:
|
|
69
|
+
content.append("Child conversation launch failed", style="bold red")
|
|
70
|
+
content.append("\n")
|
|
71
|
+
content.append(self.text)
|
|
72
|
+
return content
|
|
73
|
+
content.append("Started child conversation", style="bold green")
|
|
74
|
+
if self.title:
|
|
75
|
+
content.append(f" '{self.title}'")
|
|
76
|
+
if self.conversation_id:
|
|
77
|
+
content.append(f" ({self.conversation_id})")
|
|
78
|
+
if self.status:
|
|
79
|
+
content.append(f" - {self.status}")
|
|
80
|
+
if self.url:
|
|
81
|
+
content.append("\n")
|
|
82
|
+
content.append(self.url, style="underline")
|
|
83
|
+
return content
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_DESCRIPTION = (
|
|
87
|
+
"Start a child conversation that runs on the same backend as this "
|
|
88
|
+
"conversation, with its own agent and history, to work on a delegated task "
|
|
89
|
+
"in parallel.\n\n"
|
|
90
|
+
"The child inherits this conversation's configuration and workspace. `task` "
|
|
91
|
+
"must be a self-contained brief: the child has no access to this "
|
|
92
|
+
"conversation's history, so include the goal, relevant paths, constraints "
|
|
93
|
+
"and the expected deliverable. Use `title` to name it. `isolation` controls "
|
|
94
|
+
"the workspace: `worktree` (default) gives the child its own git worktree "
|
|
95
|
+
"and branch when the workspace is a git repository; the worktree starts "
|
|
96
|
+
"from the repository's default branch, not from this conversation's branch "
|
|
97
|
+
"or uncommitted changes, so use `shared` when the child must see them. "
|
|
98
|
+
"`shared` runs it in the same directory; backends that manage workspaces "
|
|
99
|
+
"themselves may ignore this and report what was applied.\n\n"
|
|
100
|
+
"The result contains the child's conversation id, its initial status and, "
|
|
101
|
+
"when available, a URL. Report the id and URL to the user. A launch is not "
|
|
102
|
+
"idempotent: call this once per task and do not retry a launch that already "
|
|
103
|
+
"returned a conversation id."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class StartChildConversationTool(
|
|
108
|
+
ToolDefinition[StartChildConversationAction, StartChildConversationObservation]
|
|
109
|
+
):
|
|
110
|
+
"""Tool that launches a same-backend child conversation."""
|
|
111
|
+
|
|
112
|
+
# Attached by the host that provides the launcher (the agent-server, or a
|
|
113
|
+
# hosting backend through ``launch_url``), not picked from the catalog.
|
|
114
|
+
user_selectable: ClassVar[bool] = False
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def create(
|
|
118
|
+
cls,
|
|
119
|
+
conv_state: "ConversationState",
|
|
120
|
+
launch_url: str | None = None,
|
|
121
|
+
**params,
|
|
122
|
+
) -> Sequence[Self]:
|
|
123
|
+
"""Create the tool bound to the current conversation.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
conv_state: State of the conversation that becomes the parent.
|
|
127
|
+
launch_url: Optional child-launch endpoint. Defaults to the
|
|
128
|
+
agent-server running this tool; a hosting backend can point it
|
|
129
|
+
at its own launcher.
|
|
130
|
+
"""
|
|
131
|
+
if params:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"StartChildConversationTool does not accept parameters: "
|
|
134
|
+
f"{sorted(params)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Import here to keep module import light and avoid import cycles.
|
|
138
|
+
from openhands.tools.child_conversation.impl import (
|
|
139
|
+
StartChildConversationExecutor,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
executor = StartChildConversationExecutor(
|
|
143
|
+
parent_conversation_id=conv_state.id, launch_url=launch_url
|
|
144
|
+
)
|
|
145
|
+
return [
|
|
146
|
+
cls(
|
|
147
|
+
description=_DESCRIPTION,
|
|
148
|
+
action_type=StartChildConversationAction,
|
|
149
|
+
observation_type=StartChildConversationObservation,
|
|
150
|
+
executor=executor,
|
|
151
|
+
annotations=ToolAnnotations(
|
|
152
|
+
readOnlyHint=False,
|
|
153
|
+
destructiveHint=False,
|
|
154
|
+
idempotentHint=False,
|
|
155
|
+
openWorldHint=True,
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Automatically register when this module is imported.
|
|
162
|
+
register_tool(StartChildConversationTool.name, StartChildConversationTool)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Executor for the start_child_conversation tool."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from openhands.sdk.conversation.request import (
|
|
12
|
+
StartChildConversationRequest,
|
|
13
|
+
StartChildConversationResponse,
|
|
14
|
+
)
|
|
15
|
+
from openhands.sdk.logger import get_logger
|
|
16
|
+
from openhands.sdk.tool.tool import ToolExecutor
|
|
17
|
+
from openhands.tools.child_conversation.definition import (
|
|
18
|
+
StartChildConversationAction,
|
|
19
|
+
StartChildConversationObservation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from openhands.sdk.conversation.impl.local_conversation import LocalConversation
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
# Same seam ``LookupSecret`` uses to reach the agent-server that runs this tool.
|
|
30
|
+
INTERNAL_SERVER_URL_ENV = "OH_INTERNAL_SERVER_URL"
|
|
31
|
+
DEFAULT_INTERNAL_SERVER_URL = "http://127.0.0.1:8000"
|
|
32
|
+
# v1 and v0 names of the agent-server session key, as exported by the server.
|
|
33
|
+
SESSION_API_KEY_ENVS = ("OH_SESSION_API_KEYS_0", "SESSION_API_KEY")
|
|
34
|
+
SESSION_API_KEY_HEADER = "X-Session-API-Key"
|
|
35
|
+
# A launch may include worktree or sandbox provisioning; mirror the MCP tool
|
|
36
|
+
# timeout rather than httpx's 5 second default.
|
|
37
|
+
CHILD_LAUNCH_TIMEOUT_SECONDS = 300.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def default_launch_url(parent_conversation_id: UUID) -> str:
|
|
41
|
+
"""Child-launch endpoint of the agent-server running this tool."""
|
|
42
|
+
base = os.getenv(INTERNAL_SERVER_URL_ENV, DEFAULT_INTERNAL_SERVER_URL).rstrip("/")
|
|
43
|
+
return f"{base}/api/conversations/{parent_conversation_id}/children"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def session_api_key_headers() -> dict[str, str]:
|
|
47
|
+
"""Auth header for the launcher, taken from the agent-server's own env.
|
|
48
|
+
|
|
49
|
+
The key identifies this server to its launcher, as it does for the
|
|
50
|
+
server's webhook subscribers. ``launch_url`` is chosen by whoever assembles
|
|
51
|
+
the conversation's agent, and the header grants that party nothing new: a
|
|
52
|
+
hosting backend minted the key, and a direct caller had to present one to
|
|
53
|
+
create the conversation.
|
|
54
|
+
"""
|
|
55
|
+
for env_name in SESSION_API_KEY_ENVS:
|
|
56
|
+
value = os.getenv(env_name)
|
|
57
|
+
if value:
|
|
58
|
+
return {SESSION_API_KEY_HEADER: value}
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _response_detail(response: httpx.Response) -> str:
|
|
63
|
+
try:
|
|
64
|
+
detail = response.json().get("detail")
|
|
65
|
+
except (ValueError, AttributeError):
|
|
66
|
+
detail = None
|
|
67
|
+
if isinstance(detail, str) and detail:
|
|
68
|
+
return detail
|
|
69
|
+
if detail is not None:
|
|
70
|
+
return json.dumps(detail)
|
|
71
|
+
return response.text[:500]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class StartChildConversationExecutor(
|
|
75
|
+
ToolExecutor[StartChildConversationAction, StartChildConversationObservation]
|
|
76
|
+
):
|
|
77
|
+
"""POST the launch request to the child-launch endpoint and report back.
|
|
78
|
+
|
|
79
|
+
The executor is deliberately backend-agnostic: it only knows the wire
|
|
80
|
+
contract (``StartChildConversationRequest`` in,
|
|
81
|
+
``StartChildConversationResponse`` out) and where to send it. By default it
|
|
82
|
+
targets the agent-server that runs this tool, authenticated with that
|
|
83
|
+
server's own session API key from the environment.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
parent_conversation_id: UUID,
|
|
89
|
+
launch_url: str | None = None,
|
|
90
|
+
timeout: float = CHILD_LAUNCH_TIMEOUT_SECONDS,
|
|
91
|
+
transport: httpx.BaseTransport | None = None,
|
|
92
|
+
):
|
|
93
|
+
self.parent_conversation_id = parent_conversation_id
|
|
94
|
+
self.launch_url = launch_url
|
|
95
|
+
self.timeout = timeout
|
|
96
|
+
self._transport = transport
|
|
97
|
+
|
|
98
|
+
def __call__(
|
|
99
|
+
self,
|
|
100
|
+
action: StartChildConversationAction,
|
|
101
|
+
conversation: "LocalConversation | None" = None, # noqa: ARG002
|
|
102
|
+
) -> StartChildConversationObservation:
|
|
103
|
+
url = self.launch_url or default_launch_url(self.parent_conversation_id)
|
|
104
|
+
try:
|
|
105
|
+
payload = StartChildConversationRequest(
|
|
106
|
+
task=action.task, title=action.title, isolation=action.isolation
|
|
107
|
+
).model_dump(mode="json")
|
|
108
|
+
except ValidationError as exc:
|
|
109
|
+
return self._error(f"Invalid child conversation request: {exc}")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with httpx.Client(
|
|
113
|
+
timeout=self.timeout, transport=self._transport
|
|
114
|
+
) as client:
|
|
115
|
+
response = client.post(
|
|
116
|
+
url, json=payload, headers=session_api_key_headers()
|
|
117
|
+
)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
result = StartChildConversationResponse.model_validate(response.json())
|
|
120
|
+
except httpx.HTTPStatusError as exc:
|
|
121
|
+
return self._error(
|
|
122
|
+
"Child conversation launch failed "
|
|
123
|
+
f"({exc.response.status_code}): {_response_detail(exc.response)}"
|
|
124
|
+
)
|
|
125
|
+
except httpx.TimeoutException:
|
|
126
|
+
return self._error(
|
|
127
|
+
"Child conversation launch timed out after "
|
|
128
|
+
f"{self.timeout:.0f} seconds. The child may still have been "
|
|
129
|
+
"created; check with the user before retrying."
|
|
130
|
+
)
|
|
131
|
+
except (httpx.HTTPError, ValueError, ValidationError) as exc:
|
|
132
|
+
logger.warning("start_child_conversation failed against %s: %s", url, exc)
|
|
133
|
+
return self._error(
|
|
134
|
+
f"Child conversation launch failed: {type(exc).__name__}: {exc}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
summary = (
|
|
138
|
+
f"Started child conversation {result.conversation_id} "
|
|
139
|
+
f"(status: {result.status}).\n"
|
|
140
|
+
f"{json.dumps(result.model_dump(mode='json'), indent=2)}"
|
|
141
|
+
)
|
|
142
|
+
return StartChildConversationObservation.from_text(
|
|
143
|
+
text=summary,
|
|
144
|
+
conversation_id=str(result.conversation_id),
|
|
145
|
+
parent_conversation_id=str(result.parent_conversation_id),
|
|
146
|
+
status=result.status,
|
|
147
|
+
title=result.title,
|
|
148
|
+
url=result.url,
|
|
149
|
+
workspace=result.workspace,
|
|
150
|
+
isolation=result.isolation,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _error(message: str) -> StartChildConversationObservation:
|
|
155
|
+
return StartChildConversationObservation.from_text(text=message, is_error=True)
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/tmux_terminal.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import sys
|
|
5
6
|
import time
|
|
6
7
|
import uuid
|
|
7
8
|
from collections.abc import Iterator, Mapping
|
|
@@ -75,7 +76,7 @@ def _install_libtmux_redaction_filter() -> None:
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
@contextmanager
|
|
78
|
-
def _socket_creation_lock() -> Iterator[None]:
|
|
79
|
+
def _socket_creation_lock(socket_dir: Path | None = None) -> Iterator[None]:
|
|
79
80
|
"""Serialize socket creation with the agent-server's startup cleanup.
|
|
80
81
|
|
|
81
82
|
A tmux socket exists between ``new-session`` and the recording of its owner
|
|
@@ -85,7 +86,8 @@ def _socket_creation_lock() -> Iterator[None]:
|
|
|
85
86
|
directory, which is the one path every process sharing ``TMUX_TMPDIR``
|
|
86
87
|
agrees on even while tmux has not created any socket yet.
|
|
87
88
|
"""
|
|
88
|
-
socket_dir
|
|
89
|
+
if socket_dir is None:
|
|
90
|
+
socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
|
|
89
91
|
# tmux refuses a socket directory that is group/world accessible, and
|
|
90
92
|
# FileLock would create it with the umask default, so create it 0700 first.
|
|
91
93
|
socket_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -124,9 +126,26 @@ def _new_owned_session(
|
|
|
124
126
|
lock. Startup cleanup in another process takes the same lock, so it can
|
|
125
127
|
never observe the socket before its owner is published.
|
|
126
128
|
"""
|
|
127
|
-
|
|
129
|
+
socket_name = f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}"
|
|
130
|
+
socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
|
|
131
|
+
socket_path = socket_dir / socket_name
|
|
132
|
+
# sun_path includes a NUL byte; limits are 108 on Linux and 104 on macOS/BSD.
|
|
133
|
+
max_socket_path = 107 if sys.platform.startswith("linux") else 103
|
|
134
|
+
fallback = (
|
|
135
|
+
os.name == "posix"
|
|
136
|
+
and len(os.fsencode(os.path.realpath(socket_path))) > max_socket_path
|
|
137
|
+
)
|
|
138
|
+
if fallback:
|
|
139
|
+
socket_dir = Path("/tmp") / f"tmux-{os.getuid()}"
|
|
140
|
+
# libtmux resolves socket_name using the process-wide TMUX_TMPDIR, so
|
|
141
|
+
# use an explicit path without changing other sessions' environment.
|
|
142
|
+
logger.warning("tmux socket path is too long; using %s", socket_dir)
|
|
143
|
+
|
|
144
|
+
with _socket_creation_lock(socket_dir):
|
|
128
145
|
server = libtmux.Server(
|
|
129
|
-
socket_name=
|
|
146
|
+
socket_name=None if fallback else socket_name,
|
|
147
|
+
socket_path=str(socket_dir / socket_name) if fallback else None,
|
|
148
|
+
environment=dict(env),
|
|
130
149
|
)
|
|
131
150
|
session = server.new_session(
|
|
132
151
|
session_name=session_name,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.56.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
|
|
@@ -14,6 +14,9 @@ pyproject.toml
|
|
|
14
14
|
./openhands/tools/browser_use/logging_fix.py
|
|
15
15
|
./openhands/tools/browser_use/recording.py
|
|
16
16
|
./openhands/tools/browser_use/server.py
|
|
17
|
+
./openhands/tools/child_conversation/__init__.py
|
|
18
|
+
./openhands/tools/child_conversation/definition.py
|
|
19
|
+
./openhands/tools/child_conversation/impl.py
|
|
17
20
|
./openhands/tools/delegate/__init__.py
|
|
18
21
|
./openhands/tools/delegate/definition.py
|
|
19
22
|
./openhands/tools/delegate/impl.py
|
|
@@ -112,6 +115,9 @@ openhands/tools/browser_use/impl.py
|
|
|
112
115
|
openhands/tools/browser_use/logging_fix.py
|
|
113
116
|
openhands/tools/browser_use/recording.py
|
|
114
117
|
openhands/tools/browser_use/server.py
|
|
118
|
+
openhands/tools/child_conversation/__init__.py
|
|
119
|
+
openhands/tools/child_conversation/definition.py
|
|
120
|
+
openhands/tools/child_conversation/impl.py
|
|
115
121
|
openhands/tools/delegate/__init__.py
|
|
116
122
|
openhands/tools/delegate/definition.py
|
|
117
123
|
openhands/tools/delegate/impl.py
|
|
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.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/event_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/config.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/encoding.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/file_cache.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/history.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/impl.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/definition.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/definition.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/bash_runner.md
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/code_explorer.md
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/default.md
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/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
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/__init__.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/factory.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/timeout_policy.py
RENAMED
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_tools-1.55.0 → openhands_tools-1.56.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.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|