Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion engine/agents/openai_event_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ def _map_assistant_message(
raw_item = item.raw_item
item_id = raw_item.id
parts = raw_item.content
text = "".join(part.text for part in parts if isinstance(part, ResponseOutputText))
# ``part.text`` can be ``None`` on leniently-parsed provider payloads:
# Gemini (via the LiteLLM Responses translation) emits ``output_text``
# parts with no ``text`` field when a turn is tool-calls-only, and the
# SDK materializes those without validation. Joining ``None`` raises
# ``TypeError: sequence item 0: expected str instance, NoneType found``
# and kills the run (INF: analysis-smoke gemini-3.6-flash failures).
text = "".join(part.text or "" for part in parts if isinstance(part, ResponseOutputText))
refusal_text = _extract_refusal_text(parts=parts, text=text)
if refusal_text is not None:
return MappedEvent(refusal_text=refusal_text)
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/agents/test_openai_event_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,38 @@ def test_tool_call_and_output_have_distinct_item_ids() -> None:
assert output_mapped.context_item is not None
assert call_mapped.context_item.item_id == "tool-call-call_xyz"
assert output_mapped.context_item.item_id == "tool-result-call_xyz"


def test_assistant_text_part_with_none_text_maps_as_empty() -> None:
"""Gemini via the LiteLLM Responses translation emits ``output_text`` parts
with no ``text`` field on tool-calls-only turns; the SDK materializes them
leniently with ``text=None``. The mapper must treat those as empty instead
of raising ``TypeError: sequence item 0: expected str instance, NoneType
found`` (which failed whole HALO runs on gemini-3.6-flash).
"""
from agents.items import MessageOutputItem
from agents.stream_events import RunItemStreamEvent
from openai.types.responses import ResponseOutputMessage, ResponseOutputText

from tests._sdk_events import SHARED_AGENT

raw = ResponseOutputMessage.model_construct(
id="msg_none_text",
type="message",
role="assistant",
status="completed",
content=[ResponseOutputText.model_construct(type="output_text", text=None, annotations=[])],
)
event = RunItemStreamEvent(
name="message_output_created",
item=MessageOutputItem(agent=SHARED_AGENT, raw_item=raw),
)

mapper = OpenAiEventMapper()
mapped = mapper.to_mapped_event(event, execution=_exec(), is_root=True)
assert mapped.refusal_text is None
assert mapped.context_item is not None
assert mapped.context_item.content is None
assert mapped.output_item is not None
assert mapped.output_item.final is False
assert mapped.output_item.item.content is None
Loading