From 727748cda0b9d046214e341672a5f1f34f39d8cb Mon Sep 17 00:00:00 2001 From: Guy Elsmore-Paddock Date: Fri, 26 Dec 2025 01:45:28 -0500 Subject: [PATCH] Clarify awaiting-input handling --- openhands/events/observation/commands.py | 3 +++ openhands/runtime/utils/bash.py | 14 +++++++++-- tests/unit/events/test_command_success.py | 12 ++++++++++ .../utils/test_bash_blocking_behavior.py | 24 +++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/openhands/events/observation/commands.py b/openhands/events/observation/commands.py index 6784d997fa53..ad9ee82ebabf 100644 --- a/openhands/events/observation/commands.py +++ b/openhands/events/observation/commands.py @@ -36,6 +36,7 @@ class CmdOutputMetadata(BaseModel): py_interpreter_path: str | None = None prefix: str = '' # Prefix to add to command output suffix: str = '' # Suffix to add to command output + awaiting_input: bool = False # True when the process is still running and waiting for input @classmethod def to_ps1_prompt(cls) -> str: @@ -179,6 +180,8 @@ def exit_code(self) -> int: @property def error(self) -> bool: + if getattr(self.metadata, 'awaiting_input', False): + return False return self.exit_code != 0 @property diff --git a/openhands/runtime/utils/bash.py b/openhands/runtime/utils/bash.py index 3260d35c048d..7329ebe1741d 100644 --- a/openhands/runtime/utils/bash.py +++ b/openhands/runtime/utils/bash.py @@ -161,7 +161,7 @@ def _finalize_completed( def _finalize_no_output(self, command: str, pane: str) -> CmdOutputObservation: """Run no-output handler and update state like the original loop.""" obs = self._handle_no_output(command, pane) - self.state.state = RunState.NO_OUTPUT_TIMEOUT + self.state.state = RunState.RUNNING return obs def _finalize_hard_timeout(self, command: str, pane: str, @@ -532,7 +532,13 @@ def _handle_no_output(self, command: str, pane: str) -> CmdOutputObservation: trimmed_content = output_parser.remove_command_prefix(active_pane_content, command) meta = CmdOutputMetadata() - meta.suffix = f"[No output for timeout. {TIMEOUT_MESSAGE_TEMPLATE}]" + meta.awaiting_input = True + meta.suffix = ( + "[The process is still running and awaiting input. " + "Send follow-up input with `is_input=true` (e.g., responses to prompts or " + "`C-c`/`C-d`/`C-z`). If the shell is stuck, run the reset command " + f"`{RESET_SESSION_COMMAND}`: {TIMEOUT_MESSAGE_TEMPLATE}]" + ) return CmdOutputObservation(content=trimmed_content, command=command, metadata=meta) def _handle_hard_timeout(self, command: str, pane: str, timeout: float) -> CmdOutputObservation: @@ -541,6 +547,7 @@ def _handle_hard_timeout(self, command: str, pane: str, timeout: float) -> CmdOu trimmed_content = output_parser.remove_command_prefix(active_pane_content, command) meta = CmdOutputMetadata() + meta.awaiting_input = False meta.suffix = f"[Command timed out after {timeout} seconds. {TIMEOUT_MESSAGE_TEMPLATE}]" return CmdOutputObservation(content=trimmed_content, command=command, metadata=meta) @@ -568,6 +575,9 @@ def _finalize_successful_completion( self.state.state = RunState.COMPLETED + # A command that completed should no longer be treated as awaiting input. + meta.awaiting_input = False + return CmdOutputObservation( content=output.rstrip(), command=command, diff --git a/tests/unit/events/test_command_success.py b/tests/unit/events/test_command_success.py index 298a3bcb4f6c..5a5cf6e04649 100644 --- a/tests/unit/events/test_command_success.py +++ b/tests/unit/events/test_command_success.py @@ -25,6 +25,18 @@ def test_cmd_output_success(): assert obs.error is True +def test_cmd_output_awaiting_input_is_not_error(): + obs = CmdOutputObservation( + command='git add -p', + content='', + metadata=CmdOutputMetadata(awaiting_input=True), + ) + + assert obs.error is False + assert obs.success is True + assert obs.exit_code == -1 + + def test_ipython_cell_success(): # IPython cells are always successful obs = IPythonRunCellObservation(code='print("Hello")', content='Hello') diff --git a/tests/unit/runtime/utils/test_bash_blocking_behavior.py b/tests/unit/runtime/utils/test_bash_blocking_behavior.py index 697de7f3e0a2..a4b4392ca904 100644 --- a/tests/unit/runtime/utils/test_bash_blocking_behavior.py +++ b/tests/unit/runtime/utils/test_bash_blocking_behavior.py @@ -12,6 +12,7 @@ def _session_with_mock_tmux(tmp_path): session = BashSession(work_dir=str(tmp_path)) session.tmux = Mock() session.tmux.capture.return_value = "pane" + session.tmux.clear = Mock() return session @@ -30,6 +31,29 @@ def test_blocking_after_timeout_states(tmp_path): assert "NOT executed" in blocked.metadata.suffix +def test_no_output_flagged_as_awaiting_input(tmp_path): + session = _session_with_mock_tmux(tmp_path) + session.state.last_command = "git add -p" + + obs = session._finalize_no_output("git add -p", "prompt: ...") + + assert session.state.state == RunState.RUNNING + assert obs.metadata.awaiting_input is True + assert obs.error is False + assert "awaiting input" in obs.metadata.suffix + + +def test_completion_resets_awaiting_input_flag(tmp_path): + session = _session_with_mock_tmux(tmp_path) + + meta = CmdOutputMetadata(awaiting_input=True, exit_code=0) + obs = session._finalize_successful_completion("echo hi", "output", meta) + + assert session.state.state == RunState.COMPLETED + assert obs.metadata.awaiting_input is False + assert obs.metadata.exit_code == 0 + + def test_reset_request_resets_session(tmp_path, monkeypatch): session = BashSession(work_dir=str(tmp_path)) session.tmux = Mock()