Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2808,9 +2808,19 @@ def ask_agent(self, question: str) -> str:
content=[TextContent(text=question_text)],
)

messages = prepare_llm_messages(
self.state.view, additional_messages=[user_message]
)
# Build the ask-agent context from a fresh, property-enforced view of the
# active branch rather than the cached `state.view`. The cached view is
# maintained incrementally and skips `enforce_properties` on linear
# appends, so it can transiently contain an ActionEvent whose
# ObservationEvent has not landed yet (the main agent is still executing
# it). Anthropic rejects a `tool_use` with no following `tool_result`, so
# an in-flight tool call would make ask_agent fail with a 400. A fresh
# `View.from_events` re-runs property enforcement and drops the orphaned
# action before serialization.
from openhands.sdk.context.view import View

ask_view = View.from_events(self._state.active_branch())
messages = prepare_llm_messages(ask_view, additional_messages=[user_message])

# Get or create the specialized ask-agent LLM
try:
Expand Down
66 changes: 66 additions & 0 deletions tests/sdk/conversation/test_ask_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,72 @@ def test_ask_agent_with_existing_events_and_tool_calls(
assert tool_msg.name == "terminal"


@patch("openhands.sdk.llm.llm.LLM.completion")
def test_ask_agent_drops_in_flight_action_without_observation(
mock_completion, tmp_path, agent
):
"""ask_agent must not forward an ActionEvent whose observation hasn't landed.

The main agent can be mid-tool-call when ask_agent() runs on another thread,
leaving an ActionEvent in the cached view without its ObservationEvent.
Anthropic rejects a ``tool_use`` with no following ``tool_result``, so the
orphaned action must be dropped before the context is sent.
"""
mock_completion.return_value = create_mock_llm_response("answer")

conv = Conversation(
agent=agent,
persistence_dir=str(tmp_path),
workspace=str(tmp_path),
)

conv.state.events.append(
SystemPromptEvent(
source="agent",
system_prompt=TextContent(text="You are a helpful assistant."),
tools=[],
)
)
conv.state.events.append(
MessageEvent(
source="user",
llm_message=Message(
role="user",
content=[TextContent(text="List the files")],
),
)
)

# In-flight action with NO matching observation.
tool_call = MessageToolCall(
id="call_inflight",
name="terminal",
arguments=json.dumps({"command": "ls -la"}),
origin="completion",
)
conv.state.events.append(
ActionEvent(
source="agent",
thought=[TextContent(text="Listing files")],
action=MockAction(command="ls -la"),
tool_name="terminal",
tool_call_id="call_inflight",
tool_call=tool_call,
llm_response_id="response_1",
)
)

conv.ask_agent("What did you find?")

messages = mock_completion.call_args.kwargs["messages"]
assistant_tool_calls = [
m for m in messages if m.role == "assistant" and m.tool_calls
]
assert assistant_tool_calls == [], (
"Orphaned tool_use must be dropped from the ask_agent context"
)


# ---------------------------------------------------------------------------
# Exception handling tests
# ---------------------------------------------------------------------------
Expand Down
Loading