openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

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.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. openhands-0.0.0.dist-info/top_level.txt +0 -1
@@ -1,412 +0,0 @@
1
- """PTY-based terminal backend implementation (replaces pipe-based subprocess)."""
2
-
3
- import fcntl
4
- import os
5
- import pty
6
- import re
7
- import select
8
- import signal
9
- import subprocess
10
- import threading
11
- import time
12
- import uuid
13
- from collections import deque
14
- from typing import Deque
15
-
16
- from openhands.sdk.logger import get_logger
17
- from openhands.tools.execute_bash.constants import (
18
- CMD_OUTPUT_PS1_BEGIN,
19
- CMD_OUTPUT_PS1_END,
20
- HISTORY_LIMIT,
21
- )
22
- from openhands.tools.execute_bash.metadata import CmdOutputMetadata
23
- from openhands.tools.execute_bash.terminal import TerminalInterface
24
-
25
-
26
- logger = get_logger(__name__)
27
-
28
- ENTER = b"\n"
29
-
30
-
31
- def _normalize_eols(raw: bytes) -> bytes:
32
- # CRLF/LF/CR -> CR, so each logical line is terminated with \r for the TTY
33
- raw = raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
34
- return ENTER.join(raw.split(b"\n"))
35
-
36
-
37
- class SubprocessTerminal(TerminalInterface):
38
- """PTY-backed terminal backend.
39
-
40
- Creates an interactive bash in a pseudoterminal (PTY) so programs behave as if
41
- attached to a real terminal. Initialization uses a sentinel-based handshake
42
- and prompt detection instead of blind sleeps.
43
- """
44
-
45
- def __init__(
46
- self,
47
- work_dir: str,
48
- username: str | None = None,
49
- ):
50
- super().__init__(work_dir, username)
51
- self.PS1 = CmdOutputMetadata.to_ps1_prompt()
52
- self.process: subprocess.Popen | None = None
53
- self._pty_master_fd: int | None = None
54
- # Use a slightly larger buffer to match tmux behavior which seems to keep
55
- # ~10,001 lines instead of exactly 10,000
56
- self.output_buffer: Deque[str] = deque(
57
- maxlen=HISTORY_LIMIT + 50
58
- ) # Circular buffer
59
- self.output_lock = threading.Lock()
60
- self.reader_thread: threading.Thread | None = None
61
- self._current_command_running = False
62
-
63
- # ------------------------- Lifecycle -------------------------
64
-
65
- def initialize(self) -> None:
66
- """Initialize the PTY terminal session."""
67
- if self._initialized:
68
- return
69
-
70
- env = os.environ.copy()
71
- env["PS1"] = self.PS1
72
- env["PS2"] = ""
73
- env["TERM"] = "xterm-256color"
74
-
75
- bash_cmd = ["/bin/bash", "-i"]
76
-
77
- # Create a PTY; give the slave to the child, keep the master
78
- master_fd, slave_fd = pty.openpty()
79
-
80
- logger.debug("Initializing PTY terminal with: %s", " ".join(bash_cmd))
81
- try:
82
- self.process = subprocess.Popen(
83
- bash_cmd,
84
- stdin=slave_fd,
85
- stdout=slave_fd,
86
- stderr=slave_fd,
87
- cwd=self.work_dir,
88
- env=env,
89
- text=False, # bytes I/O
90
- bufsize=0,
91
- preexec_fn=os.setsid, # new process group for signal handling
92
- close_fds=True,
93
- )
94
- finally:
95
- # Parent must close its copy of the slave FD
96
- try:
97
- os.close(slave_fd)
98
- except Exception:
99
- pass
100
-
101
- self._pty_master_fd = master_fd
102
-
103
- # Set master FD non-blocking
104
- flags = fcntl.fcntl(self._pty_master_fd, fcntl.F_GETFL)
105
- fcntl.fcntl(self._pty_master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
106
-
107
- # Start output reader thread
108
- self.reader_thread = threading.Thread(
109
- target=self._read_output_continuously_pty, daemon=True
110
- )
111
- self.reader_thread.start()
112
- self._initialized = True
113
-
114
- # ===== Deterministic readiness (no blind sleeps) =====
115
- # 1) Single atomic init line: clear PROMPT_COMMAND, set PS2/PS1, print sentinel
116
- sentinel = f"__OH_READY_{uuid.uuid4().hex}__"
117
- init_cmd = (
118
- f"export PROMPT_COMMAND='export PS1=\"{self.PS1}\"'; "
119
- f'export PS2=""; '
120
- f'printf "{sentinel}"'
121
- ).encode("utf-8", "ignore")
122
-
123
- self._write_pty(init_cmd + ENTER)
124
- if not self._wait_for_output(sentinel, timeout=8.0):
125
- raise RuntimeError("Shell did not become ready (sentinel not seen)")
126
-
127
- self.clear_screen()
128
-
129
- # 3) Wait for prompt to actually be visible
130
- if not self._wait_for_prompt(timeout=5.0):
131
- raise RuntimeError("Prompt not visible after init")
132
-
133
- logger.debug("PTY terminal initialized with work dir: %s", self.work_dir)
134
-
135
- def close(self) -> None:
136
- """Clean up the PTY terminal."""
137
- if self._closed:
138
- return
139
-
140
- try:
141
- if self.process:
142
- # Try a graceful exit
143
- try:
144
- self._write_pty(b"exit\n")
145
- except Exception:
146
- pass
147
- try:
148
- self.process.wait(timeout=2)
149
- except subprocess.TimeoutExpired:
150
- # Escalate
151
- try:
152
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
153
- self.process.wait(timeout=1)
154
- except subprocess.TimeoutExpired:
155
- os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
156
- except Exception as e:
157
- logger.error(f"Error closing PTY terminal: {e}", exc_info=True)
158
- finally:
159
- # Reader thread stop: close master FD; thread exits on read error/EOF
160
- try:
161
- if self._pty_master_fd is not None:
162
- os.close(self._pty_master_fd)
163
- except Exception:
164
- pass
165
- self._pty_master_fd = None
166
-
167
- if self.reader_thread and self.reader_thread.is_alive():
168
- self.reader_thread.join(timeout=1)
169
-
170
- self.process = None
171
- self._closed = True
172
-
173
- # ------------------------- I/O Core -------------------------
174
-
175
- def _write_pty(self, data: bytes) -> None:
176
- if not self._initialized and self._pty_master_fd is None:
177
- # allow init path to call before _initialized flips
178
- raise RuntimeError("PTY master FD not ready")
179
- if self._pty_master_fd is None:
180
- raise RuntimeError("PTY terminal is not initialized")
181
- try:
182
- logger.debug(f"Wrote to subprocess PTY: {data!r}")
183
- os.write(self._pty_master_fd, data)
184
- except Exception as e:
185
- logger.error(f"Failed to write to PTY: {e}", exc_info=True)
186
- raise
187
-
188
- def _read_output_continuously_pty(self) -> None:
189
- """Continuously read output from the PTY master in a separate thread."""
190
- fd = self._pty_master_fd
191
- if fd is None:
192
- return
193
-
194
- try:
195
- while True:
196
- # Exit early if process died
197
- if self.process and self.process.poll() is not None:
198
- break
199
-
200
- # Use select to avoid busy spin
201
- r, _, _ = select.select([fd], [], [], 0.1)
202
- if not r:
203
- continue
204
-
205
- try:
206
- chunk = os.read(fd, 4096)
207
- if not chunk:
208
- break # EOF
209
- # Normalize newlines; PTY typically uses \n already
210
- text = chunk.decode("utf-8", errors="replace")
211
- with self.output_lock:
212
- # Store one line per buffer item to make deque truncation work
213
- self._add_text_to_buffer(text)
214
- except OSError:
215
- # Would-block or FD closed
216
- continue
217
- except Exception as e:
218
- logger.debug(f"Error reading PTY output: {e}")
219
- break
220
- except Exception as e:
221
- logger.error(f"PTY reader thread error: {e}", exc_info=True)
222
-
223
- def _add_text_to_buffer(self, text: str) -> None:
224
- """Add text to buffer, ensuring one line per buffer item."""
225
- # If there's a partial line in the last buffer item, combine with new text
226
- if self.output_buffer and not self.output_buffer[-1].endswith("\n"):
227
- combined_text = self.output_buffer[-1] + text
228
- self.output_buffer.pop() # Remove the partial line
229
- else:
230
- combined_text = text
231
-
232
- # Split into lines and add each line as a separate buffer item
233
- lines = combined_text.split("\n")
234
-
235
- # Add all complete lines (all but the last, which might be partial)
236
- for line in lines[:-1]:
237
- self.output_buffer.append(line + "\n")
238
-
239
- # Add the last part (might be partial line)
240
- if lines[-1]: # Only add if not empty
241
- self.output_buffer.append(lines[-1])
242
-
243
- # ------------------------- Readiness Helpers -------------------------
244
-
245
- def _wait_for_output(self, pattern: str | re.Pattern, timeout: float = 5.0) -> bool:
246
- """Wait until the output buffer contains pattern (regex or literal)."""
247
- deadline = time.time() + timeout
248
- is_regex = hasattr(pattern, "search")
249
- while time.time() < deadline:
250
- # quick yield to reader thread
251
- if self._pty_master_fd is not None:
252
- select.select([], [], [], 0.02)
253
- with self.output_lock:
254
- data = "".join(self.output_buffer)
255
- if is_regex:
256
- assert isinstance(pattern, re.Pattern)
257
- if pattern.search(data):
258
- return True
259
- else:
260
- assert isinstance(pattern, str)
261
- if pattern in data:
262
- return True
263
- return False
264
-
265
- def _wait_for_prompt(self, timeout: float = 5.0) -> bool:
266
- """Wait until the screen ends with our PS1 end marker (prompt visible)."""
267
- pat = re.compile(re.escape(CMD_OUTPUT_PS1_END.rstrip()) + r"\s*$")
268
- deadline = time.time() + timeout
269
- while time.time() < deadline:
270
- with self.output_lock:
271
- tail = "".join(self.output_buffer)[-4096:]
272
- if pat.search(tail):
273
- return True
274
- time.sleep(0.05)
275
- return False
276
-
277
- # ------------------------- Public API -------------------------
278
-
279
- def send_keys(self, text: str, enter: bool = True) -> None:
280
- """Send keystrokes to the PTY.
281
-
282
- Supports:
283
- - Plain text
284
- - Ctrl sequences: 'C-a'..'C-z' (Ctrl+C sends ^C byte)
285
- - Special names: 'ENTER','TAB','BS','ESC','UP','DOWN','LEFT','RIGHT',
286
- 'HOME','END','PGUP','PGDN','C-L','C-D'
287
- """
288
- if not self._initialized:
289
- raise RuntimeError("PTY terminal is not initialized")
290
-
291
- specials = {
292
- "ENTER": ENTER,
293
- "TAB": b"\t",
294
- "BS": b"\x7f", # Backspace (DEL)
295
- "ESC": b"\x1b",
296
- "UP": b"\x1b[A",
297
- "DOWN": b"\x1b[B",
298
- "RIGHT": b"\x1b[C",
299
- "LEFT": b"\x1b[D",
300
- "HOME": b"\x1b[H",
301
- "END": b"\x1b[F",
302
- "PGUP": b"\x1b[5~",
303
- "PGDN": b"\x1b[6~",
304
- "C-L": b"\x0c", # Ctrl+L
305
- "C-D": b"\x04", # Ctrl+D (EOF)
306
- }
307
-
308
- upper = text.upper().strip()
309
- payload: bytes | None = None
310
-
311
- # Named specials
312
- if upper in specials:
313
- payload = specials[upper]
314
- # Do NOT auto-append another EOL; special already includes it when needed.
315
- append_eol = False
316
- # Generic Ctrl-<letter>, including C-C (preferred over sending SIGINT directly)
317
- elif upper.startswith(("C-", "CTRL-", "CTRL+")):
318
- # last char after dash/plus is the key
319
- key = upper.split("-", 1)[-1].split("+", 1)[-1]
320
- if len(key) == 1 and "A" <= key <= "Z":
321
- payload = bytes([ord(key) & 0x1F])
322
- else:
323
- # Unknown form; fall back to raw text
324
- payload = text.encode("utf-8", "ignore")
325
- append_eol = False # ctrl combos are “instant”
326
- else:
327
- raw = text.encode("utf-8", "ignore")
328
- payload = _normalize_eols(raw) if enter else raw
329
- append_eol = enter and not payload.endswith(ENTER)
330
-
331
- if append_eol:
332
- payload += ENTER
333
-
334
- self._write_pty(payload)
335
- self._current_command_running = self._current_command_running or (
336
- append_eol or payload.endswith(ENTER)
337
- )
338
-
339
- def read_screen(self) -> str:
340
- """Read the current terminal screen content.
341
-
342
- The content we return should NOT contains carriage returns (CR, \r).
343
- """
344
- if not self._initialized:
345
- raise RuntimeError("PTY terminal is not initialized")
346
-
347
- # Give the reader thread a moment to capture any pending output
348
- # This is especially important after sending a command
349
- time.sleep(0.01)
350
-
351
- with self.output_lock:
352
- content = "".join(self.output_buffer)
353
- lines = content.split("\n")
354
- content = "\n".join(lines).replace("\r", "")
355
- logger.debug(f"Read from subprocess PTY: {content!r}")
356
- return content
357
-
358
- def clear_screen(self) -> None:
359
- """Drop buffered output up to the most recent PS1 block; do not emit ^L."""
360
- if not self._initialized:
361
- return
362
-
363
- need_prompt_nudge = False
364
- with self.output_lock:
365
- if not self.output_buffer:
366
- need_prompt_nudge = True
367
- else:
368
- data = "".join(self.output_buffer)
369
- start_idx = data.rfind(CMD_OUTPUT_PS1_BEGIN)
370
- end_idx = data.rfind(CMD_OUTPUT_PS1_END)
371
- if start_idx != -1 and end_idx != -1 and end_idx >= start_idx:
372
- tail = data[start_idx:]
373
- self.output_buffer.clear()
374
- self.output_buffer.append(tail)
375
- else:
376
- self.output_buffer.clear()
377
- need_prompt_nudge = True
378
-
379
- if need_prompt_nudge:
380
- try:
381
- self._write_pty(ENTER) # ask bash to render a prompt, no screen clear
382
- except Exception:
383
- pass
384
-
385
- def interrupt(self) -> bool:
386
- """Send SIGINT to the PTY process group (fallback to signal-based interrupt)."""
387
- if not self._initialized or not self.process:
388
- return False
389
-
390
- try:
391
- os.killpg(os.getpgid(self.process.pid), signal.SIGINT)
392
- self._current_command_running = False
393
- return True
394
- except Exception as e:
395
- logger.error(f"Failed to interrupt subprocess: {e}", exc_info=True)
396
- return False
397
-
398
- def is_running(self) -> bool:
399
- """Heuristic: command running if not at PS1 prompt and process alive."""
400
- if not self._initialized or not self.process:
401
- return False
402
-
403
- # Check if process is still alive
404
- if self.process.poll() is not None:
405
- return False
406
-
407
- try:
408
- content = self.read_screen()
409
- # If screen ends with prompt, no command is running
410
- return not content.rstrip().endswith(CMD_OUTPUT_PS1_END.rstrip())
411
- except Exception:
412
- return self._current_command_running