Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,5 @@ cache
/workspace/
openapi.json
.client/
# Git worktrees
.worktrees/
Comment thread
enyst marked this conversation as resolved.
Outdated
3 changes: 2 additions & 1 deletion examples/01_hello_world.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def conversation_callback(event: Event):
)

conversation.send_message(
"Read the current repo and write 3 facts about the project into FACTS.txt."
"Read the current repo README.md and write 1 line on top"
" of the file, using a tool call with insert line. You must use insert line."
Comment thread
enyst marked this conversation as resolved.
Outdated
)
conversation.run()

Expand Down
86 changes: 86 additions & 0 deletions examples/26_responses_reasoning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
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__)


def run_agent_conversation(llm: LLM) -> None:
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 ''}")


def main():
# Prefer proxy credentials commonly used in this repo's examples
Comment thread
enyst marked this conversation as resolved.
Outdated
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",
)

# Run Agent + Conversation using /responses routing
run_agent_conversation(llm)


if __name__ == "__main__":
main()
53 changes: 46 additions & 7 deletions openhands/sdk/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
AgentErrorEvent,
LLMConvertibleEvent,
MessageEvent,
NonExecutableActionEvent,
ObservationEvent,
SystemPromptEvent,
)
from openhands.sdk.event.condenser import Condensation, CondensationRequest
from openhands.sdk.llm import (
Message,
MessageToolCall,
ReasoningItemModel,
RedactedThinkingBlock,
TextContent,
ThinkingBlock,
Expand Down Expand Up @@ -169,16 +171,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 +235,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 +307,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,6 +320,17 @@ 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_call(s)
# so next turn has matching call_id for tool output
tc_event = NonExecutableActionEvent(
source="agent",
thought=thought,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks,
responses_reasoning_item=responses_reasoning_item,
tool_calls=[tool_call],
)
on_event(tc_event)
event = AgentErrorEvent(
error=err,
tool_name=tool_name,
Expand Down Expand Up @@ -340,6 +367,17 @@ def _get_action_event(
f"Error validating args {tool_call.arguments} for tool "
f"'{tool.name}': {e}"
)
# Persist assistant function_call(s)
# so next turn has matching call_id for tool output
tc_event = NonExecutableActionEvent(
source="agent",
thought=thought,
reasoning_content=reasoning_content,
thinking_blocks=thinking_blocks,
responses_reasoning_item=responses_reasoning_item,
tool_calls=[tool_call],
)
on_event(tc_event)
event = AgentErrorEvent(
error=err,
tool_name=tool_name,
Expand All @@ -353,6 +391,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
12 changes: 10 additions & 2 deletions openhands/sdk/context/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
LLMConvertibleEvent,
)
from openhands.sdk.event.base import Event, EventID
from openhands.sdk.event.llm_convertible import ActionEvent, ObservationBaseEvent
from openhands.sdk.event.llm_convertible import (
ActionEvent,
NonExecutableActionEvent,
ObservationBaseEvent,
)
from openhands.sdk.event.types import ToolCallID


Expand Down Expand Up @@ -107,11 +111,15 @@ def filter_unmatched_tool_calls(

@staticmethod
def _get_action_tool_call_ids(events: list[LLMConvertibleEvent]) -> set[ToolCallID]:
"""Extract tool_call_ids from ActionEvents."""
"""Extract tool_call_ids from ActionEvents and NonExecutableActionEvent."""
tool_call_ids = set()
for event in events:
if isinstance(event, ActionEvent) and event.tool_call_id is not None:
tool_call_ids.add(event.tool_call_id)
elif isinstance(event, NonExecutableActionEvent):
for tc in event.tool_calls:
if tc.id:
tool_call_ids.add(tc.id)
return tool_call_ids

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions openhands/sdk/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ActionEvent,
AgentErrorEvent,
MessageEvent,
NonExecutableActionEvent,
ObservationBaseEvent,
ObservationEvent,
SystemPromptEvent,
Expand All @@ -23,6 +24,7 @@
"LLMConvertibleEvent",
"SystemPromptEvent",
"ActionEvent",
"NonExecutableActionEvent",
"ObservationEvent",
"ObservationBaseEvent",
"MessageEvent",
Expand Down
6 changes: 5 additions & 1 deletion openhands/sdk/event/llm_convertible/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from openhands.sdk.event.llm_convertible.action import ActionEvent
from openhands.sdk.event.llm_convertible.action import (
ActionEvent,
NonExecutableActionEvent,
)
from openhands.sdk.event.llm_convertible.message import MessageEvent
from openhands.sdk.event.llm_convertible.observation import (
AgentErrorEvent,
Expand All @@ -12,6 +15,7 @@
__all__ = [
"SystemPromptEvent",
"ActionEvent",
"NonExecutableActionEvent",
"ObservationEvent",
"ObservationBaseEvent",
"MessageEvent",
Expand Down
Loading
Loading