All Runtime Status Codes should be in the RuntimeStatus enum (#9601)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Tim O'Farrell
2025-07-09 15:34:46 -06:00
committed by GitHub
parent 1f416f616c
commit cf276b2e96
16 changed files with 121 additions and 76 deletions

View File

@@ -5,4 +5,15 @@ export type RuntimeStatus =
| "STATUS$RUNTIME_STARTED" | "STATUS$RUNTIME_STARTED"
| "STATUS$SETTING_UP_WORKSPACE" | "STATUS$SETTING_UP_WORKSPACE"
| "STATUS$SETTING_UP_GIT_HOOKS" | "STATUS$SETTING_UP_GIT_HOOKS"
| "STATUS$READY"; | "STATUS$READY"
| "STATUS$ERROR"
| "STATUS$ERROR_RUNTIME_DISCONNECTED"
| "STATUS$ERROR_LLM_AUTHENTICATION"
| "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE"
| "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR"
| "STATUS$ERROR_LLM_OUT_OF_CREDITS"
| "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION"
| "CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE"
| "STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR"
| "STATUS$LLM_RETRY"
| "STATUS$ERROR_MEMORY";

View File

@@ -88,11 +88,15 @@ export function getStatusCode(
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") { if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
return I18nKey.CHAT_INTERFACE$STOPPED; return I18nKey.CHAT_INTERFACE$STOPPED;
} }
if (runtimeStatus === "STATUS$BUILDING_RUNTIME") { if (
return I18nKey.STATUS$BUILDING_RUNTIME; runtimeStatus &&
} !["STATUS$READY", "STATUS$RUNTIME_STARTED"].includes(runtimeStatus)
if (runtimeStatus === "STATUS$STARTING_RUNTIME") { ) {
return I18nKey.STATUS$STARTING_RUNTIME; const result = (I18nKey as { [key: string]: string })[runtimeStatus];
if (result) {
return result;
}
return runtimeStatus;
} }
if (webSocketStatus === "DISCONNECTED") { if (webSocketStatus === "DISCONNECTED") {
return I18nKey.CHAT_INTERFACE$DISCONNECTED; return I18nKey.CHAT_INTERFACE$DISCONNECTED;

View File

@@ -75,6 +75,7 @@ from openhands.events.observation import (
from openhands.events.serialization.event import truncate_content from openhands.events.serialization.event import truncate_content
from openhands.llm.llm import LLM from openhands.llm.llm import LLM
from openhands.llm.metrics import Metrics from openhands.llm.metrics import Metrics
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.storage.files import FileStore from openhands.storage.files import FileStore
# note: RESUME is only available on web GUI # note: RESUME is only available on web GUI
@@ -248,10 +249,10 @@ class AgentController:
self.state.last_error = f'{type(e).__name__}: {str(e)}' self.state.last_error = f'{type(e).__name__}: {str(e)}'
if self.status_callback is not None: if self.status_callback is not None:
err_id = '' runtime_status = RuntimeStatus.ERROR
if isinstance(e, AuthenticationError): if isinstance(e, AuthenticationError):
err_id = 'STATUS$ERROR_LLM_AUTHENTICATION' runtime_status = RuntimeStatus.ERROR_LLM_AUTHENTICATION
self.state.last_error = err_id self.state.last_error = runtime_status.value
elif isinstance( elif isinstance(
e, e,
( (
@@ -260,20 +261,20 @@ class AgentController:
APIError, APIError,
), ),
): ):
err_id = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE' runtime_status = RuntimeStatus.ERROR_LLM_SERVICE_UNAVAILABLE
self.state.last_error = err_id self.state.last_error = runtime_status.value
elif isinstance(e, InternalServerError): elif isinstance(e, InternalServerError):
err_id = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR' runtime_status = RuntimeStatus.ERROR_LLM_INTERNAL_SERVER_ERROR
self.state.last_error = err_id self.state.last_error = runtime_status.value
elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e): elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e):
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS' runtime_status = RuntimeStatus.ERROR_LLM_OUT_OF_CREDITS
self.state.last_error = err_id self.state.last_error = runtime_status.value
elif isinstance(e, ContentPolicyViolationError) or ( elif isinstance(e, ContentPolicyViolationError) or (
isinstance(e, BadRequestError) isinstance(e, BadRequestError)
and 'ContentPolicyViolationError' in str(e) and 'ContentPolicyViolationError' in str(e)
): ):
err_id = 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION' runtime_status = RuntimeStatus.ERROR_LLM_CONTENT_POLICY_VIOLATION
self.state.last_error = err_id self.state.last_error = runtime_status.value
elif isinstance(e, RateLimitError): elif isinstance(e, RateLimitError):
# Check if this is the final retry attempt # Check if this is the final retry attempt
if ( if (
@@ -283,14 +284,14 @@ class AgentController:
): ):
# All retries exhausted, set to ERROR state with a special message # All retries exhausted, set to ERROR state with a special message
self.state.last_error = ( self.state.last_error = (
'CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE' RuntimeStatus.AGENT_RATE_LIMITED_STOPPED_MESSAGE.value
) )
await self.set_agent_state_to(AgentState.ERROR) await self.set_agent_state_to(AgentState.ERROR)
else: else:
# Still retrying, set to RATE_LIMITED state # Still retrying, set to RATE_LIMITED state
await self.set_agent_state_to(AgentState.RATE_LIMITED) await self.set_agent_state_to(AgentState.RATE_LIMITED)
return return
self.status_callback('error', err_id, self.state.last_error) self.status_callback('error', runtime_status, self.state.last_error)
# Set the agent state to ERROR after storing the reason # Set the agent state to ERROR after storing the reason
await self.set_agent_state_to(AgentState.ERROR) await self.set_agent_state_to(AgentState.ERROR)

View File

@@ -5,6 +5,7 @@ from openhands.core.logger import openhands_logger as logger
from openhands.core.schema import AgentState from openhands.core.schema import AgentState
from openhands.memory.memory import Memory from openhands.memory.memory import Memory
from openhands.runtime.base import Runtime from openhands.runtime.base import Runtime
from openhands.runtime.runtime_status import RuntimeStatus
async def run_agent_until_done( async def run_agent_until_done(
@@ -19,7 +20,7 @@ async def run_agent_until_done(
Note that runtime must be connected before being passed in here. Note that runtime must be connected before being passed in here.
""" """
def status_callback(msg_type: str, msg_id: str, msg: str) -> None: def status_callback(msg_type: str, runtime_status: RuntimeStatus, msg: str) -> None:
if msg_type == 'error': if msg_type == 'error':
logger.error(msg) logger.error(msg)
if controller: if controller:

View File

@@ -23,6 +23,7 @@ from openhands.microagent import (
load_microagents_from_dir, load_microagents_from_dir,
) )
from openhands.runtime.base import Runtime from openhands.runtime.base import Runtime
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.utils.prompt import ( from openhands.utils.prompt import (
ConversationInstructions, ConversationInstructions,
RepositoryInfo, RepositoryInfo,
@@ -133,7 +134,7 @@ class Memory:
except Exception as e: except Exception as e:
error_str = f'Error: {str(e.__class__.__name__)}' error_str = f'Error: {str(e.__class__.__name__)}'
logger.error(error_str) logger.error(error_str)
self.send_error_message('STATUS$ERROR_MEMORY', error_str) self.set_runtime_status(RuntimeStatus.ERROR_MEMORY, error_str)
return return
def _on_workspace_context_recall( def _on_workspace_context_recall(
@@ -361,22 +362,24 @@ class Memory:
content=conversation_instructions or '' content=conversation_instructions or ''
) )
def send_error_message(self, message_id: str, message: str): def set_runtime_status(self, status: RuntimeStatus, message: str):
"""Sends an error message if the callback function was provided.""" """Sends an error message if the callback function was provided."""
if self.status_callback: if self.status_callback:
try: try:
if self.loop is None: if self.loop is None:
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._send_status_message('error', message_id, message), self.loop self._set_runtime_status('error', status, message), self.loop
) )
except RuntimeError as e: except (RuntimeError, KeyError) as e:
logger.error( logger.error(
f'Error sending status message: {e.__class__.__name__}', f'Error sending status message: {e.__class__.__name__}',
stack_info=False, stack_info=False,
) )
async def _send_status_message(self, msg_type: str, id: str, message: str): async def _set_runtime_status(
self, msg_type: str, runtime_status: RuntimeStatus, message: str
):
"""Sends a status message to the client.""" """Sends a status message to the client."""
if self.status_callback: if self.status_callback:
self.status_callback(msg_type, id, message) self.status_callback(msg_type, runtime_status, message)

View File

@@ -113,7 +113,7 @@ class Runtime(FileEditRuntimeMixin):
config: OpenHandsConfig config: OpenHandsConfig
initial_env_vars: dict[str, str] initial_env_vars: dict[str, str]
attach_to_existing: bool attach_to_existing: bool
status_callback: Callable[[str, str, str], None] | None status_callback: Callable[[str, RuntimeStatus, str], None] | None
runtime_status: RuntimeStatus | None runtime_status: RuntimeStatus | None
_runtime_initialized: bool = False _runtime_initialized: bool = False
@@ -124,7 +124,7 @@ class Runtime(FileEditRuntimeMixin):
sid: str = 'default', sid: str = 'default',
plugins: list[PluginRequirement] | None = None, plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None, env_vars: dict[str, str] | None = None,
status_callback: Callable[[str, str, str], None] | None = None, status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
attach_to_existing: bool = False, attach_to_existing: bool = False,
headless_mode: bool = False, headless_mode: bool = False,
user_id: str | None = None, user_id: str | None = None,
@@ -207,16 +207,13 @@ class Runtime(FileEditRuntimeMixin):
message = f'[runtime {self.sid}] {message}' message = f'[runtime {self.sid}] {message}'
getattr(logger, level)(message, stacklevel=2) getattr(logger, level)(message, stacklevel=2)
def set_runtime_status(self, runtime_status: RuntimeStatus): def set_runtime_status(
self, runtime_status: RuntimeStatus, msg: str = '', level: str = 'info'
):
"""Sends a status message if the callback function was provided.""" """Sends a status message if the callback function was provided."""
self.runtime_status = runtime_status self.runtime_status = runtime_status
if self.status_callback: if self.status_callback:
msg_id: str = runtime_status.value # type: ignore self.status_callback(level, runtime_status, msg)
self.status_callback('info', msg_id, runtime_status.message)
def send_error_message(self, message_id: str, message: str):
if self.status_callback:
self.status_callback('error', message_id, message)
# ==================================================================== # ====================================================================
@@ -344,15 +341,13 @@ class Runtime(FileEditRuntimeMixin):
else: else:
observation = await call_sync_from_async(self.run_action, event) observation = await call_sync_from_async(self.run_action, event)
except Exception as e: except Exception as e:
err_id = '' runtime_status = RuntimeStatus.ERROR
if isinstance(e, httpx.NetworkError) or isinstance( if isinstance(e, (httpx.NetworkError, AgentRuntimeDisconnectedError)):
e, AgentRuntimeDisconnectedError runtime_status = RuntimeStatus.ERROR_RUNTIME_DISCONNECTED
):
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
error_message = f'{type(e).__name__}: {str(e)}' error_message = f'{type(e).__name__}: {str(e)}'
self.log('error', f'Unexpected error while running action: {error_message}') self.log('error', f'Unexpected error while running action: {error_message}')
self.log('error', f'Problematic action: {str(event)}') self.log('error', f'Problematic action: {str(event)}')
self.send_error_message(err_id, error_message) self.set_runtime_status(runtime_status, error_message)
return return
observation._cause = event.id # type: ignore[attr-defined] observation._cause = event.id # type: ignore[attr-defined]
@@ -397,7 +392,7 @@ class Runtime(FileEditRuntimeMixin):
if self.status_callback: if self.status_callback:
self.status_callback( self.status_callback(
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...' 'info', RuntimeStatus.SETTING_UP_WORKSPACE, 'Setting up workspace...'
) )
dir_name = selected_repository.split('/')[-1] dir_name = selected_repository.split('/')[-1]
@@ -438,7 +433,7 @@ class Runtime(FileEditRuntimeMixin):
if self.status_callback: if self.status_callback:
self.status_callback( self.status_callback(
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...' 'info', RuntimeStatus.SETTING_UP_WORKSPACE, 'Setting up workspace...'
) )
# setup scripts time out after 10 minutes # setup scripts time out after 10 minutes
@@ -470,7 +465,7 @@ class Runtime(FileEditRuntimeMixin):
if self.status_callback: if self.status_callback:
self.status_callback( self.status_callback(
'info', 'STATUS$SETTING_UP_GIT_HOOKS', 'Setting up git hooks...' 'info', RuntimeStatus.SETTING_UP_GIT_HOOKS, 'Setting up git hooks...'
) )
# Ensure the git hooks directory exists # Ensure the git hooks directory exists

View File

@@ -113,7 +113,7 @@ class CLIRuntime(Runtime):
sid: str = 'default', sid: str = 'default',
plugins: list[PluginRequirement] | None = None, plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None, env_vars: dict[str, str] | None = None,
status_callback: Callable[[str, str, str], None] | None = None, status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
attach_to_existing: bool = False, attach_to_existing: bool = False,
headless_mode: bool = False, headless_mode: bool = False,
user_id: str | None = None, user_id: str | None = None,

View File

@@ -252,8 +252,8 @@ class KubernetesRuntime(ActionExecutionClient):
await call_sync_from_async(self._wait_until_ready) await call_sync_from_async(self._wait_until_ready)
except Exception as alive_error: except Exception as alive_error:
self.log('error', f'Failed to connect to runtime: {alive_error}') self.log('error', f'Failed to connect to runtime: {alive_error}')
self.send_error_message( self.set_runtime_status(
'ERROR$RUNTIME_CONNECTION', RuntimeStatus.ERROR_RUNTIME_DISCONNECTED,
f'Failed to connect to runtime: {alive_error}', f'Failed to connect to runtime: {alive_error}',
) )
raise AgentRuntimeDisconnectedError( raise AgentRuntimeDisconnectedError(

View File

@@ -134,7 +134,7 @@ class LocalRuntime(ActionExecutionClient):
sid: str = 'default', sid: str = 'default',
plugins: list[PluginRequirement] | None = None, plugins: list[PluginRequirement] | None = None,
env_vars: dict[str, str] | None = None, env_vars: dict[str, str] | None = None,
status_callback: Callable[[str, str, str], None] | None = None, status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
attach_to_existing: bool = False, attach_to_existing: bool = False,
headless_mode: bool = True, headless_mode: bool = True,
user_id: str | None = None, user_id: str | None = None,

View File

@@ -2,14 +2,23 @@ from enum import Enum
class RuntimeStatus(Enum): class RuntimeStatus(Enum):
def __init__(self, value: str, message: str): STOPPED = 'STATUS$STOPPED'
self._value_ = value BUILDING_RUNTIME = 'STATUS$BUILDING_RUNTIME'
self.message = message STARTING_RUNTIME = 'STATUS$STARTING_RUNTIME'
RUNTIME_STARTED = 'STATUS$RUNTIME_STARTED'
STOPPED = 'STATUS$STOPPED', 'Stopped' SETTING_UP_WORKSPACE = 'STATUS$SETTING_UP_WORKSPACE'
BUILDING_RUNTIME = 'STATUS$BUILDING_RUNTIME', 'Building runtime...' SETTING_UP_GIT_HOOKS = 'STATUS$SETTING_UP_GIT_HOOKS'
STARTING_RUNTIME = 'STATUS$STARTING_RUNTIME', 'Starting runtime...' READY = 'STATUS$READY'
RUNTIME_STARTED = 'STATUS$RUNTIME_STARTED', 'Runtime started...' ERROR = 'STATUS$ERROR'
SETTING_UP_WORKSPACE = 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...' ERROR_RUNTIME_DISCONNECTED = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
SETTING_UP_GIT_HOOKS = 'STATUS$SETTING_UP_GIT_HOOKS', 'Setting up git hooks...' ERROR_LLM_AUTHENTICATION = 'STATUS$ERROR_LLM_AUTHENTICATION'
READY = 'STATUS$READY', 'Ready...' ERROR_LLM_SERVICE_UNAVAILABLE = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
ERROR_LLM_INTERNAL_SERVER_ERROR = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
ERROR_LLM_OUT_OF_CREDITS = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
ERROR_LLM_CONTENT_POLICY_VIOLATION = 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION'
AGENT_RATE_LIMITED_STOPPED_MESSAGE = (
'CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE'
)
GIT_PROVIDER_AUTHENTICATION_ERROR = 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR'
LLM_RETRY = 'STATUS$LLM_RETRY'
ERROR_MEMORY = 'STATUS$ERROR_MEMORY'

View File

@@ -32,6 +32,7 @@ from openhands.integrations.service_types import (
) )
from openhands.llm.llm import LLM from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls from openhands.runtime import get_runtime_cls
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.data_models.agent_loop_info import AgentLoopInfo from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.data_models.conversation_info import ConversationInfo from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.data_models.conversation_info_result_set import ( from openhands.server.data_models.conversation_info_result_set import (
@@ -189,7 +190,7 @@ async def new_conversation(
content={ content={
'status': 'error', 'status': 'error',
'message': str(e), 'message': str(e),
'msg_id': 'STATUS$ERROR_LLM_AUTHENTICATION', 'msg_id': RuntimeStatus.ERROR_LLM_AUTHENTICATION.value,
}, },
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
) )
@@ -199,7 +200,7 @@ async def new_conversation(
content={ content={
'status': 'error', 'status': 'error',
'message': str(e), 'message': str(e),
'msg_id': 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR', 'msg_id': RuntimeStatus.GIT_PROVIDER_AUTHENTICATION_ERROR.value,
}, },
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
) )

View File

@@ -27,6 +27,7 @@ from openhands.microagent.microagent import BaseMicroagent
from openhands.runtime import get_runtime_cls from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime from openhands.runtime.base import Runtime
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.security import SecurityAnalyzer, options from openhands.security import SecurityAnalyzer, options
from openhands.storage.data_models.user_secrets import UserSecrets from openhands.storage.data_models.user_secrets import UserSecrets
from openhands.storage.files import FileStore from openhands.storage.files import FileStore
@@ -377,7 +378,7 @@ class AgentSession:
self.logger.error(f'Runtime initialization failed: {e}') self.logger.error(f'Runtime initialization failed: {e}')
if self._status_callback: if self._status_callback:
self._status_callback( self._status_callback(
'error', 'STATUS$ERROR_RUNTIME_DISCONNECTED', str(e) 'error', RuntimeStatus.ERROR_RUNTIME_DISCONNECTED, str(e)
) )
return False return False

View File

@@ -29,6 +29,7 @@ from openhands.events.observation.error import ErrorObservation
from openhands.events.serialization import event_from_dict, event_to_dict from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.events.stream import EventStreamSubscriber from openhands.events.stream import EventStreamSubscriber
from openhands.llm.llm import LLM from openhands.llm.llm import LLM
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.session.agent_session import AgentSession from openhands.server.session.agent_session import AgentSession
from openhands.server.session.conversation_init_data import ConversationInitData from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.storage.data_models.settings import Settings from openhands.storage.data_models.settings import Settings
@@ -249,9 +250,8 @@ class Session:
) )
def _notify_on_llm_retry(self, retries: int, max: int) -> None: def _notify_on_llm_retry(self, retries: int, max: int) -> None:
msg_id = 'STATUS$LLM_RETRY'
self.queue_status_message( self.queue_status_message(
'info', msg_id, f'Retrying LLM request, {retries} / {max}' 'info', RuntimeStatus.LLM_RETRY, f'Retrying LLM request, {retries} / {max}'
) )
def on_event(self, event: Event) -> None: def on_event(self, event: Event) -> None:
@@ -337,7 +337,9 @@ class Session:
"""Sends an error message to the client.""" """Sends an error message to the client."""
await self.send({'error': True, 'message': message}) await self.send({'error': True, 'message': message})
async def _send_status_message(self, msg_type: str, id: str, message: str) -> None: async def _send_status_message(
self, msg_type: str, runtime_status: RuntimeStatus, message: str
) -> None:
"""Sends a status message to the client.""" """Sends a status message to the client."""
if msg_type == 'error': if msg_type == 'error':
agent_session = self.agent_session agent_session = self.agent_session
@@ -349,11 +351,18 @@ class Session:
extra={'signal': 'agent_status_error'}, extra={'signal': 'agent_status_error'},
) )
await self.send( await self.send(
{'status_update': True, 'type': msg_type, 'id': id, 'message': message} {
'status_update': True,
'type': msg_type,
'id': runtime_status.value,
'message': message,
}
) )
def queue_status_message(self, msg_type: str, id: str, message: str) -> None: def queue_status_message(
self, msg_type: str, runtime_status: RuntimeStatus, message: str
) -> None:
"""Queues a status message to be sent asynchronously.""" """Queues a status message to be sent asynchronously."""
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._send_status_message(msg_type, id, message), self.loop self._send_status_message(msg_type, runtime_status, message), self.loop
) )

View File

@@ -44,6 +44,7 @@ from openhands.runtime.base import Runtime
from openhands.runtime.impl.action_execution.action_execution_client import ( from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient, ActionExecutionClient,
) )
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.storage.memory import InMemoryFileStore from openhands.storage.memory import InMemoryFileStore
@@ -229,12 +230,15 @@ async def test_react_to_content_policy_violation(
# Verify the status callback was called with correct parameters # Verify the status callback was called with correct parameters
mock_status_callback.assert_called_once_with( mock_status_callback.assert_called_once_with(
'error', 'error',
'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION', RuntimeStatus.ERROR_LLM_CONTENT_POLICY_VIOLATION,
'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION', RuntimeStatus.ERROR_LLM_CONTENT_POLICY_VIOLATION.value,
) )
# Verify the state was updated correctly # Verify the state was updated correctly
assert controller.state.last_error == 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION' assert (
controller.state.last_error
== RuntimeStatus.ERROR_LLM_CONTENT_POLICY_VIOLATION.value
)
assert controller.state.agent_state == AgentState.ERROR assert controller.state.agent_state == AgentState.ERROR
await controller.close() await controller.close()
@@ -829,13 +833,15 @@ async def test_notify_on_llm_retry(mock_agent, mock_event_stream, mock_status_ca
) )
def notify_on_llm_retry(attempt, max_attempts): def notify_on_llm_retry(attempt, max_attempts):
controller.status_callback('info', 'STATUS$LLM_RETRY', ANY) controller.status_callback('info', RuntimeStatus.LLM_RETRY, ANY)
# Attach the retry listener to the agent's LLM # Attach the retry listener to the agent's LLM
controller.agent.llm.retry_listener = notify_on_llm_retry controller.agent.llm.retry_listener = notify_on_llm_retry
controller.agent.llm.retry_listener(1, 2) controller.agent.llm.retry_listener(1, 2)
controller.status_callback.assert_called_once_with('info', 'STATUS$LLM_RETRY', ANY) controller.status_callback.assert_called_once_with(
'info', RuntimeStatus.LLM_RETRY, ANY
)
await controller.close() await controller.close()

View File

@@ -15,6 +15,7 @@ from openhands.integrations.service_types import (
SuggestedTask, SuggestedTask,
TaskType, TaskType,
) )
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.data_models.conversation_info import ConversationInfo from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.data_models.conversation_info_result_set import ( from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet, ConversationInfoResultSet,
@@ -405,7 +406,9 @@ async def test_new_conversation_invalid_session_api_key(provider_handler_mock):
assert 'Error authenticating with the LLM provider' in response.body.decode( assert 'Error authenticating with the LLM provider' in response.body.decode(
'utf-8' 'utf-8'
) )
assert 'STATUS$ERROR_LLM_AUTHENTICATION' in response.body.decode('utf-8') assert RuntimeStatus.ERROR_LLM_AUTHENTICATION.value in response.body.decode(
'utf-8'
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -575,7 +578,7 @@ async def test_new_conversation_with_provider_authentication_error(
assert json.loads(response.body.decode('utf-8')) == { assert json.loads(response.body.decode('utf-8')) == {
'status': 'error', 'status': 'error',
'message': 'auth error', 'message': 'auth error',
'msg_id': 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR', 'msg_id': RuntimeStatus.GIT_PROVIDER_AUTHENTICATION_ERROR.value,
} }
# Verify that verify_repo_provider was called with the repository # Verify that verify_repo_provider was called with the repository

View File

@@ -7,6 +7,7 @@ from litellm.exceptions import (
from openhands.core.config.llm_config import LLMConfig from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.openhands_config import OpenHandsConfig from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.session.session import Session from openhands.server.session.session import Session
from openhands.storage.memory import InMemoryFileStore from openhands.storage.memory import InMemoryFileStore
@@ -64,6 +65,6 @@ async def test_notify_on_llm_retry(
assert mock_litellm_completion.call_count == 2 assert mock_litellm_completion.call_count == 2
session.queue_status_message.assert_called_once_with( session.queue_status_message.assert_called_once_with(
'info', 'STATUS$LLM_RETRY', ANY 'info', RuntimeStatus.LLM_RETRY, ANY
) )
await session.close() await session.close()