Skip to content

Commit 80af3a8

Browse files
(Hotfix): Track reason for Error AgentState (All-Hands-AI#7584)
Co-authored-by: openhands <[email protected]>
1 parent 9f971c5 commit 80af3a8

File tree

5 files changed

+56
-3
lines changed

5 files changed

+56
-3
lines changed

evaluation/utils/shared.py

+5
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,11 @@ def compatibility_for_eval_history_pairs(
521521

522522

523523
def is_fatal_evaluation_error(error: str | None) -> bool:
524+
"""
525+
The AgentController class overrides last error for certain exceptions
526+
We want to ensure those exeption do not overlap with fatal exceptions defined here
527+
This is because we do a comparisino against the stringified error
528+
"""
524529
if not error:
525530
return False
526531

frontend/src/i18n/translation.json

+1
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,7 @@
20782078
"tr": "Ajan hız sınırına ulaştı",
20792079
"ja": "エージェントがレート制限中"
20802080
},
2081+
20812082
"CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": {
20822083
"en": "Agent has paused.",
20832084
"de": "Agent pausiert.",

openhands/controller/agent_controller.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,14 @@ async def _react_to_exception(
227227
e: Exception,
228228
):
229229
"""React to an exception by setting the agent state to error and sending a status message."""
230-
await self.set_agent_state_to(AgentState.ERROR)
230+
# Store the error reason before setting the agent state
231+
self.state.last_error = f'{type(e).__name__}: {str(e)}'
232+
231233
if self.status_callback is not None:
232234
err_id = ''
233235
if isinstance(e, AuthenticationError):
234236
err_id = 'STATUS$ERROR_LLM_AUTHENTICATION'
237+
self.state.last_error = err_id
235238
elif isinstance(
236239
e,
237240
(
@@ -241,14 +244,21 @@ async def _react_to_exception(
241244
),
242245
):
243246
err_id = 'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE'
247+
self.state.last_error = err_id
244248
elif isinstance(e, InternalServerError):
245249
err_id = 'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR'
250+
self.state.last_error = err_id
246251
elif isinstance(e, BadRequestError) and 'ExceededBudget' in str(e):
247252
err_id = 'STATUS$ERROR_LLM_OUT_OF_CREDITS'
253+
# Set error reason for budget exceeded
254+
self.state.last_error = err_id
248255
elif isinstance(e, RateLimitError):
249256
await self.set_agent_state_to(AgentState.RATE_LIMITED)
250257
return
251-
self.status_callback('error', err_id, type(e).__name__ + ': ' + str(e))
258+
self.status_callback('error', err_id, self.state.last_error)
259+
260+
# Set the agent state to ERROR after storing the reason
261+
await self.set_agent_state_to(AgentState.ERROR)
252262

253263
def step(self):
254264
asyncio.create_task(self._step_with_exception_handling())
@@ -581,8 +591,14 @@ async def set_agent_state_to(self, new_state: AgentState) -> None:
581591
self.event_stream.add_event(self._pending_action, EventSource.AGENT)
582592

583593
self.state.agent_state = new_state
594+
595+
# Create observation with reason field if it's an error state
596+
reason = ''
597+
if new_state == AgentState.ERROR:
598+
reason = self.state.last_error
599+
584600
self.event_stream.add_event(
585-
AgentStateChangedObservation('', self.state.agent_state),
601+
AgentStateChangedObservation('', self.state.agent_state, reason),
586602
EventSource.ENVIRONMENT,
587603
)
588604

openhands/events/observation/agent.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class AgentStateChangedObservation(Observation):
1010
"""This data class represents the result from delegating to another agent"""
1111

1212
agent_state: str
13+
reason: str = ''
1314
observation: str = ObservationType.AGENT_STATE_CHANGED
1415

1516
@property

tests/unit/test_agent_controller.py

+30
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from openhands.events.action.agent import RecallAction
1818
from openhands.events.event import RecallType
1919
from openhands.events.observation import (
20+
AgentStateChangedObservation,
2021
ErrorObservation,
2122
)
2223
from openhands.events.observation.agent import RecallObservation
@@ -217,9 +218,17 @@ def on_event_memory(event: Event):
217218
print(f'state: {state}')
218219
events = list(test_event_stream.get_events())
219220
print(f'event_stream: {events}')
221+
error_observations = test_event_stream.get_matching_events(
222+
reverse=True, limit=1, event_types=(AgentStateChangedObservation)
223+
)
224+
assert len(error_observations) == 1
225+
error_observation = error_observations[0]
220226
assert state.iteration == 3
221227
assert state.agent_state == AgentState.ERROR
222228
assert state.last_error == 'AgentStuckInLoopError: Agent got stuck in a loop'
229+
assert (
230+
error_observation.reason == 'AgentStuckInLoopError: Agent got stuck in a loop'
231+
)
223232
assert len(events) == 11
224233

225234

@@ -622,6 +631,17 @@ def on_event_memory(event: Event):
622631
state.last_error
623632
== 'RuntimeError: Agent reached maximum iteration in headless mode. Current iteration: 3, max iteration: 3'
624633
)
634+
error_observations = test_event_stream.get_matching_events(
635+
reverse=True, limit=1, event_types=(AgentStateChangedObservation)
636+
)
637+
assert len(error_observations) == 1
638+
error_observation = error_observations[0]
639+
640+
assert (
641+
error_observation.reason
642+
== 'RuntimeError: Agent reached maximum iteration in headless mode. Current iteration: 3, max iteration: 3'
643+
)
644+
625645
assert (
626646
state.metrics.accumulated_cost == 10.0 * 3
627647
), f'Expected accumulated cost to be 30.0, but got {state.metrics.accumulated_cost}'
@@ -896,6 +916,16 @@ def on_event_memory(event: Event):
896916
== 'LLMContextWindowExceedError: Conversation history longer than LLM context window limit. Consider turning on enable_history_truncation config to avoid this error'
897917
)
898918

919+
error_observations = test_event_stream.get_matching_events(
920+
reverse=True, limit=1, event_types=(AgentStateChangedObservation)
921+
)
922+
assert len(error_observations) == 1
923+
error_observation = error_observations[0]
924+
assert (
925+
error_observation.reason
926+
== 'LLMContextWindowExceedError: Conversation history longer than LLM context window limit. Consider turning on enable_history_truncation config to avoid this error'
927+
)
928+
899929
# Check that the context window exceeded error was raised during the run
900930
assert step_state.has_errored
901931

0 commit comments

Comments
 (0)