OpenHands / OpenHands/software-agent-sdk
[Bug]: MessageEvent.visualize returns blank output for an empty TextContent block
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 542
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Is there an existing issue for the same bug?
- I have searched existing issues and this is not a duplicate.
Bug Description
MessageEvent.visualize handles two semantically empty message representations inconsistently.
A message with no content blocks displays the existing placeholder:
Message(role="user", content=[])
# visualize.plain == "[no text content]"
However, a valid message containing one empty TextContent block produces blank output:
Message(
role="user",
content=[TextContent(text="")],
)
# visualize.plain == ""
DefaultConversationVisualizer subsequently skips the second event because its rendered text is empty. The event therefore disappears from conversation visualization instead of displaying [no text content].
Expected Behavior
Both empty representations should produce:
[no text content]
DefaultConversationVisualizer should create a visible event block for both representations. Non-empty text and image content should remain unchanged.
Actual Behavior
The two representations produce different results:
content=[]:
visualize.plain = "[no text content]"
event block created = True
content=[TextContent(text="")]:
visualize.plain = ""
event block created = False
The underlying event remains valid, but the default conversation visualizer omits it.
Steps to Reproduce
-
Check out release
v1.44.1at commit9d143aac35c2dcec9cbb046ff9f35ac5eb072f6a. -
Set up the development environment:
make build
- Create
tests/sdk/event/test_message_event_empty_visualization.py:
from openhands.sdk.conversation.visualizer.default import (
DefaultConversationVisualizer,
)
from openhands.sdk.event.llm_convertible import MessageEvent
from openhands.sdk.llm import Message, TextContent
def make_event(content: list[TextContent]) -> MessageEvent:
return MessageEvent(
source="user",
llm_message=Message(
role="user",
content=content,
),
)
def test_empty_text_block_uses_no_text_placeholder():
no_blocks = make_event([])
empty_block = make_event([TextContent(text="")])
visualizer = DefaultConversationVisualizer()
observed = {
"no_blocks_text": no_blocks.visualize.plain,
"empty_block_text": empty_block.visualize.plain,
"no_blocks_rendered": (
visualizer._create_event_block(no_blocks) is not None
),
"empty_block_rendered": (
visualizer._create_event_block(empty_block) is not None
),
}
assert observed == {
"no_blocks_text": "[no text content]",
"empty_block_text": "[no text content]",
"no_blocks_rendered": True,
"empty_block_rendered": True,
}
- Run:
uv run pytest \
tests/sdk/event/test_message_event_empty_visualization.py \
-q
- Observe that the test fails because
empty_block_textis""andempty_block_renderedisFalse.
Acceptance Criteria
-
MessageEvent.visualizerenders[no text content]forcontent=[]. - It renders the same placeholder for
[TextContent(text="")]. -
DefaultConversationVisualizerdoes not discard the empty-block event. - Non-empty text and image visualization remain unchanged.
- Regression coverage includes both empty representations.
Installation Method
Source checkout using make build (uv sync --dev)
If you selected "Other", please specify
Not applicable
SDK Version
1.44.1, main@9d143aac35c2dcec9cbb046ff9f35ac5eb072f6a
Version Confirmation
- I have confirmed this bug exists on the LATEST version of OpenHands SDK
Python Version
3.13.2
Model Name (if applicable)
Not applicable; reproduced by directly invoking deterministic event visualization code.
Operating System
MacOS
Logs and Error Messages
content=[]:
visualize.plain = '[no text content]'
event block created = True
content=[TextContent(text='')]:
visualize.plain = ''
event block created = False
The received assertion value is equivalent to:
{
"no_blocks_text": "[no text content]",
"empty_block_text": "",
"no_blocks_rendered": True,
"empty_block_rendered": False,
}
Minimal Code Sample
from openhands.sdk.event.llm_convertible import MessageEvent
from openhands.sdk.llm import Message, TextContent
event = MessageEvent(
source="user",
llm_message=Message(
role="user",
content=[TextContent(text="")],
),
)
assert event.visualize.plain == "[no text content]"
Screenshots and Additional Context
No screenshot is required; this is a deterministic unit-level reproduction using public SDK models.
The root cause is that content_to_str returns [""] for the empty block. Although its only string is empty, the list itself is truthy:
text_parts = content_to_str(self.llm_message.content)
if text_parts:
full_content = "".join(text_parts)
content.append(full_content)
else:
content.append("[no text content]")
Joining [""] produces "", so the placeholder branch is skipped. The default visualizer then removes the event:
content = event.visualize
if not content.plain.strip():
return None
The repository already treats [TextContent(text="")] as blank content in the native test test_message_tool_calls_strip_blank_list_content. This is therefore a supported model shape rather than an invalid handcrafted value.
A possible fix is to decide whether to use the placeholder after joining the text parts:
full_content = "".join(text_parts)
if full_content:
content.append(full_content)
else:
content.append("[no text content]")
The relevant implementation is unchanged through main@704cbe6015e3d59cabe04632175d99df2d448999. Targeted issue and pull-request searches found no same-root report.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at MessageEvent.visualize and content_to_str, then run tests/sdk/event/test_message_event_empty_visualization.py with the provided uv command. Check the existing native test test_message_tool_calls_strip_blank_list_content for related blank-content behavior; done means both empty representations render [no text content], remain visible, and non-empty text and image output is unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- developer-experience
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100