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.
Files changed (109) hide show
  1. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/PKG-INFO +1 -1
  2. openhands_tools-1.56.0/openhands/tools/child_conversation/__init__.py +36 -0
  3. openhands_tools-1.56.0/openhands/tools/child_conversation/definition.py +162 -0
  4. openhands_tools-1.56.0/openhands/tools/child_conversation/impl.py +155 -0
  5. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/tmux_terminal.py +23 -4
  6. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/PKG-INFO +1 -1
  7. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/SOURCES.txt +6 -0
  8. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/pyproject.toml +1 -1
  9. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/__init__.py +0 -0
  10. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/__init__.py +0 -0
  11. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/core.py +0 -0
  12. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/apply_patch/definition.py +0 -0
  13. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  14. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/definition.py +0 -0
  15. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/ask_oracle/impl.py +0 -0
  16. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/__init__.py +0 -0
  17. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/definition.py +0 -0
  18. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/event_storage.py +0 -0
  19. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/impl.py +0 -0
  20. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  21. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/recording.py +0 -0
  22. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/browser_use/server.py +0 -0
  23. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/__init__.py +0 -0
  24. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/definition.py +0 -0
  25. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/impl.py +0 -0
  26. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/delegate/visualizer.py +0 -0
  27. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/__init__.py +0 -0
  28. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/definition.py +0 -0
  29. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/editor.py +0 -0
  30. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/exceptions.py +0 -0
  31. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/impl.py +0 -0
  32. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  33. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/config.py +0 -0
  34. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  35. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  36. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  37. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  38. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/history.py +0 -0
  39. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  40. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/__init__.py +0 -0
  41. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  42. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/definition.py +0 -0
  43. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/edit/impl.py +0 -0
  44. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/file_change.py +0 -0
  45. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  46. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  47. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  48. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  49. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  50. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  51. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  52. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  53. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  54. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/__init__.py +0 -0
  55. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/definition.py +0 -0
  56. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/glob/impl.py +0 -0
  57. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/__init__.py +0 -0
  58. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/definition.py +0 -0
  59. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/grep/impl.py +0 -0
  60. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  61. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  62. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  63. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/__init__.py +0 -0
  64. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/default.py +0 -0
  65. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/gemini.py +0 -0
  66. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/gpt5.py +0 -0
  67. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/planning.py +0 -0
  68. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  69. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  70. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/default.md +0 -0
  71. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  72. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/preset/task_outcome.py +0 -0
  73. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/py.typed +0 -0
  74. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/__init__.py +0 -0
  75. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/definition.py +0 -0
  76. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/impl.py +0 -0
  77. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task/manager.py +0 -0
  78. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task_tracker/__init__.py +0 -0
  79. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/task_tracker/definition.py +0 -0
  80. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/__init__.py +0 -0
  81. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/constants.py +0 -0
  82. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/definition.py +0 -0
  83. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/descriptions.py +0 -0
  84. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/env.py +0 -0
  85. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/impl.py +0 -0
  86. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/metadata.py +0 -0
  87. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  88. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  89. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  90. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  91. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  92. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +0 -0
  93. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
  94. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  95. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  96. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/command.py +0 -0
  97. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  98. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/__init__.py +0 -0
  99. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/definition.py +0 -0
  100. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/tom_consult/executor.py +0 -0
  101. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/utils/__init__.py +0 -0
  102. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/utils/timeout.py +0 -0
  103. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/__init__.py +0 -0
  104. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/definition.py +0 -0
  105. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands/tools/workflow/impl.py +0 -0
  106. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  107. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/requires.txt +0 -0
  108. {openhands_tools-1.55.0 → openhands_tools-1.56.0}/openhands_tools.egg-info/top_level.txt +0 -0
  109. {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.55.0
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)
@@ -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 = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
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
- with _socket_creation_lock():
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=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=dict(env)
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.55.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.55.0"
3
+ version = "1.56.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"