openhands-tools 1.55.1__tar.gz → 1.57.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 (110) hide show
  1. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/PKG-INFO +1 -1
  2. openhands_tools-1.57.0/openhands/tools/child_conversation/__init__.py +36 -0
  3. openhands_tools-1.57.0/openhands/tools/child_conversation/definition.py +162 -0
  4. openhands_tools-1.57.0/openhands/tools/child_conversation/impl.py +155 -0
  5. openhands_tools-1.57.0/openhands/tools/terminal/terminal/process_priority.py +39 -0
  6. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +4 -1
  7. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +8 -3
  8. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/tmux_terminal.py +30 -7
  9. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/windows_terminal.py +10 -4
  10. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands_tools.egg-info/PKG-INFO +1 -1
  11. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands_tools.egg-info/SOURCES.txt +8 -0
  12. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/pyproject.toml +1 -1
  13. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/__init__.py +0 -0
  14. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/apply_patch/__init__.py +0 -0
  15. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/apply_patch/core.py +0 -0
  16. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/apply_patch/definition.py +0 -0
  17. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  18. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/ask_oracle/definition.py +0 -0
  19. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/ask_oracle/impl.py +0 -0
  20. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/__init__.py +0 -0
  21. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/definition.py +0 -0
  22. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/event_storage.py +0 -0
  23. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/impl.py +0 -0
  24. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  25. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/recording.py +0 -0
  26. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/browser_use/server.py +0 -0
  27. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/delegate/__init__.py +0 -0
  28. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/delegate/definition.py +0 -0
  29. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/delegate/impl.py +0 -0
  30. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/delegate/visualizer.py +0 -0
  31. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/__init__.py +0 -0
  32. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/definition.py +0 -0
  33. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/editor.py +0 -0
  34. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/exceptions.py +0 -0
  35. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/impl.py +0 -0
  36. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  37. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/config.py +0 -0
  38. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  39. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  40. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/encoding.py +0 -0
  41. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  42. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/history.py +0 -0
  43. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  44. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/__init__.py +0 -0
  45. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  46. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/edit/definition.py +0 -0
  47. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/edit/impl.py +0 -0
  48. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/file_change.py +0 -0
  49. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  50. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  51. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  52. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  53. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  54. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  55. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  56. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  57. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  58. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/glob/__init__.py +0 -0
  59. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/glob/definition.py +0 -0
  60. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/glob/impl.py +0 -0
  61. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/grep/__init__.py +0 -0
  62. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/grep/definition.py +0 -0
  63. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/grep/impl.py +0 -0
  64. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  65. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  66. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  67. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/__init__.py +0 -0
  68. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/default.py +0 -0
  69. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/gemini.py +0 -0
  70. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/gpt5.py +0 -0
  71. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/planning.py +0 -0
  72. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  73. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  74. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/subagents/default.md +0 -0
  75. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  76. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/preset/task_outcome.py +0 -0
  77. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/py.typed +0 -0
  78. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task/__init__.py +0 -0
  79. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task/definition.py +0 -0
  80. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task/impl.py +0 -0
  81. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task/manager.py +0 -0
  82. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task_tracker/__init__.py +0 -0
  83. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/task_tracker/definition.py +0 -0
  84. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/__init__.py +0 -0
  85. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/constants.py +0 -0
  86. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/definition.py +0 -0
  87. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/descriptions.py +0 -0
  88. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/env.py +0 -0
  89. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/impl.py +0 -0
  90. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/metadata.py +0 -0
  91. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  92. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  93. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  94. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/terminal/terminal_session.py +0 -0
  95. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  96. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  97. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/utils/command.py +0 -0
  98. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  99. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/tom_consult/__init__.py +0 -0
  100. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/tom_consult/definition.py +0 -0
  101. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/tom_consult/executor.py +0 -0
  102. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/utils/__init__.py +0 -0
  103. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/utils/timeout.py +0 -0
  104. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/workflow/__init__.py +0 -0
  105. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/workflow/definition.py +0 -0
  106. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands/tools/workflow/impl.py +0 -0
  107. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  108. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands_tools.egg-info/requires.txt +0 -0
  109. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/openhands_tools.egg-info/top_level.txt +0 -0
  110. {openhands_tools-1.55.1 → openhands_tools-1.57.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.55.1
3
+ Version: 1.57.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)
@@ -0,0 +1,39 @@
1
+ """Lower the CPU priority of agent terminals so they cannot starve the server."""
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ from collections.abc import Mapping
7
+ from typing import Final
8
+
9
+
10
+ TERMINAL_PROCESS_PRIORITY_ENV: Final[str] = "OH_TERMINAL_PROCESS_PRIORITY"
11
+
12
+ # macOS largely ignores nice; the utility QoS clamp is what keeps the server
13
+ # responsive. Avoid the background clamp: it also throttles I/O heavily.
14
+ _TASKPOLICY_PATH: Final[str] = "/usr/sbin/taskpolicy"
15
+ _LINUX_NICE_INCREMENT: Final[str] = "10"
16
+
17
+
18
+ def should_lower_process_priority(env: Mapping[str, str]) -> bool:
19
+ """Return False when the terminal env opts out with ``none``."""
20
+ value = env.get(TERMINAL_PROCESS_PRIORITY_ENV, "")
21
+ return value.strip().lower() != "none"
22
+
23
+
24
+ def process_priority_prefix(env: Mapping[str, str]) -> list[str]:
25
+ """Return an argv prefix that runs the shell at lower CPU priority.
26
+
27
+ This must be an argv prefix rather than a ``preexec_fn``: a macOS QoS class
28
+ set on the calling thread does not survive ``exec``. Both ``taskpolicy``
29
+ and ``nice`` are inherited by every process the shell starts.
30
+ """
31
+ if not should_lower_process_priority(env):
32
+ return []
33
+
34
+ system = platform.system()
35
+ if system == "Darwin" and os.path.exists(_TASKPOLICY_PATH):
36
+ return [_TASKPOLICY_PATH, "-c", "utility"]
37
+ if system == "Linux" and (nice_path := shutil.which("nice")) is not None:
38
+ return [nice_path, "-n", _LINUX_NICE_INCREMENT]
39
+ return []
@@ -35,6 +35,9 @@ from openhands.tools.terminal.env import (
35
35
  from openhands.tools.terminal.metadata import CmdOutputMetadata
36
36
  from openhands.tools.terminal.terminal import TerminalInterface
37
37
  from openhands.tools.terminal.terminal.interface import parse_ctrl_key
38
+ from openhands.tools.terminal.terminal.process_priority import (
39
+ process_priority_prefix,
40
+ )
38
41
 
39
42
 
40
43
  logger = get_logger(__name__)
@@ -151,7 +154,7 @@ class SubprocessTerminal(TerminalInterface):
151
154
  env["PS2"] = ""
152
155
  env["TERM"] = "xterm-256color"
153
156
 
154
- bash_cmd = [resolved_shell_path, "-i"]
157
+ bash_cmd = [*process_priority_prefix(env), resolved_shell_path, "-i"]
155
158
 
156
159
  # Create a PTY; give the slave to the child, keep the master
157
160
  master_fd, slave_fd = pty.openpty()
@@ -6,6 +6,7 @@ tmux session, enabling concurrent command execution across panes.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import shlex
9
10
  import threading
10
11
  import time
11
12
  import uuid
@@ -27,6 +28,9 @@ from openhands.tools.terminal.env import (
27
28
  build_terminal_env,
28
29
  normalize_terminal_env,
29
30
  )
31
+ from openhands.tools.terminal.terminal.process_priority import (
32
+ process_priority_prefix,
33
+ )
30
34
  from openhands.tools.terminal.terminal.tmux_terminal import (
31
35
  TmuxTerminal,
32
36
  _new_owned_session,
@@ -169,13 +173,14 @@ class TmuxPanePool:
169
173
  """Create a new PooledTmuxTerminal within the shared session."""
170
174
  assert self._session is not None
171
175
 
172
- shell_command = "/bin/bash"
176
+ shell_command = ["/bin/bash"]
173
177
  if self.username in ["root", "openhands"]:
174
- shell_command = f"su {self.username} -"
178
+ shell_command = ["su", self.username, "-"]
179
+ priority_prefix = process_priority_prefix(build_terminal_env(self.env))
175
180
 
176
181
  window = self._session.new_window(
177
182
  window_name=f"pane-{len(self._all_panes)}",
178
- window_shell=shell_command,
183
+ window_shell=shlex.join([*priority_prefix, *shell_command]),
179
184
  start_directory=self.work_dir,
180
185
  )
181
186
  active_pane = window.active_pane
@@ -2,6 +2,8 @@
2
2
 
3
3
  import logging
4
4
  import os
5
+ import shlex
6
+ import sys
5
7
  import time
6
8
  import uuid
7
9
  from collections.abc import Iterator, Mapping
@@ -28,6 +30,9 @@ from openhands.tools.terminal.env import (
28
30
  from openhands.tools.terminal.metadata import CmdOutputMetadata
29
31
  from openhands.tools.terminal.terminal import TerminalInterface
30
32
  from openhands.tools.terminal.terminal.interface import parse_ctrl_key
33
+ from openhands.tools.terminal.terminal.process_priority import (
34
+ process_priority_prefix,
35
+ )
31
36
 
32
37
 
33
38
  logger = get_logger(__name__)
@@ -75,7 +80,7 @@ def _install_libtmux_redaction_filter() -> None:
75
80
 
76
81
 
77
82
  @contextmanager
78
- def _socket_creation_lock() -> Iterator[None]:
83
+ def _socket_creation_lock(socket_dir: Path | None = None) -> Iterator[None]:
79
84
  """Serialize socket creation with the agent-server's startup cleanup.
80
85
 
81
86
  A tmux socket exists between ``new-session`` and the recording of its owner
@@ -85,7 +90,8 @@ def _socket_creation_lock() -> Iterator[None]:
85
90
  directory, which is the one path every process sharing ``TMUX_TMPDIR``
86
91
  agrees on even while tmux has not created any socket yet.
87
92
  """
88
- socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
93
+ if socket_dir is None:
94
+ socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
89
95
  # tmux refuses a socket directory that is group/world accessible, and
90
96
  # FileLock would create it with the umask default, so create it 0700 first.
91
97
  socket_dir.mkdir(parents=True, exist_ok=True)
@@ -124,9 +130,26 @@ def _new_owned_session(
124
130
  lock. Startup cleanup in another process takes the same lock, so it can
125
131
  never observe the socket before its owner is published.
126
132
  """
127
- with _socket_creation_lock():
133
+ socket_name = f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}"
134
+ socket_dir = Path(os.environ.get("TMUX_TMPDIR", "/tmp")) / f"tmux-{os.getuid()}"
135
+ socket_path = socket_dir / socket_name
136
+ # sun_path includes a NUL byte; limits are 108 on Linux and 104 on macOS/BSD.
137
+ max_socket_path = 107 if sys.platform.startswith("linux") else 103
138
+ fallback = (
139
+ os.name == "posix"
140
+ and len(os.fsencode(os.path.realpath(socket_path))) > max_socket_path
141
+ )
142
+ if fallback:
143
+ socket_dir = Path("/tmp") / f"tmux-{os.getuid()}"
144
+ # libtmux resolves socket_name using the process-wide TMUX_TMPDIR, so
145
+ # use an explicit path without changing other sessions' environment.
146
+ logger.warning("tmux socket path is too long; using %s", socket_dir)
147
+
148
+ with _socket_creation_lock(socket_dir):
128
149
  server = libtmux.Server(
129
- socket_name=f"{TMUX_SOCKET_NAME}-{uuid.uuid4().hex}", environment=dict(env)
150
+ socket_name=None if fallback else socket_name,
151
+ socket_path=str(socket_dir / socket_name) if fallback else None,
152
+ environment=dict(env),
130
153
  )
131
154
  session = server.new_session(
132
155
  session_name=session_name,
@@ -193,12 +216,12 @@ class TmuxTerminal(TerminalInterface):
193
216
  # auto-launch `less` on a TTY don't capture the pane and wedge the session.
194
217
  env.setdefault("GIT_PAGER", "cat")
195
218
  env.setdefault("PAGER", "cat")
196
- _shell_command = "/bin/bash"
219
+ shell_command = ["/bin/bash"]
197
220
  if self.username in ["root", "openhands"]:
198
221
  # This starts a non-login (new) shell for the given user
199
- _shell_command = f"su {self.username} -"
222
+ shell_command = ["su", self.username, "-"]
200
223
 
201
- window_command = _shell_command
224
+ window_command = shlex.join([*process_priority_prefix(env), *shell_command])
202
225
 
203
226
  logger.debug(f"Initializing tmux terminal with command: {window_command}")
204
227
  # Never reuse a socket: tmux numeric IDs restart at zero after server exit.
@@ -26,6 +26,9 @@ from openhands.tools.terminal.terminal.interface import (
26
26
  TerminalInterface,
27
27
  parse_ctrl_key,
28
28
  )
29
+ from openhands.tools.terminal.terminal.process_priority import (
30
+ should_lower_process_priority,
31
+ )
29
32
 
30
33
 
31
34
  logger = get_logger(__name__)
@@ -92,6 +95,10 @@ class WindowsTerminal(TerminalInterface):
92
95
  if self._initialized:
93
96
  return
94
97
 
98
+ env = build_terminal_env(self._env)
99
+ env.setdefault("PYTHONIOENCODING", "utf-8")
100
+ env.setdefault("PYTHONUTF8", "1")
101
+
95
102
  startupinfo = None
96
103
  creationflags = 0
97
104
  if platform.system() == "Windows":
@@ -101,10 +108,9 @@ class WindowsTerminal(TerminalInterface):
101
108
  startupinfo.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 0)
102
109
  creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
103
110
  creationflags |= getattr(subprocess, "CREATE_NO_WINDOW", 0)
104
-
105
- env = build_terminal_env(self._env)
106
- env.setdefault("PYTHONIOENCODING", "utf-8")
107
- env.setdefault("PYTHONUTF8", "1")
111
+ # Not PROCESS_MODE_BACKGROUND_BEGIN: it also throttles disk I/O.
112
+ if should_lower_process_priority(env):
113
+ creationflags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
108
114
 
109
115
  self.process = subprocess.Popen(
110
116
  [self.shell_path, "-NoLogo", "-NoProfile"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.55.1
3
+ Version: 1.57.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
@@ -81,6 +84,7 @@ pyproject.toml
81
84
  ./openhands/tools/terminal/terminal/__init__.py
82
85
  ./openhands/tools/terminal/terminal/factory.py
83
86
  ./openhands/tools/terminal/terminal/interface.py
87
+ ./openhands/tools/terminal/terminal/process_priority.py
84
88
  ./openhands/tools/terminal/terminal/subprocess_terminal.py
85
89
  ./openhands/tools/terminal/terminal/terminal_session.py
86
90
  ./openhands/tools/terminal/terminal/tmux_pane_pool.py
@@ -112,6 +116,9 @@ openhands/tools/browser_use/impl.py
112
116
  openhands/tools/browser_use/logging_fix.py
113
117
  openhands/tools/browser_use/recording.py
114
118
  openhands/tools/browser_use/server.py
119
+ openhands/tools/child_conversation/__init__.py
120
+ openhands/tools/child_conversation/definition.py
121
+ openhands/tools/child_conversation/impl.py
115
122
  openhands/tools/delegate/__init__.py
116
123
  openhands/tools/delegate/definition.py
117
124
  openhands/tools/delegate/impl.py
@@ -179,6 +186,7 @@ openhands/tools/terminal/timeout_policy.py
179
186
  openhands/tools/terminal/terminal/__init__.py
180
187
  openhands/tools/terminal/terminal/factory.py
181
188
  openhands/tools/terminal/terminal/interface.py
189
+ openhands/tools/terminal/terminal/process_priority.py
182
190
  openhands/tools/terminal/terminal/subprocess_terminal.py
183
191
  openhands/tools/terminal/terminal/terminal_session.py
184
192
  openhands/tools/terminal/terminal/tmux_pane_pool.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-tools"
3
- version = "1.55.1"
3
+ version = "1.57.0"
4
4
  description = "OpenHands Tools - Runtime tools for AI agents"
5
5
 
6
6
  requires-python = ">=3.12"