diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index d63e4a2891..61a56a1a67 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -93,6 +93,15 @@ def response_to_actions( is_input = arguments.get('is_input', 'false') == 'true' action = CmdRunAction(command=arguments['command'], is_input=is_input) + # Set hard timeout if provided + if 'timeout' in arguments: + try: + action.set_hard_timeout(float(arguments['timeout'])) + except ValueError as e: + raise FunctionCallValidationError( + f"Invalid float passed to 'timeout' argument: {arguments['timeout']}" + ) from e + # ================================================ # IPythonTool (Jupyter) # ================================================ diff --git a/openhands/agenthub/codeact_agent/tools/bash.py b/openhands/agenthub/codeact_agent/tools/bash.py index fac350c981..1152cf9418 100644 --- a/openhands/agenthub/codeact_agent/tools/bash.py +++ b/openhands/agenthub/codeact_agent/tools/bash.py @@ -7,10 +7,10 @@ _DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a ### Command Execution * One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together. * Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands. -* Timeout: Commands have a soft timeout of 120 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details) +* Timeout: Commands have a soft timeout of 10 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details) ### Running and Interacting with Processes -* Long running commands: For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`. +* Long running commands: For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`. For commands that need to run for a specific duration, like "sleep", you can set the "timeout" argument to specify a hard timeout in seconds. * Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, you can: - Send empty `command` to retrieve additional logs - Send text (set `command` to the text) to STDIN of the running process @@ -25,7 +25,7 @@ _DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a """ _SHORT_BASH_DESCRIPTION = """Execute a bash command in the terminal. -* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. +* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. For commands that need to run for a specific duration, you can set the "timeout" argument to specify a hard timeout in seconds. * Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process. * One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.""" @@ -63,6 +63,10 @@ def create_cmd_run_tool( ), 'enum': ['true', 'false'], }, + 'timeout': { + 'type': 'number', + 'description': 'Optional. Sets a hard timeout in seconds for the command execution. If not provided, the command will use the default soft timeout behavior.', + }, }, 'required': ['command'], }, diff --git a/tests/unit/test_function_calling.py b/tests/unit/test_function_calling.py index 5d0ba9b98c..ae19b619e8 100644 --- a/tests/unit/test_function_calling.py +++ b/tests/unit/test_function_calling.py @@ -1,6 +1,7 @@ """Test function calling module.""" import json +from unittest.mock import patch import pytest from litellm import ModelResponse @@ -56,6 +57,21 @@ def test_execute_bash_valid(): assert actions[0].command == 'ls' assert actions[0].is_input is False + # Test with timeout parameter + with patch.object(CmdRunAction, 'set_hard_timeout') as mock_set_hard_timeout: + response_with_timeout = create_mock_response( + 'execute_bash', {'command': 'ls', 'is_input': 'false', 'timeout': 30} + ) + actions_with_timeout = response_to_actions(response_with_timeout) + + # Verify set_hard_timeout was called with the correct value + mock_set_hard_timeout.assert_called_once_with(30.0) + + assert len(actions_with_timeout) == 1 + assert isinstance(actions_with_timeout[0], CmdRunAction) + assert actions_with_timeout[0].command == 'ls' + assert actions_with_timeout[0].is_input is False + def test_execute_bash_missing_command(): """Test execute_bash with missing command argument."""