adorable-city-42958
12/14/2025, 8:29 PMapp-1 | openhands.sdk.conversation.exceptions.ConversationRunError: Conversation run failed for id=c02b8a60-1825-42df-90ab-72745c0cadfb: litellm.BadRequestError: AnthropicException - {"type":"error","error":{"type":"invalid_request_error","message":"messages.2: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01Cnyhua59bGtcZjuVDt4gYm. Each `tool_use` block must have a corresponding `tool_result` block in the next message."},"request_id":"req_011CW7D3TS6djwSvoFGrwN3h"}
app-1 |
Been getting this a lot when there is a lot of events and right after a condensation. Gonna try to investigate
EDIT: Bug found, repro test in the threadadorable-city-42958
12/14/2025, 8:29 PMadorable-city-42958
12/14/2025, 8:46 PMadorable-city-42958
12/14/2025, 9:07 PMBug: Condensation summary can be inserted between action-observation pairs, breaking LLM API message ordering
Summary
The LLMSummarizingCondenser uses a fixed summary_offset (equal to keep_first) when creating a Condensation event. However, after _enforce_batch_atomicity and filter_unmatched_tool_calls modify the kept_events list, the offset may no longer point to a safe insertion position. This can result in the CondensationSummaryEvent being inserted between an ActionEvent and its corresponding ObservationEvent, breaking the required message ordering for LLM APIs.
Root Cause
In LLMSummarizingCondenser.get_condensation():
return Condensation( forgotten_event_ids=[event.id for event in forgotten_events], summary=summary, summary_offset=self.keep_first, # Fixed offset based on original view ...)
Later, in View.from_events():
_enforce_batch_atomicity() may forget additional events (expanding forgotten_event_ids)
filter_unmatched_tool_calls() removes orphaned actions/observations
The summary is inserted at summary_offset before the final filtering
After these transformations, the event list structure changes, but summary_offset remains fixed at keep_first, potentially landing between an action and its observation.
Reproduction
When condensation occurs and:
Some events from the original "head" are filtered out (e.g., due to batch atomicity)
The remaining events don't align with the original keep_first boundary
Example observed in production:
[0] SystemPromptEvent[1] MessageEvent[2] ObservationEvent [orphaned - action was forgotten][3] ActionEvent [tool_call_id=abc123][4] ObservationEvent [tool_call_id=abc123] --> INSERT HERE (summary_offset=4)
The summary is inserted between [3] and [4], which share the same tool_call_id.
Expected Behavior
The summary should be inserted at a position that does not break:
Action-observation pairs (matched by tool_call_id)
Consecutive actions from the same LLM response batch (matched by llm_response_id)
Actual Behavior
The summary can be inserted between an ActionEvent and its ObservationEvent, resulting in malformed LLM API requests:
assistant message (with tool_use)user message (summary) ← breaks the sequencetool message (result)
This violates API requirements (especially Anthropic) where tool results must immediately follow the assistant's tool_use block.adorable-city-42958
12/14/2025, 9:17 PMadorable-city-42958
12/14/2025, 9:24 PMadorable-city-42958
12/14/2025, 9:25 PM@staticmethod
def _find_safe_summary_offset(
events: list[LLMConvertibleEvent],
desired_offset: int,
) -> int:
"""Find a safe offset for inserting the summary that doesn't break
action-observation pairs.
The summary should not be inserted between:
1. Consecutive ActionEvents from the same LLM response batch
2. An ActionEvent and its corresponding ObservationEvent
This method adjusts the offset forward to find a safe insertion point.
"""
if desired_offset >= len(events):
return len(events)
if desired_offset <= 0:
return 0
# Build a map of tool_call_id -> action index for matching
action_indices: dict[ToolCallID, int] = {}
for i, event in enumerate(events):
if isinstance(event, ActionEvent):
action_indices[event.tool_call_id] = i
# Check if the offset lands in an unsafe position and adjust forward
offset = desired_offset
while offset < len(events):
is_safe = True
# Check 1: Don't insert between consecutive actions from same batch
if offset > 0 and offset < len(events):
prev_event = events[offset - 1]
curr_event = events[offset]
# If previous is an action, check if current is also an action
# from the same LLM response
if isinstance(prev_event, ActionEvent) and isinstance(
curr_event, ActionEvent
):
if prev_event.llm_response_id == curr_event.llm_response_id:
is_safe = False
logger.debug(
f"Offset {offset} unsafe: between actions of same batch"
)
# Check 2: Don't insert between an action and its observation
if is_safe and offset > 0:
prev_event = events[offset - 1]
# If previous event is an action, check if any observation
# for this action exists later in the list
if isinstance(prev_event, ActionEvent):
action_tool_call_id = prev_event.tool_call_id
# Look for matching observation after the offset
for j in range(offset, len(events)):
event = events[j]
if (
isinstance(event, ObservationBaseEvent)
and event.tool_call_id == action_tool_call_id
):
# Found observation for this action after offset
# This means we'd be inserting between action and obs
is_safe = False
logger.debug(
f"Offset {offset} unsafe: between action and "
f"observation (tool_call_id={action_tool_call_id})"
)
break
if is_safe:
return offset
offset += 1
return len(events)
....
if summary is not None and summary_offset is not None:
# logger.debug(f"Inserting summary at offset {summary_offset}")
# Clamp summary_offset to valid range
summary_offset = min(summary_offset, len(kept_events))
# Adjust offset to avoid inserting between an action and its observation.
# Find a safe insertion point by ensuring we don't split action-observation pairs.
adjusted_offset = View._find_safe_summary_offset(kept_events, summary_offset)
logger.debug(
f"Inserting summary at offset {adjusted_offset} "
f"(original: {summary_offset})"
)
_new_summary_event = CondensationSummaryEvent(summary=summary)
# kept_events.insert(summary_offset, _new_summary_event)
kept_events.insert(adjusted_offset, _new_summary_event)
...adorable-city-42958
12/14/2025, 9:25 PMadorable-city-42958
12/15/2025, 7:45 PM