openhands-tools 1.53.0__tar.gz → 1.55.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/PKG-INFO +2 -1
  2. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/delegate/impl.py +6 -1
  3. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/editor.py +25 -13
  4. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/encoding.py +18 -2
  5. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task/definition.py +4 -4
  6. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task/manager.py +10 -2
  7. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/definition.py +21 -2
  8. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/constants.py +15 -1
  9. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/terminal_session.py +188 -51
  10. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_pane_pool.py +7 -8
  11. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/tmux_terminal.py +75 -7
  12. openhands_tools-1.55.0/openhands/tools/terminal/utils/command.py +195 -0
  13. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/PKG-INFO +2 -1
  14. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/requires.txt +1 -0
  15. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/pyproject.toml +2 -1
  16. openhands_tools-1.53.0/openhands/tools/terminal/utils/command.py +0 -111
  17. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/__init__.py +0 -0
  18. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/__init__.py +0 -0
  19. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/core.py +0 -0
  20. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/apply_patch/definition.py +0 -0
  21. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/__init__.py +0 -0
  22. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/definition.py +0 -0
  23. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/ask_oracle/impl.py +0 -0
  24. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/__init__.py +0 -0
  25. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/definition.py +0 -0
  26. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/event_storage.py +0 -0
  27. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/impl.py +0 -0
  28. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/logging_fix.py +0 -0
  29. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/recording.py +0 -0
  30. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/browser_use/server.py +0 -0
  31. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/delegate/__init__.py +0 -0
  32. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/delegate/definition.py +0 -0
  33. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/delegate/visualizer.py +0 -0
  34. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/__init__.py +0 -0
  35. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/definition.py +0 -0
  36. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/exceptions.py +0 -0
  37. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/impl.py +0 -0
  38. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/__init__.py +0 -0
  39. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/config.py +0 -0
  40. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/constants.py +0 -0
  41. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/diff.py +0 -0
  42. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/file_cache.py +0 -0
  43. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/history.py +0 -0
  44. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/file_editor/utils/shell.py +0 -0
  45. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/__init__.py +0 -0
  46. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/__init__.py +0 -0
  47. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/definition.py +0 -0
  48. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/edit/impl.py +0 -0
  49. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/file_change.py +0 -0
  50. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/__init__.py +0 -0
  51. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/definition.py +0 -0
  52. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/list_directory/impl.py +0 -0
  53. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/__init__.py +0 -0
  54. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/definition.py +0 -0
  55. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/read_file/impl.py +0 -0
  56. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/__init__.py +0 -0
  57. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/definition.py +0 -0
  58. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/gemini/write_file/impl.py +0 -0
  59. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/glob/__init__.py +0 -0
  60. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/glob/definition.py +0 -0
  61. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/glob/impl.py +0 -0
  62. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/grep/__init__.py +0 -0
  63. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/grep/definition.py +0 -0
  64. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/grep/impl.py +0 -0
  65. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/__init__.py +0 -0
  66. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/definition.py +0 -0
  67. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/planning_file_editor/impl.py +0 -0
  68. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/__init__.py +0 -0
  69. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/default.py +0 -0
  70. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/gemini.py +0 -0
  71. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/gpt5.py +0 -0
  72. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/planning.py +0 -0
  73. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/bash_runner.md +0 -0
  74. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/code_explorer.md +0 -0
  75. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/default.md +0 -0
  76. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/subagents/web_researcher.md +0 -0
  77. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/preset/task_outcome.py +0 -0
  78. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/py.typed +0 -0
  79. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task/__init__.py +0 -0
  80. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task/impl.py +0 -0
  81. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/task_tracker/__init__.py +0 -0
  82. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/__init__.py +0 -0
  83. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/definition.py +0 -0
  84. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/descriptions.py +0 -0
  85. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/env.py +0 -0
  86. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/impl.py +0 -0
  87. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/metadata.py +0 -0
  88. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/__init__.py +0 -0
  89. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/factory.py +0 -0
  90. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/interface.py +0 -0
  91. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/subprocess_terminal.py +0 -0
  92. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/terminal/windows_terminal.py +0 -0
  93. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/timeout_policy.py +0 -0
  94. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/__init__.py +0 -0
  95. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/terminal/utils/escape_filter.py +0 -0
  96. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/__init__.py +0 -0
  97. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/definition.py +0 -0
  98. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/tom_consult/executor.py +0 -0
  99. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/utils/__init__.py +0 -0
  100. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/utils/timeout.py +0 -0
  101. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/workflow/__init__.py +0 -0
  102. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/workflow/definition.py +0 -0
  103. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands/tools/workflow/impl.py +0 -0
  104. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/SOURCES.txt +0 -0
  105. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/dependency_links.txt +0 -0
  106. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/openhands_tools.egg-info/top_level.txt +0 -0
  107. {openhands_tools-1.53.0 → openhands_tools-1.55.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-tools
3
- Version: 1.53.0
3
+ Version: 1.55.0
4
4
  Summary: OpenHands Tools - Runtime tools for AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -12,6 +12,7 @@ Requires-Dist: tree-sitter>=0.24
12
12
  Requires-Dist: tree-sitter-bash>=0.23
13
13
  Requires-Dist: binaryornot>=0.6.0
14
14
  Requires-Dist: cachetools
15
+ Requires-Dist: filelock>=3.20.1
15
16
  Requires-Dist: libtmux>=0.53.0
16
17
  Requires-Dist: pydantic>=2.13.5
17
18
  Requires-Dist: browser-use>=0.8.0
@@ -13,6 +13,7 @@ from openhands.sdk.conversation.state import (
13
13
  )
14
14
  from openhands.sdk.logger import get_logger
15
15
  from openhands.sdk.subagent import get_agent_factory
16
+ from openhands.sdk.subagent.scope import inherit_mcp_config
16
17
  from openhands.sdk.tool.tool import ToolExecutor
17
18
  from openhands.tools.delegate.definition import DelegateObservation
18
19
 
@@ -188,7 +189,9 @@ class DelegateExecutor(ToolExecutor):
188
189
  worker_agent = factory.factory_func(sub_agent_llm)
189
190
 
190
191
  # ensuring that the sub-agent LLM has stream deactivated
191
- worker_agent = worker_agent.model_copy(
192
+ worker_agent = inherit_mcp_config(
193
+ parent_conversation.agent, worker_agent
194
+ ).model_copy(
192
195
  update={
193
196
  "llm": worker_agent.llm.model_copy(update={"stream": False})
194
197
  }
@@ -223,6 +226,8 @@ class DelegateExecutor(ToolExecutor):
223
226
  "_parent_llm_call_context": (
224
227
  parent_conversation.get_llm_call_context()
225
228
  ),
229
+ "mcp_tool_provider": parent_conversation._mcp_tool_provider,
230
+ "_mcp_config_from_parent": True,
226
231
  }
227
232
 
228
233
  if factory.definition.max_iteration_per_run is not None:
@@ -414,11 +414,9 @@ class FileEditor:
414
414
  )
415
415
 
416
416
  file_content = self.read_file(path, start_line=start_line, end_line=end_line)
417
+ file_content = self._normalize_lines(file_content)
417
418
 
418
- # Get the detected encoding
419
- output = self._make_output(
420
- "\n".join(file_content.splitlines()), str(path), start_line
421
- ) # Remove extra newlines
419
+ output = self._make_output(file_content, str(path), start_line)
422
420
 
423
421
  # Prepend warning if we truncated the end_line
424
422
  if warning_message:
@@ -787,6 +785,17 @@ class FileEditor:
787
785
  except Exception as e:
788
786
  raise ToolError(f"Ran into {e} while trying to read {path}") from None
789
787
 
788
+ def _normalize_lines(self, content: str) -> str:
789
+ """Rejoin a template snippet without folding exotic line separators.
790
+
791
+ ``read_file`` returns a snippet whose lines are separated by ``\\n``. The
792
+ ranged ``view`` path used to round-trip it through ``str.splitlines()``,
793
+ which also breaks on form feeds and Unicode separators that the rest of
794
+ the editor treats as ordinary characters, shifting line numbers. Joining
795
+ on ``\\n`` instead keeps every other character intact.
796
+ """
797
+ return "\n".join(content.split("\n"))
798
+
790
799
  def _make_output(
791
800
  self,
792
801
  snippet_content: str,
@@ -815,14 +824,17 @@ class FileEditor:
815
824
  truncate_notice=TEXT_FILE_CONTENT_TRUNCATED_NOTICE,
816
825
  )
817
826
 
827
+ # Split on "\n" only, matching how read_file/_count_lines define lines, so that
828
+ # characters splitlines() treats as breaks (\x0c, \u2028, ...) keep line numbers
829
+ # consistent with insert/view_range. A terminating newline ends the last line
830
+ # rather than adding a blank one, so drop the empty field it produces.
831
+ lines = snippet_content.split("\n")
832
+ if lines[-1] == "":
833
+ lines.pop()
818
834
  snippet_content = "\n".join(
819
- [
820
- f"{i + start_line:6}\t{line}"
821
- for i, line in enumerate(snippet_content.split("\n"))
822
- ]
823
- )
824
- return (
825
- f"Here's the result of running `cat -n` on {snippet_description}:\n"
826
- + snippet_content
827
- + "\n"
835
+ [f"{i + start_line:6}\t{line}" for i, line in enumerate(lines)]
828
836
  )
837
+ output = f"Here's the result of running `cat -n` on {snippet_description}:\n"
838
+ if snippet_content:
839
+ output += snippet_content + "\n"
840
+ return output
@@ -1,5 +1,6 @@
1
1
  """Encoding management for file operations."""
2
2
 
3
+ import codecs
3
4
  import functools
4
5
  import inspect
5
6
  import os
@@ -22,7 +23,8 @@ BINARY_CONTROL_CHARS = re.compile(r"[\x00-\x08\x0e-\x1f\x7f]")
22
23
  # file's encoding. Short non-UTF-8 files fall below the detector's confidence
23
24
  # threshold and are reported as the default encoding, so the detected value
24
25
  # alone is not enough to recover them. Ordered most to least likely for source
25
- # files; the detector's own guess is tried first when it is one of these.
26
+ # files; the detector's own guess is tried right after UTF-8 when it is one of
27
+ # these.
26
28
  TEXT_DECODE_CANDIDATES = ("utf-8", "cp1252", "latin-1", "cp1251")
27
29
 
28
30
  # charset_normalizer reports some codecs under IANA names; map them to the
@@ -70,6 +72,16 @@ class EncodingManager:
70
72
  with open(path, "rb") as f:
71
73
  raw_data = f.read(sample_size)
72
74
 
75
+ if raw_data.startswith(codecs.BOM_UTF8):
76
+ return "utf-8-sig"
77
+
78
+ try:
79
+ raw_data.decode(self.default_encoding)
80
+ except UnicodeDecodeError:
81
+ pass
82
+ else:
83
+ return self.default_encoding
84
+
73
85
  # Use charset_normalizer instead of chardet
74
86
  results = charset_normalizer.detect(raw_data)
75
87
 
@@ -116,7 +128,11 @@ class EncodingManager:
116
128
 
117
129
  def _recover_encoding(self, path: Path) -> str | None:
118
130
  """Return a codec that decodes ``path`` to control-byte-free text."""
119
- candidates = (self._best_guess_encoding(path), *TEXT_DECODE_CANDIDATES)
131
+ candidates = (
132
+ self.default_encoding,
133
+ self._best_guess_encoding(path),
134
+ *TEXT_DECODE_CANDIDATES,
135
+ )
120
136
  for candidate in dict.fromkeys(c for c in candidates if c):
121
137
  try:
122
138
  text = path.read_text(encoding=candidate)
@@ -244,8 +244,9 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
244
244
  confirmation policy requires user approval. Receives
245
245
  `(task_id, pending_actions)` and must return `True` to
246
246
  approve or `False` to reject.
247
- sub_agent_scope: Which of this agent's capabilities the sub-agents
248
- it offers and starts must stay within.
247
+ sub_agent_scope: Whether the sub-agents it offers and starts must
248
+ stay within this agent's tools. Sub-agents always inherit this
249
+ agent's MCP servers.
249
250
 
250
251
  Returns:
251
252
  List containing a single TaskTool.
@@ -264,8 +265,7 @@ class TaskToolSet(ToolDefinition[TaskAction, TaskObservation]):
264
265
  agent_types_info = get_factory_info(lambda d: d.name in registered)
265
266
  else:
266
267
  agent_types_info = (
267
- "- None: every registered agent uses tools or MCP servers "
268
- "this agent does not have."
268
+ "- None: every registered agent uses tools this agent does not have."
269
269
  )
270
270
 
271
271
  task_tool_examples = "\n".join(
@@ -35,7 +35,11 @@ from openhands.sdk.logger import get_logger
35
35
  from openhands.sdk.observability.laminar import detached_delegate_context
36
36
  from openhands.sdk.security import ConfirmationPolicyBase
37
37
  from openhands.sdk.subagent.registry import AgentFactory, get_agent_factory
38
- from openhands.sdk.subagent.scope import SubAgentScope, scope_delegation_tools
38
+ from openhands.sdk.subagent.scope import (
39
+ SubAgentScope,
40
+ inherit_mcp_config,
41
+ scope_delegation_tools,
42
+ )
39
43
 
40
44
 
41
45
  if TYPE_CHECKING:
@@ -225,6 +229,8 @@ class TaskManager:
225
229
  _parent_llm_call_context=(
226
230
  self.parent_conversation.get_llm_call_context()
227
231
  ),
232
+ mcp_tool_provider=self.parent_conversation._mcp_tool_provider,
233
+ _mcp_config_from_parent=True,
228
234
  delete_on_close=True,
229
235
  observability_metadata=self._delegate_observability_metadata(
230
236
  task_id=resume, subagent_type=subagent_type, link=link
@@ -328,6 +334,8 @@ class TaskManager:
328
334
  max_budget_per_run=max_budget_per_run,
329
335
  hook_config=hook_config,
330
336
  _parent_llm_call_context=parent.get_llm_call_context(),
337
+ mcp_tool_provider=parent._mcp_tool_provider,
338
+ _mcp_config_from_parent=True,
331
339
  delete_on_close=True,
332
340
  prompt_cache_key=str(parent.state.id),
333
341
  observability_metadata=self._delegate_observability_metadata(
@@ -391,7 +399,7 @@ class TaskManager:
391
399
  )
392
400
 
393
401
  # ensuring that the sub-agent LLM has stream deactivated
394
- sub_agent = sub_agent.model_copy(
402
+ sub_agent = inherit_mcp_config(parent.agent, sub_agent).model_copy(
395
403
  update={
396
404
  "llm": sub_agent.llm.model_copy(update={"stream": False}),
397
405
  "tools": scope_delegation_tools(sub_agent.tools, self._sub_agent_scope),
@@ -1,9 +1,9 @@
1
1
  import json
2
2
  from collections.abc import Sequence
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, ClassVar, Literal
4
+ from typing import TYPE_CHECKING, ClassVar, Literal, get_args
5
5
 
6
- from pydantic import BaseModel, Field, ValidationError
6
+ from pydantic import BaseModel, Field, ValidationError, field_validator
7
7
 
8
8
 
9
9
  if TYPE_CHECKING:
@@ -28,6 +28,14 @@ logger = get_logger(__name__)
28
28
  # Type alias for task tracker status
29
29
  TaskTrackerStatusType = Literal["todo", "in_progress", "done"]
30
30
 
31
+ # Models trained on other agents' todo tools send these names for our statuses.
32
+ _STATUS_ALIASES: dict[str, TaskTrackerStatusType] = {
33
+ "pending": "todo",
34
+ "to_do": "todo",
35
+ "completed": "done",
36
+ "complete": "done",
37
+ }
38
+
31
39
 
32
40
  class TaskItem(BaseModel):
33
41
  title: str = Field(..., description="A brief title for the task.")
@@ -38,6 +46,16 @@ class TaskItem(BaseModel):
38
46
  "One of 'todo', 'in_progress', or 'done'.",
39
47
  )
40
48
 
49
+ @field_validator("status", mode="before")
50
+ @classmethod
51
+ def _normalize_status(cls, value: object) -> object:
52
+ if not isinstance(value, str):
53
+ return value
54
+ normalized = value.strip().lower().replace("-", "_").replace(" ", "_")
55
+ if normalized in get_args(TaskTrackerStatusType):
56
+ return normalized
57
+ return _STATUS_ALIASES.get(normalized, value)
58
+
41
59
 
42
60
  class TaskTrackerAction(Action):
43
61
  """An action where the agent writes or updates a task list for task management."""
@@ -406,6 +424,7 @@ class TaskTrackerTool(ToolDefinition[TaskTrackerAction, TaskTrackerObservation])
406
424
  catalog_description: ClassVar[str] = (
407
425
  "Keep a running task list to organise multi-step work."
408
426
  )
427
+ conversation_local: ClassVar[bool] = True
409
428
 
410
429
  @classmethod
411
430
  def create(cls, conv_state: "ConversationState") -> Sequence["TaskTrackerTool"]:
@@ -30,12 +30,26 @@ TIMEOUT_MESSAGE_TEMPLATE: Final[str] = (
30
30
  # How long to wait with no new output before considering it a no-change timeout
31
31
  NO_CHANGE_TIMEOUT_SECONDS: Final[int] = 30
32
32
 
33
- # How often to poll for new output in seconds
33
+ # How often to poll for new output in seconds, once a command has run for a while
34
34
  POLL_INTERVAL: Final[float] = 0.5
35
+ # Shortest poll interval, used right after a command is sent
36
+ MIN_POLL_INTERVAL: Final[float] = 0.02
35
37
  HISTORY_LIMIT: Final[int] = 10_000
36
38
 
37
39
  TMUX_SOCKET_NAME: Final[str] = "openhands"
38
40
 
41
+ # tmux user option recording the PID of the process that created a socket.
42
+ # Startup cleanup reads it so it does not kill the live sessions of a sibling
43
+ # agent-server that shares one externally supplied TMUX_TMPDIR.
44
+ TMUX_SOCKET_OWNER_OPTION: Final[str] = "@openhands_socket_owner_pid"
45
+
46
+ # Cross-process lock file, inside the tmux socket directory, that serializes
47
+ # socket creation with the agent-server's startup cleanup. A socket only exists
48
+ # between creation and the recording of its owner, so both operations take this
49
+ # lock to keep cleanup from reclaiming a socket that a live process is still
50
+ # setting up.
51
+ TMUX_SOCKET_CREATION_LOCK: Final[str] = ".openhands-socket-creation.lock"
52
+
39
53
  # Tmux session dimensions (columns x rows). Keep the viewport wide enough for
40
54
  # common command output while leaving scrollback retention to HISTORY_LIMIT.
41
55
  # Output wider than TMUX_SESSION_WIDTH columns will wrap; this is an accepted