Skip to content

Commit cf276b2

Browse files
All Runtime Status Codes should be in the RuntimeStatus enum (OpenHands#9601)
Co-authored-by: openhands <[email protected]>
1 parent 1f416f6 commit cf276b2

File tree

16 files changed

+121
-76
lines changed

16 files changed

+121
-76
lines changed

frontend/src/types/runtime-status.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,15 @@ export type RuntimeStatus =
55
| "STATUS$RUNTIME_STARTED"
66
| "STATUS$SETTING_UP_WORKSPACE"
77
| "STATUS$SETTING_UP_GIT_HOOKS"
8-
| "STATUS$READY";
8+
| "STATUS$READY"
9+
| "STATUS$ERROR"
10+
| "STATUS$ERROR_RUNTIME_DISCONNECTED"
11+
| "STATUS$ERROR_LLM_AUTHENTICATION"
12+
| "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE"
13+
| "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR"
14+
| "STATUS$ERROR_LLM_OUT_OF_CREDITS"
15+
| "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION"
16+
| "CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE"
17+
| "STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR"
18+
| "STATUS$LLM_RETRY"
19+
| "STATUS$ERROR_MEMORY";

frontend/src/utils/status.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,15 @@ export function getStatusCode(
8888
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
8989
return I18nKey.CHAT_INTERFACE$STOPPED;
9090
}
91-
if (runtimeStatus === "STATUS$BUILDING_RUNTIME") {
92-
return I18nKey.STATUS$BUILDING_RUNTIME;
93-
}
94-
if (runtimeStatus === "STATUS$STARTING_RUNTIME") {
95-
return I18nKey.STATUS$STARTING_RUNTIME;
91+
if (
92+
runtimeStatus &&
93+
!["STATUS$READY", "STATUS$RUNTIME_STARTED"].includes(runtimeStatus)
94+
) {
95+
const result = (I18nKey as { [key: string]: string })[runtimeStatus];
96+
if (result) {
97+
return result;
98+
}
99+
return runtimeStatus;
96100
}
97101
if (webSocketStatus === "DISCONNECTED") {
98102
return I18nKey.CHAT_INTERFACE$DISCONNECTED;

openhands/controller/agent_controller.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
from openhands.events.serialization.event import truncate_content
7676
from openhands.llm.llm import LLM
7777
from openhands.llm.metrics import Metrics
78+
from openhands.runtime.runtime_status import RuntimeStatus
7879
from openhands.storage.files import FileStore
7980

8081
# note: RESUME is only available on web GUI
@@ -248,10 +249,10 @@ async def _react_to_exception(
248249
self.state.last_error = f'{type(e).__name__}: {str(e)}'
249250

250251
if self.status_callback is not None:
251-
err_id = ''
252+
runtime_status = RuntimeStatus.ERROR
252253
if isinstance(e, AuthenticationError):
253-
err_id = 'STATUS$ERROR_LLM_AUTHENTICATION'
254-
self.state.last_error = err_id
254+
runtime_status = RuntimeStatus.ERROR_LLM_AUTHENTICATION
255+
self.state.last_error = runtime_status.value
255256
elif isinstance(
256257
e,
257258
(
@@ -260,20 +261,20 @@ async def _react_to_exception(
260261
APIError,
261262
),
262263
):
263-
err_id = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
264-
self.state.last_error = err_id
264+
runtime_status = RuntimeStatus.ERROR_LLM_SERVICE_UNAVAILABLE
265+
self.state.last_error = runtime_status.value
265266
elif isinstance(e, InternalServerError):
266-
err_id = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
267-
self.state.last_error = err_id
267+
runtime_status = RuntimeStatus.ERROR_LLM_INTERNAL_SERVER_ERROR
268+
self.state.last_error = runtime_status.value
268269
elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e):
269-
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
270-
self.state.last_error = err_id
270+
runtime_status = RuntimeStatus.ERROR_LLM_OUT_OF_CREDITS
271+
self.state.last_error = runtime_status.value
271272
elif isinstance(e, ContentPolicyViolationError) or (
272273
isinstance(e, BadRequestError)
273274
and 'ContentPolicyViolationError' in str(e)
274275
):
275-
err_id = 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION'
276-
self.state.last_error = err_id
276+
runtime_status = RuntimeStatus.ERROR_LLM_CONTENT_POLICY_VIOLATION
277+
self.state.last_error = runtime_status.value
277278
elif isinstance(e, RateLimitError):
278279
# Check if this is the final retry attempt
279280
if (
@@ -283,14 +284,14 @@ async def _react_to_exception(
283284
):
284285
# All retries exhausted, set to ERROR state with a special message
285286
self.state.last_error = (
286-
'CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE'
287+
RuntimeStatus.AGENT_RATE_LIMITED_STOPPED_MESSAGE.value
287288
)
288289
await self.set_agent_state_to(AgentState.ERROR)
289290
else:
290291
# Still retrying, set to RATE_LIMITED state
291292
await self.set_agent_state_to(AgentState.RATE_LIMITED)
292293
return
293-
self.status_callback('error', err_id, self.state.last_error)
294+
self.status_callback('error', runtime_status, self.state.last_error)
294295

295296
# Set the agent state to ERROR after storing the reason
296297
await self.set_agent_state_to(AgentState.ERROR)

openhands/core/loop.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from openhands.core.schema import AgentState
66
from openhands.memory.memory import Memory
77
from openhands.runtime.base import Runtime
8+
from openhands.runtime.runtime_status import RuntimeStatus
89

910

1011
async def run_agent_until_done(
@@ -19,7 +20,7 @@ async def run_agent_until_done(
1920
Note that runtime must be connected before being passed in here.
2021
"""
2122

22-
def status_callback(msg_type: str, msg_id: str, msg: str) -> None:
23+
def status_callback(msg_type: str, runtime_status: RuntimeStatus, msg: str) -> None:
2324
if msg_type == 'error':
2425
logger.error(msg)
2526
if controller:

openhands/memory/memory.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
load_microagents_from_dir,
2424
)
2525
from openhands.runtime.base import Runtime
26+
from openhands.runtime.runtime_status import RuntimeStatus
2627
from openhands.utils.prompt import (
2728
ConversationInstructions,
2829
RepositoryInfo,
@@ -133,7 +134,7 @@ async def _on_event(self, event: Event):
133134
except Exception as e:
134135
error_str = f'Error: {str(e.__class__.__name__)}'
135136
logger.error(error_str)
136-
self.send_error_message('STATUS$ERROR_MEMORY', error_str)
137+
self.set_runtime_status(RuntimeStatus.ERROR_MEMORY, error_str)
137138
return
138139

139140
def _on_workspace_context_recall(
@@ -361,22 +362,24 @@ def set_conversation_instructions(
361362
content=conversation_instructions or ''
362363
)
363364

364-
def send_error_message(self, message_id: str, message: str):
365+
def set_runtime_status(self, status: RuntimeStatus, message: str):
365366
"""Sends an error message if the callback function was provided."""
366367
if self.status_callback:
367368
try:
368369
if self.loop is None:
369370
self.loop = asyncio.get_running_loop()
370371
asyncio.run_coroutine_threadsafe(
371-
self._send_status_message('error', message_id, message), self.loop
372+
self._set_runtime_status('error', status, message), self.loop
372373
)
373-
except RuntimeError as e:
374+
except (RuntimeError, KeyError) as e:
374375
logger.error(
375376
f'Error sending status message: {e.__class__.__name__}',
376377
stack_info=False,
377378
)
378379

379-
async def _send_status_message(self, msg_type: str, id: str, message: str):
380+
async def _set_runtime_status(
381+
self, msg_type: str, runtime_status: RuntimeStatus, message: str
382+
):
380383
"""Sends a status message to the client."""
381384
if self.status_callback:
382-
self.status_callback(msg_type, id, message)
385+
self.status_callback(msg_type, runtime_status, message)

openhands/runtime/base.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class Runtime(FileEditRuntimeMixin):
113113
config: OpenHandsConfig
114114
initial_env_vars: dict[str, str]
115115
attach_to_existing: bool
116-
status_callback: Callable[[str, str, str], None] | None
116+
status_callback: Callable[[str, RuntimeStatus, str], None] | None
117117
runtime_status: RuntimeStatus | None
118118
_runtime_initialized: bool = False
119119

@@ -124,7 +124,7 @@ def __init__(
124124
sid: str = 'default',
125125
plugins: list[PluginRequirement] | None = None,
126126
env_vars: dict[str, str] | None = None,
127-
status_callback: Callable[[str, str, str], None] | None = None,
127+
status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
128128
attach_to_existing: bool = False,
129129
headless_mode: bool = False,
130130
user_id: str | None = None,
@@ -207,16 +207,13 @@ def log(self, level: str, message: str) -> None:
207207
message = f'[runtime {self.sid}] {message}'
208208
getattr(logger, level)(message, stacklevel=2)
209209

210-
def set_runtime_status(self, runtime_status: RuntimeStatus):
210+
def set_runtime_status(
211+
self, runtime_status: RuntimeStatus, msg: str = '', level: str = 'info'
212+
):
211213
"""Sends a status message if the callback function was provided."""
212214
self.runtime_status = runtime_status
213215
if self.status_callback:
214-
msg_id: str = runtime_status.value # type: ignore
215-
self.status_callback('info', msg_id, runtime_status.message)
216-
217-
def send_error_message(self, message_id: str, message: str):
218-
if self.status_callback:
219-
self.status_callback('error', message_id, message)
216+
self.status_callback(level, runtime_status, msg)
220217

221218
# ====================================================================
222219

@@ -344,15 +341,13 @@ async def _handle_action(self, event: Action) -> None:
344341
else:
345342
observation = await call_sync_from_async(self.run_action, event)
346343
except Exception as e:
347-
err_id = ''
348-
if isinstance(e, httpx.NetworkError) or isinstance(
349-
e, AgentRuntimeDisconnectedError
350-
):
351-
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
344+
runtime_status = RuntimeStatus.ERROR
345+
if isinstance(e, (httpx.NetworkError, AgentRuntimeDisconnectedError)):
346+
runtime_status = RuntimeStatus.ERROR_RUNTIME_DISCONNECTED
352347
error_message = f'{type(e).__name__}: {str(e)}'
353348
self.log('error', f'Unexpected error while running action: {error_message}')
354349
self.log('error', f'Problematic action: {str(event)}')
355-
self.send_error_message(err_id, error_message)
350+
self.set_runtime_status(runtime_status, error_message)
356351
return
357352

358353
observation._cause = event.id # type: ignore[attr-defined]
@@ -397,7 +392,7 @@ async def clone_or_init_repo(
397392

398393
if self.status_callback:
399394
self.status_callback(
400-
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...'
395+
'info', RuntimeStatus.SETTING_UP_WORKSPACE, 'Setting up workspace...'
401396
)
402397

403398
dir_name = selected_repository.split('/')[-1]
@@ -438,7 +433,7 @@ def maybe_run_setup_script(self):
438433

439434
if self.status_callback:
440435
self.status_callback(
441-
'info', 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...'
436+
'info', RuntimeStatus.SETTING_UP_WORKSPACE, 'Setting up workspace...'
442437
)
443438

444439
# setup scripts time out after 10 minutes
@@ -470,7 +465,7 @@ def maybe_setup_git_hooks(self):
470465

471466
if self.status_callback:
472467
self.status_callback(
473-
'info', 'STATUS$SETTING_UP_GIT_HOOKS', 'Setting up git hooks...'
468+
'info', RuntimeStatus.SETTING_UP_GIT_HOOKS, 'Setting up git hooks...'
474469
)
475470

476471
# Ensure the git hooks directory exists

openhands/runtime/impl/cli/cli_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def __init__(
113113
sid: str = 'default',
114114
plugins: list[PluginRequirement] | None = None,
115115
env_vars: dict[str, str] | None = None,
116-
status_callback: Callable[[str, str, str], None] | None = None,
116+
status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
117117
attach_to_existing: bool = False,
118118
headless_mode: bool = False,
119119
user_id: str | None = None,

openhands/runtime/impl/kubernetes/kubernetes_runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ async def connect(self):
252252
await call_sync_from_async(self._wait_until_ready)
253253
except Exception as alive_error:
254254
self.log('error', f'Failed to connect to runtime: {alive_error}')
255-
self.send_error_message(
256-
'ERROR$RUNTIME_CONNECTION',
255+
self.set_runtime_status(
256+
RuntimeStatus.ERROR_RUNTIME_DISCONNECTED,
257257
f'Failed to connect to runtime: {alive_error}',
258258
)
259259
raise AgentRuntimeDisconnectedError(

openhands/runtime/impl/local/local_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def __init__(
134134
sid: str = 'default',
135135
plugins: list[PluginRequirement] | None = None,
136136
env_vars: dict[str, str] | None = None,
137-
status_callback: Callable[[str, str, str], None] | None = None,
137+
status_callback: Callable[[str, RuntimeStatus, str], None] | None = None,
138138
attach_to_existing: bool = False,
139139
headless_mode: bool = True,
140140
user_id: str | None = None,

openhands/runtime/runtime_status.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22

33

44
class RuntimeStatus(Enum):
5-
def __init__(self, value: str, message: str):
6-
self._value_ = value
7-
self.message = message
8-
9-
STOPPED = 'STATUS$STOPPED', 'Stopped'
10-
BUILDING_RUNTIME = 'STATUS$BUILDING_RUNTIME', 'Building runtime...'
11-
STARTING_RUNTIME = 'STATUS$STARTING_RUNTIME', 'Starting runtime...'
12-
RUNTIME_STARTED = 'STATUS$RUNTIME_STARTED', 'Runtime started...'
13-
SETTING_UP_WORKSPACE = 'STATUS$SETTING_UP_WORKSPACE', 'Setting up workspace...'
14-
SETTING_UP_GIT_HOOKS = 'STATUS$SETTING_UP_GIT_HOOKS', 'Setting up git hooks...'
15-
READY = 'STATUS$READY', 'Ready...'
5+
STOPPED = 'STATUS$STOPPED'
6+
BUILDING_RUNTIME = 'STATUS$BUILDING_RUNTIME'
7+
STARTING_RUNTIME = 'STATUS$STARTING_RUNTIME'
8+
RUNTIME_STARTED = 'STATUS$RUNTIME_STARTED'
9+
SETTING_UP_WORKSPACE = 'STATUS$SETTING_UP_WORKSPACE'
10+
SETTING_UP_GIT_HOOKS = 'STATUS$SETTING_UP_GIT_HOOKS'
11+
READY = 'STATUS$READY'
12+
ERROR = 'STATUS$ERROR'
13+
ERROR_RUNTIME_DISCONNECTED = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
14+
ERROR_LLM_AUTHENTICATION = 'STATUS$ERROR_LLM_AUTHENTICATION'
15+
ERROR_LLM_SERVICE_UNAVAILABLE = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
16+
ERROR_LLM_INTERNAL_SERVER_ERROR = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
17+
ERROR_LLM_OUT_OF_CREDITS = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
18+
ERROR_LLM_CONTENT_POLICY_VIOLATION = 'STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION'
19+
AGENT_RATE_LIMITED_STOPPED_MESSAGE = (
20+
'CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE'
21+
)
22+
GIT_PROVIDER_AUTHENTICATION_ERROR = 'STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR'
23+
LLM_RETRY = 'STATUS$LLM_RETRY'
24+
ERROR_MEMORY = 'STATUS$ERROR_MEMORY'

0 commit comments

Comments
 (0)