fix(opencode): transition subagent ToolPart to error state on RunErrorEvent - #24
Conversation
…rEvent When a subagent fails, it emits RunErrorEvent which was silently dropped by EventProcessor.process() - no match arm existed for it. The parent session's ToolPart stayed in ToolStateRunning forever, making the UI show the subagent as perpetually running even after it crashed. Changes: - Add RunErrorEvent handler in _process_subagent_event() that transitions ToolPart from ToolStateRunning to ToolStateError with error message - Emit SessionErrorEvent for child session (matching TS client behavior) - Add is_errored flag to EventProcessorContext to prevent subsequent StreamCompleteEvent from overriding the error state - Add TDD tests: error transition, error without prior spawn, error state terminal (not overridden by late StreamCompleteEvent)
There was a problem hiding this comment.
Code Review
This pull request implements error handling for subagents by processing RunErrorEvent, ensuring that failures transition the parent's tool part to an error state and are not overwritten by subsequent completion events. It introduces an is_errored flag to the EventProcessorContext and includes comprehensive tests for these scenarios. The review feedback suggests persisting the child session's assistant message to storage upon failure to maintain history consistency and refining the error state logic to better handle out-of-order events and preserve input metadata.
| start_time = ( | ||
| existing.state.time.start | ||
| if isinstance(existing.state, ToolStateRunning) | ||
| else now_ms() | ||
| ) | ||
| error_state = ToolStateError( | ||
| error=error_msg, | ||
| input={ | ||
| "description": tool_title, | ||
| "subagent_type": tool_title, | ||
| "prompt": "", | ||
| }, | ||
| metadata={"sessionId": child_session_id, "title": tool_title}, | ||
| time=TimeStartEnd(start=start_time, end=now_ms()), | ||
| ) |
There was a problem hiding this comment.
The start_time calculation can be improved to handle cases where the subagent might already be in a completed or error state (e.g., due to out-of-order events), as those states also contain the original start time. Additionally, it is safer to reuse the existing input from the tool part to ensure that metadata like the original prompt or description is preserved in the error state.
start_time = (
existing.state.time.start
if hasattr(existing.state, "time") and existing.state.time
else now_ms()
)
error_state = ToolStateError(
error=error_msg,
input=existing.state.input,
metadata={"sessionId": child_session_id, "title": tool_title},
time=TimeStartEnd(start=start_time, end=now_ms()),
)Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Summary
RunErrorEvent), the parent session's ToolPart stays inToolStateRunningforever, making the UI show the subagent as perpetually "running" even after it crashedEventProcessor.process()had no match arm forRunErrorEvent— it was silently dropped. The_process_subagent_event()method only handledStreamCompleteEvent(happy path)RunErrorEventhandler in_process_subagent_event()that transitions ToolPart toToolStateError, emitsSessionErrorEventfor the child session, and prevents subsequentStreamCompleteEventfrom overriding the error stateChanges
event_processor.pyRunErrorEvent+SessionErrorEventimports; add step 8 handler forRunErrorEventin_process_subagent_event(); guardStreamCompleteEventwithis_erroredcheckevent_processor_context.pyis_errored: boolfield to prevent error state overridetest_subagent_error_state.pyTest plan
test_subagent_run_error_transitions_toolpart_to_error— RunErrorEvent transitions ToolPart to ToolStateError with correct error message, emits SessionErrorEventtest_subagent_run_error_without_prior_spawn— RunErrorEvent as first event still creates context and transitions to errortest_subagent_stream_complete_after_run_error_stays_error— Error state is terminal, late StreamCompleteEvent doesn't override