Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ad36e9a
native responses, clean up
enyst Oct 2, 2025
6c8bfa3
refactor message, serialization and reading
enyst Oct 3, 2025
809ecaf
tweak
enyst Oct 3, 2025
1ec94df
fix telemetry
enyst Oct 3, 2025
ffbb3bb
fix typing
enyst Oct 3, 2025
5120a97
Add FACTS.txt with one project fact
openhands-agent Oct 3, 2025
640bfd0
Remove FACTS.txt
openhands-agent Oct 3, 2025
8efe577
use typed reasoning
enyst Oct 3, 2025
f223ddc
chore: ignore .worktrees and remove from index (no embedded repos)
enyst Oct 3, 2025
fe3509d
chore: remove mistakenly committed .worktrees gitlinks
enyst Oct 3, 2025
3d1fe96
fix agent oops
enyst Oct 3, 2025
430a4e4
fix reasoning types
enyst Oct 3, 2025
3be1a21
fix display
enyst Oct 3, 2025
5a906ae
examples: conversation-only Responses reasoning demo (format)
enyst Oct 3, 2025
d26d510
fix input_text (assistant can only be output); clean up a little
enyst Oct 3, 2025
4d0a158
clean up a little
enyst Oct 3, 2025
30bf50d
use standard model features
enyst Oct 3, 2025
2ec226e
fix tests, types
enyst Oct 3, 2025
ab97401
tests: add unit tests for Responses API (#634)
enyst Oct 4, 2025
bb3d93f
Fix import (#636)
enyst Oct 4, 2025
88f710a
Update .gitignore
enyst Oct 4, 2025
d7f9482
Update examples/01_hello_world.py
enyst Oct 4, 2025
b0aad14
Update examples/26_responses_reasoning.py
enyst Oct 4, 2025
56ac872
tests(llm): move LLM import to top-level in responses serialization test
enyst Oct 4, 2025
f1bb293
Merge branch 'main' into response
enyst Oct 4, 2025
a439ecd
tests(sdk): add coverage for Responses API message/tool gaps
enyst Oct 4, 2025
e83af81
tests(sdk): fix output coercion tests for Observation wrapping when o…
enyst Oct 4, 2025
a578e4f
tests(sdk): adjust non-function tool_call test to avoid LiteLLM type …
enyst Oct 4, 2025
d9d8bd5
tests(sdk): rename Action class in responses tool security test to av…
enyst Oct 4, 2025
2784a5a
tests(agent): add Agent.step routing test for Responses vs Completion…
enyst Oct 4, 2025
1b8e97b
tests(agent): add model-features based gating test for Agent.step
enyst Oct 4, 2025
966150f
Update openhands/sdk/llm/message.py
enyst Oct 4, 2025
3f073c9
Merge branch 'main' into response
enyst Oct 4, 2025
b46dbc4
merge: align response branch with main refactor for non-executable to…
enyst Oct 4, 2025
c9c5b38
dudup code
xingyaoww Oct 4, 2025
e0f2a63
adjust formatting
xingyaoww Oct 4, 2025
fa4aec6
add comment back
xingyaoww Oct 4, 2025
d3d948a
minor simplification
xingyaoww Oct 4, 2025
14ccfc8
Update openhands/sdk/tool/tool.py
enyst Oct 4, 2025
64eba97
adapt test
enyst Oct 4, 2025
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
77 changes: 77 additions & 0 deletions examples/26_responses_reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Example: Responses API path via LiteLLM in a Real Agent Conversation

- Runs a real Agent/Conversation to verify /responses path works
- Demonstrates rendering of Responses reasoning within normal conversation events
"""

from __future__ import annotations

import os

from pydantic import SecretStr

from openhands.sdk import (
Conversation,
Event,
LLMConvertibleEvent,
get_logger,
)
from openhands.sdk.llm import LLM
from openhands.tools.preset.default import get_default_agent


logger = get_logger(__name__)


api_key = os.getenv("LITELLM_API_KEY") or os.getenv("OPENAI_API_KEY")
assert api_key, "Set LITELLM_API_KEY or OPENAI_API_KEY in your environment."

model = os.getenv("OPENAI_RESPONSES_MODEL", "openai/gpt-5-mini")
base_url = os.getenv("LITELLM_BASE_URL", "https://llm-proxy.eval.all-hands.dev")

llm = LLM(
model=model,
api_key=SecretStr(api_key),
base_url=base_url,
# Responses-path options
enable_encrypted_reasoning=True, # request encrypted reasoning passthrough
reasoning_effort="high",
# Logging / behavior tweaks
log_completions=False,
drop_params=True,
service_id="agent",
)

print("\n=== Agent Conversation using /responses path ===")
agent = get_default_agent(
llm=llm,
cli_mode=True, # disable browser tools for env simplicity
)

llm_messages = [] # collect raw LLM-convertible messages for inspection


def conversation_callback(event: Event):
if isinstance(event, LLMConvertibleEvent):
llm_messages.append(event.to_llm_message())


conversation = Conversation(
agent=agent,
callbacks=[conversation_callback],
workspace=os.getcwd(),
)

# Keep the tasks short for demo purposes
conversation.send_message("Read the repo and write one fact into FACTS.txt.")
conversation.run()

conversation.send_message("Now delete FACTS.txt.")
conversation.run()

print("=" * 100)
print("Conversation finished. Got the following LLM messages:")
for i, message in enumerate(llm_messages):
ms = str(message)
print(f"Message {i}: {ms[:200]}{'...' if len(ms) > 200 else ''}")
38 changes: 27 additions & 11 deletions openhands/sdk/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from openhands.sdk.llm import (
Message,
MessageToolCall,
ReasoningItemModel,
RedactedThinkingBlock,
TextContent,
ThinkingBlock,
Expand Down Expand Up @@ -169,16 +170,26 @@ def step(
_messages = LLMConvertibleEvent.events_to_messages(llm_convertible_events)
logger.debug(
"Sending messages to LLM: "
f"{json.dumps([m.model_dump() for m in _messages], indent=2)}"
f"{json.dumps([m.model_dump() for m in _messages[1:]], indent=2)}"
)

try:
llm_response = self.llm.completion(
messages=_messages,
tools=list(self.tools_map.values()),
extra_body={"metadata": self.llm.metadata},
add_security_risk_prediction=self._add_security_risk_prediction,
)
if self.llm.uses_responses_api():
llm_response = self.llm.responses(
messages=_messages,
tools=list(self.tools_map.values()),
include=None,
store=False,
add_security_risk_prediction=self._add_security_risk_prediction,
metadata=self.llm.metadata,
)
else:
llm_response = self.llm.completion(
messages=_messages,
tools=list(self.tools_map.values()),
extra_body={"metadata": self.llm.metadata},
add_security_risk_prediction=self._add_security_risk_prediction,
)
except Exception as e:
# If there is a condenser registered and the exception is a context window
# exceeded, we can recover by triggering a condensation request.
Expand Down Expand Up @@ -223,6 +234,9 @@ def step(
reasoning_content=message.reasoning_content if i == 0 else None,
# Only first gets thinking blocks
thinking_blocks=list(message.thinking_blocks) if i == 0 else [],
responses_reasoning_item=message.responses_reasoning_item
if i == 0
else None,
)
if action_event is None:
continue
Expand Down Expand Up @@ -292,6 +306,7 @@ def _get_action_event(
thought: list[TextContent] = [],
reasoning_content: str | None = None,
thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] = [],
responses_reasoning_item: ReasoningItemModel | None = None,
) -> ActionEvent | None:
"""Converts a tool call into an ActionEvent, validating arguments.

Expand All @@ -304,13 +319,13 @@ def _get_action_event(
available = list(self.tools_map.keys())
err = f"Tool '{tool_name}' not found. Available: {available}"
logger.error(err)
# Persist assistant function_calls so next turn
# has matching call_id for tool output
# Persist assistant function_call so next turn has matching call_id
tc_event = ActionEvent(
source="agent",
thought=thought,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks,
responses_reasoning_item=responses_reasoning_item,
tool_call=tool_call,
tool_name=tool_call.name,
tool_call_id=tool_call.id,
Expand Down Expand Up @@ -354,13 +369,13 @@ def _get_action_event(
f"Error validating args {tool_call.arguments} for tool "
f"'{tool.name}': {e}"
)
# Persist assistant function_calls so next turn
# has matching call_id for tool output
# Persist assistant function_call so next turn has matching call_id
tc_event = ActionEvent(
source="agent",
thought=thought,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks,
responses_reasoning_item=responses_reasoning_item,
tool_call=tool_call,
tool_name=tool_call.name,
tool_call_id=tool_call.id,
Expand All @@ -381,6 +396,7 @@ def _get_action_event(
thought=thought,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks,
responses_reasoning_item=responses_reasoning_item,
tool_name=tool.name,
tool_call_id=tool_call.id,
tool_call=tool_call,
Expand Down
16 changes: 16 additions & 0 deletions openhands/sdk/event/llm_convertible/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from openhands.sdk.llm import (
Message,
MessageToolCall,
ReasoningItemModel,
RedactedThinkingBlock,
TextContent,
ThinkingBlock,
Expand All @@ -29,6 +30,9 @@ class ActionEvent(LLMConvertibleEvent):
default_factory=list,
description="Anthropic thinking blocks from the LLM response",
)
responses_reasoning_item: ReasoningItemModel | None = Field(
default=None, description="OpenAI Responses reasoning item from model output"
)
action: Action | None = Field(
default=None,
description="Single tool call returned by LLM (None when non-executable)",
Expand Down Expand Up @@ -82,6 +86,17 @@ def visualize(self) -> Text:
content.append(thought_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")

# Display action information using action's visualize method
if self.action:
content.append(self.action.visualize)
Expand All @@ -100,6 +115,7 @@ def to_llm_message(self) -> Message:
tool_calls=[self.tool_call],
reasoning_content=self.reasoning_content,
thinking_blocks=self.thinking_blocks,
responses_reasoning_item=self.responses_reasoning_item,
)

def __str__(self) -> str:
Expand Down
18 changes: 16 additions & 2 deletions openhands/sdk/event/llm_convertible/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,29 @@ def visualize(self) -> Text:
"""Return Rich Text representation of this message event."""
content = Text()

# Message text content
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]")

# 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")

# Add microagent information if present
if self.activated_microagents:
content.append(
f"\nActivated Microagents: {', '.join(self.activated_microagents)}",
f"\n\nActivated Microagents: {', '.join(self.activated_microagents)}",
)

# Add extended content if available
Expand All @@ -69,7 +81,9 @@ def visualize(self) -> Text:
isinstance(c, ImageContent) for c in self.extended_content
), "Extended content should not contain images"
text_parts = content_to_str(self.extended_content)
content.append("\nPrompt Extension based on Agent Context:\n")
content.append(
"\n\nPrompt Extension based on Agent Context:\n", style="bold"
)
content.append(" ".join(text_parts))

return content
Expand Down
2 changes: 2 additions & 0 deletions openhands/sdk/event/llm_convertible/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def visualize(self) -> Text:
return content

def to_llm_message(self) -> Message:
# Provide plain string error content; serializers handle Chat vs Responses.
# For Responses API, output is a string; JSON is not required.
return Message(
role="tool",
content=[TextContent(text=self.error)],
Expand Down
2 changes: 2 additions & 0 deletions openhands/sdk/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
ImageContent,
Message,
MessageToolCall,
ReasoningItemModel,
RedactedThinkingBlock,
TextContent,
ThinkingBlock,
Expand All @@ -31,6 +32,7 @@
"ImageContent",
"ThinkingBlock",
"RedactedThinkingBlock",
"ReasoningItemModel",
"content_to_str",
"Metrics",
"MetricsSnapshot",
Expand Down
Loading
Loading