When an LLM response arrives with finish_reason='length' and has_tool_calls
but blank text content (e.g. the model spent its whole output budget on a
tool call whose closing tag was truncated), the runner dropped the tool
calls and then misrouted the blank response into the empty-response retry
branch. Retrying the same prompt cannot recover from output-budget
exhaustion, so every retry hit the same length ceiling and the turn ended
in the generic apology.
The length-recovery branch was gated on 'finish_reason == length and not
is_blank_text(clean)', so a blank-but-truncated turn could never reach it.
- The empty-response retry branch now excludes finish_reason == 'length'
(in addition to 'error').
- The length-recovery branch no longer requires non-blank content, so a
blank-but-truncated turn enters recovery and appends
build_length_recovery_message (which handles a blank tail safely).
Adds a regression test asserting the length-recovery path is taken; it
fails on the unfixed code and passes with the fix.
Fixes#5133
The tool execution path caught BaseException, which includes
KeyboardInterrupt, SystemExit, MemoryError, and GeneratorExit.
These should never be caught and converted into conversational
error messages. CancelledError is already handled separately.
Change except BaseException to except Exception so fatal signals
propagate instead of being swallowed.
Adds parametrized regression test for KeyboardInterrupt and
SystemExit propagation.
Fixes#4788