fix(harness): normalize ToolMessage structured content in serialization#1167
fix(harness): normalize ToolMessage structured content in serialization#1167mvanhorn wants to merge 2 commits intobytedance:mainfrom
Conversation
When models return ToolMessage content as a list of content blocks
(e.g., [{"type": "text", "text": "..."}]), _serialize_message and the
stream method used str() which produced raw Python repr strings in the
UI. Use _extract_text() instead, which already handles both string and
list content correctly.
The same normalization pattern was applied to titles in PR bytedance#1155 but
the main message serialization path was not updated.
Fixes bytedance#1149
There was a problem hiding this comment.
Pull request overview
Normalizes LangChain ToolMessage structured content (list-of-blocks) to plain text during backend serialization so the frontend doesn’t display Python repr(...) strings for tool results.
Changes:
- Replace
str(msg.content)with_extract_text(msg.content)forToolMessageserialization in both_serialize_message()andstream(). - Add regression tests covering
_serialize_message()behavior for tool message content variants.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| backend/packages/harness/deerflow/client.py | Uses _extract_text() to normalize ToolMessage content for values and messages-tuple streaming events. |
| backend/tests/test_serialize_message_content.py | Adds regression tests to ensure _serialize_message() extracts plain text from ToolMessage list/block content. |
| return { | ||
| "type": "tool", | ||
| "content": msg.content if isinstance(msg.content, str) else str(msg.content), | ||
| "content": DeerFlowClient._extract_text(msg.content), |
| return { | ||
| "type": "tool", | ||
| "content": msg.content if isinstance(msg.content, str) else str(msg.content), | ||
| "content": DeerFlowClient._extract_text(msg.content), |
| data={ | ||
| "type": "tool", | ||
| "content": msg.content if isinstance(msg.content, str) else str(msg.content), | ||
| "content": self._extract_text(msg.content), |
|
@WillemJiang - good question. The abnormal text the user sees in the browser originates from the backend serialization. When qwen3-max returns structured ToolMessage content (a list of content blocks), This is the same pattern that was fixed for titles in PR #1155 - this PR extends If there's also a separate frontend rendering issue, this PR wouldn't cover that - but the backend producing clean text instead of Python repr strings should address the reported symptom. Happy to close if you've already fixed this elsewhere. |
Summary
When models return ToolMessage content as a list of content blocks (e.g.,
[{"type": "text", "text": "..."}]), the UI displays the raw Python repr string instead of the extracted text.Why this matters
Changes
Two call sites in
client.pyusedstr(msg.content)for ToolMessage content when it isn't a string. Replaced both with the existing_extract_text()helper, which already handles string content, list-of-blocks content, and other types:_serialize_message()(L242) - used invalueseventsstream()(L348) - used inmessages-tupleeventsAdded
test_serialize_message_content.pywith 6 regression tests covering string, list-of-blocks, mixed, and empty content.Testing
Fixes #1149
This contribution was developed with AI assistance (Claude Code).