diff --git a/openhands-sdk/openhands/sdk/event/llm_convertible/action.py b/openhands-sdk/openhands/sdk/event/llm_convertible/action.py index d2bd9ada3..0bd03cbb1 100644 --- a/openhands-sdk/openhands/sdk/event/llm_convertible/action.py +++ b/openhands-sdk/openhands/sdk/event/llm_convertible/action.py @@ -5,6 +5,7 @@ from openhands.sdk.event.base import N_CHAR_PREVIEW, EventID, LLMConvertibleEvent from openhands.sdk.event.types import SourceType, ToolCallID +from openhands.sdk.event.utils import render_responses_reasoning_block from openhands.sdk.llm import ( Message, MessageToolCall, @@ -87,15 +88,9 @@ def visualize(self) -> Text: content.append("\n\n") # Responses API reasoning (plaintext only; never render encrypted_content) - reasoning_item = self.responses_reasoning_item - if reasoning_item is not None: - content.append("Reasoning:\n", style="bold") - if reasoning_item.summary: - for s in reasoning_item.summary: - content.append(f"- {s}\n") - if reasoning_item.content: - for b in reasoning_item.content: - content.append(f"{b}\n") + reasoning_text = render_responses_reasoning_block(self.responses_reasoning_item) + if reasoning_text: + content.append(reasoning_text) # Display action information using action's visualize method if self.action: diff --git a/openhands-sdk/openhands/sdk/event/llm_convertible/message.py b/openhands-sdk/openhands/sdk/event/llm_convertible/message.py index c71b27ac2..d6126dfea 100644 --- a/openhands-sdk/openhands/sdk/event/llm_convertible/message.py +++ b/openhands-sdk/openhands/sdk/event/llm_convertible/message.py @@ -7,6 +7,7 @@ from openhands.sdk.event.base import N_CHAR_PREVIEW, EventID, LLMConvertibleEvent from openhands.sdk.event.types import SourceType +from openhands.sdk.event.utils import render_responses_reasoning_block from openhands.sdk.llm import ( ImageContent, Message, @@ -74,15 +75,11 @@ def visualize(self) -> Text: content.append("[no text content]") # Responses API reasoning (plaintext only; never render encrypted_content) - reasoning_item = self.llm_message.responses_reasoning_item - if reasoning_item is not None: - content.append("\n\nReasoning:\n", style="bold") - if reasoning_item.summary: - for s in reasoning_item.summary: - content.append(f"- {s}\n") - if reasoning_item.content: - for b in reasoning_item.content: - content.append(f"{b}\n") + reasoning_text = render_responses_reasoning_block( + self.llm_message.responses_reasoning_item, leading_newlines=True + ) + if reasoning_text: + content.append(reasoning_text) # Add skill information if present if self.activated_skills: diff --git a/openhands-sdk/openhands/sdk/event/utils.py b/openhands-sdk/openhands/sdk/event/utils.py new file mode 100644 index 000000000..683bd064c --- /dev/null +++ b/openhands-sdk/openhands/sdk/event/utils.py @@ -0,0 +1,37 @@ +from rich.text import Text + +from openhands.sdk.llm import ReasoningItemModel + + +def render_responses_reasoning_block( + reasoning_item: ReasoningItemModel | None, + *, + leading_newlines: bool = False, +) -> Text | None: + """Build a Rich Text block for Responses API reasoning. + + Only renders when either summary or content is present. Returns None + when there is nothing to render. + """ + if reasoning_item is None: + return None + + has_summary = bool(reasoning_item.summary) + has_content = bool(reasoning_item.content) + if not (has_summary or has_content): + return None + + t = Text() + if leading_newlines: + t.append("\n\n") + t.append("Reasoning:\n", style="bold") + + if has_summary: + for s in list(reasoning_item.summary or []): + t.append(f"- {s}\n") + + if has_content: + for b in list(reasoning_item.content or []): + t.append(f"{b}\n") + + return t diff --git a/tests/sdk/conversation/test_visualizer.py b/tests/sdk/conversation/test_visualizer.py index d878be3cb..94413bc34 100644 --- a/tests/sdk/conversation/test_visualizer.py +++ b/tests/sdk/conversation/test_visualizer.py @@ -138,6 +138,17 @@ def test_action_event_visualize(): result = event.visualize assert isinstance(result, Text) + # If no Responses reasoning is present, Reasoning: should not be printed + event_no_reason = ActionEvent( + thought=[TextContent(text="I need to list files")], + action=action, + tool_name="terminal", + tool_call_id="call_124", + tool_call=create_tool_call("call_124", "terminal", {"command": "ls -la"}), + llm_response_id="response_457", + ) + text_no_reason = event_no_reason.visualize.plain + assert "Reasoning:" not in text_no_reason text_content = result.plain assert "Reasoning:" in text_content