mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
This commit is contained in:
parent
a7b234d1f6
commit
b06a3bdb7c
@ -28,6 +28,7 @@ from openhands.core.config import (
|
||||
OpenHandsConfig,
|
||||
)
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.core.schema.exit_reason import ExitReason
|
||||
from openhands.events import EventSource
|
||||
from openhands.events.action import (
|
||||
ChangeAgentStateAction,
|
||||
@ -45,10 +46,11 @@ async def handle_commands(
|
||||
config: OpenHandsConfig,
|
||||
current_dir: str,
|
||||
settings_store: FileSettingsStore,
|
||||
) -> tuple[bool, bool, bool]:
|
||||
) -> tuple[bool, bool, bool, ExitReason]:
|
||||
close_repl = False
|
||||
reload_microagents = False
|
||||
new_session_requested = False
|
||||
exit_reason = ExitReason.ERROR
|
||||
|
||||
if command == '/exit':
|
||||
close_repl = handle_exit_command(
|
||||
@ -57,6 +59,8 @@ async def handle_commands(
|
||||
usage_metrics,
|
||||
sid,
|
||||
)
|
||||
if close_repl:
|
||||
exit_reason = ExitReason.INTENTIONAL
|
||||
elif command == '/help':
|
||||
handle_help_command()
|
||||
elif command == '/init':
|
||||
@ -69,6 +73,8 @@ async def handle_commands(
|
||||
close_repl, new_session_requested = handle_new_command(
|
||||
config, event_stream, usage_metrics, sid
|
||||
)
|
||||
if close_repl:
|
||||
exit_reason = ExitReason.INTENTIONAL
|
||||
elif command == '/settings':
|
||||
await handle_settings_command(config, settings_store)
|
||||
elif command == '/resume':
|
||||
@ -78,7 +84,7 @@ async def handle_commands(
|
||||
action = MessageAction(content=command)
|
||||
event_stream.add_event(action, EventSource.USER)
|
||||
|
||||
return close_repl, reload_microagents, new_session_requested
|
||||
return close_repl, reload_microagents, new_session_requested, exit_reason
|
||||
|
||||
|
||||
def handle_exit_command(
|
||||
|
||||
@ -43,6 +43,7 @@ from openhands.core.config.mcp_config import OpenHandsMCPConfigImpl
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.loop import run_agent_until_done
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.core.schema.exit_reason import ExitReason
|
||||
from openhands.core.setup import (
|
||||
create_agent,
|
||||
create_controller,
|
||||
@ -116,6 +117,7 @@ async def run_session(
|
||||
) -> bool:
|
||||
reload_microagents = False
|
||||
new_session_requested = False
|
||||
exit_reason = ExitReason.INTENTIONAL
|
||||
|
||||
sid = generate_sid(config, session_name)
|
||||
is_loaded = asyncio.Event()
|
||||
@ -152,7 +154,7 @@ async def run_session(
|
||||
usage_metrics = UsageMetrics()
|
||||
|
||||
async def prompt_for_next_task(agent_state: str) -> None:
|
||||
nonlocal reload_microagents, new_session_requested
|
||||
nonlocal reload_microagents, new_session_requested, exit_reason
|
||||
while True:
|
||||
next_message = await read_prompt_input(
|
||||
config, agent_state, multiline=config.cli_multiline_input
|
||||
@ -165,6 +167,7 @@ async def run_session(
|
||||
close_repl,
|
||||
reload_microagents,
|
||||
new_session_requested,
|
||||
exit_reason,
|
||||
) = await handle_commands(
|
||||
next_message,
|
||||
event_stream,
|
||||
@ -330,6 +333,11 @@ async def run_session(
|
||||
|
||||
await cleanup_session(loop, agent, runtime, controller)
|
||||
|
||||
if exit_reason == ExitReason.INTENTIONAL:
|
||||
print_formatted_text('✅ Session terminated successfully.\n')
|
||||
else:
|
||||
print_formatted_text(f'⚠️ Session was interrupted: {exit_reason.value}\n')
|
||||
|
||||
return new_session_requested
|
||||
|
||||
|
||||
@ -478,7 +486,7 @@ def main():
|
||||
try:
|
||||
loop.run_until_complete(main_with_loop(loop))
|
||||
except KeyboardInterrupt:
|
||||
print('Received keyboard interrupt, shutting down...')
|
||||
print_formatted_text('⚠️ Session was interrupted: interrupted\n')
|
||||
except ConnectionRefusedError as e:
|
||||
print(f'Connection refused: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
7
openhands/core/schema/exit_reason.py
Normal file
7
openhands/core/schema/exit_reason.py
Normal file
@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ExitReason(Enum):
|
||||
INTENTIONAL = 'intentional'
|
||||
INTERRUPTED = 'interrupted'
|
||||
ERROR = 'error'
|
||||
@ -45,7 +45,7 @@ class TestHandleCommands:
|
||||
async def test_handle_exit_command(self, mock_handle_exit, mock_dependencies):
|
||||
mock_handle_exit.return_value = True
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/exit', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -64,7 +64,7 @@ class TestHandleCommands:
|
||||
async def test_handle_help_command(self, mock_handle_help, mock_dependencies):
|
||||
mock_handle_help.return_value = (False, False, False)
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/help', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -78,7 +78,7 @@ class TestHandleCommands:
|
||||
async def test_handle_init_command(self, mock_handle_init, mock_dependencies):
|
||||
mock_handle_init.return_value = (True, True)
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/init', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -96,7 +96,7 @@ class TestHandleCommands:
|
||||
async def test_handle_status_command(self, mock_handle_status, mock_dependencies):
|
||||
mock_handle_status.return_value = (False, False, False)
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/status', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -112,7 +112,7 @@ class TestHandleCommands:
|
||||
async def test_handle_new_command(self, mock_handle_new, mock_dependencies):
|
||||
mock_handle_new.return_value = (True, True)
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/new', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -131,7 +131,7 @@ class TestHandleCommands:
|
||||
async def test_handle_settings_command(
|
||||
self, mock_handle_settings, mock_dependencies
|
||||
):
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
'/settings', **mock_dependencies
|
||||
)
|
||||
|
||||
@ -147,7 +147,7 @@ class TestHandleCommands:
|
||||
async def test_handle_unknown_command(self, mock_dependencies):
|
||||
user_message = 'Hello, this is not a command'
|
||||
|
||||
close_repl, reload_microagents, new_session = await handle_commands(
|
||||
close_repl, reload_microagents, new_session, _ = await handle_commands(
|
||||
user_message, **mock_dependencies
|
||||
)
|
||||
|
||||
|
||||
@ -253,7 +253,12 @@ class TestCliCommandsPauseResume:
|
||||
mock_handle_resume.return_value = (False, False)
|
||||
|
||||
# Call handle_commands
|
||||
close_repl, reload_microagents, new_session_requested = await handle_commands(
|
||||
(
|
||||
close_repl,
|
||||
reload_microagents,
|
||||
new_session_requested,
|
||||
_,
|
||||
) = await handle_commands(
|
||||
message,
|
||||
event_stream,
|
||||
usage_metrics,
|
||||
|
||||
56
tests/unit/test_exit_reason.py
Normal file
56
tests/unit/test_exit_reason.py
Normal file
@ -0,0 +1,56 @@
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.cli.commands import handle_commands
|
||||
from openhands.core.schema.exit_reason import ExitReason
|
||||
|
||||
|
||||
def test_exit_reason_enum_values():
|
||||
assert ExitReason.INTENTIONAL.value == 'intentional'
|
||||
assert ExitReason.INTERRUPTED.value == 'interrupted'
|
||||
assert ExitReason.ERROR.value == 'error'
|
||||
|
||||
|
||||
def test_exit_reason_enum_names():
|
||||
assert ExitReason['INTENTIONAL'] == ExitReason.INTENTIONAL
|
||||
assert ExitReason['INTERRUPTED'] == ExitReason.INTERRUPTED
|
||||
assert ExitReason['ERROR'] == ExitReason.ERROR
|
||||
|
||||
|
||||
def test_exit_reason_str_representation():
|
||||
assert str(ExitReason.INTENTIONAL) == 'ExitReason.INTENTIONAL'
|
||||
assert repr(ExitReason.ERROR) == "<ExitReason.ERROR: 'error'>"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_exit_command_returns_intentional(monkeypatch):
|
||||
monkeypatch.setattr('openhands.cli.commands.cli_confirm', lambda *a, **k: 0)
|
||||
|
||||
mock_usage_metrics = MagicMock()
|
||||
mock_usage_metrics.session_init_time = time.time() - 3600
|
||||
mock_usage_metrics.metrics.accumulated_cost = 0.123456
|
||||
|
||||
# Mock all token counts used in display formatting
|
||||
mock_usage_metrics.metrics.accumulated_token_usage.prompt_tokens = 1234
|
||||
mock_usage_metrics.metrics.accumulated_token_usage.cache_read_tokens = 5678
|
||||
mock_usage_metrics.metrics.accumulated_token_usage.cache_write_tokens = 9012
|
||||
mock_usage_metrics.metrics.accumulated_token_usage.completion_tokens = 3456
|
||||
|
||||
(
|
||||
close_repl,
|
||||
reload_microagents,
|
||||
new_session_requested,
|
||||
exit_reason,
|
||||
) = await handle_commands(
|
||||
'/exit',
|
||||
MagicMock(),
|
||||
mock_usage_metrics,
|
||||
'test-session',
|
||||
MagicMock(),
|
||||
'/tmp/test',
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
assert exit_reason == ExitReason.INTENTIONAL
|
||||
Loading…
x
Reference in New Issue
Block a user