Skip to content

Commit d5c43b1

Browse files
Address PR review feedback
- Rename _make_sentinel_llm → _make_dummy_llm - Move inline acp imports to top-level - Add GitHub issue link to TODO comment - Simplify _filter_jsonrpc_lines exception handling - Add debug logging for on_token and stats callback failures - Remove chatty Example from class docstring - Remove critic validation check (ACP server manages its own evaluation) - Add comment explaining lazy import in __init__.py (avoids circular dependency) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ae9b9b commit d5c43b1

File tree

3 files changed

+34
-50
lines changed

3 files changed

+34
-50
lines changed

openhands-sdk/openhands/sdk/agent/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
from openhands.sdk.agent.acp_agent import ACPAgent
1111

1212

13+
# Lazy import: eagerly importing ACPAgent registers it in the
14+
# DiscriminatedUnionMixin, which makes `kind` required in Agent payloads
15+
# that previously defaulted. It also avoids a circular import through
16+
# tool/builtins/finish.py if ACPAgent is imported before Agent.
1317
def __getattr__(name: str):
1418
if name == "ACPAgent":
1519
from openhands.sdk.agent.acp_agent import ACPAgent

openhands-sdk/openhands/sdk/agent/acp_agent.py

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,24 @@
1010

1111
from __future__ import annotations
1212

13+
import asyncio
1314
import os
1415
from collections.abc import Generator
1516
from typing import TYPE_CHECKING, Any
1617

18+
from acp.client.connection import ClientSideConnection
19+
from acp.helpers import text_block
20+
from acp.schema import (
21+
AgentMessageChunk,
22+
AgentThoughtChunk,
23+
AllowedOutcome,
24+
RequestPermissionResponse,
25+
TextContentBlock,
26+
ToolCallProgress,
27+
ToolCallStart,
28+
UsageUpdate,
29+
)
30+
from acp.transports import default_environment
1731
from pydantic import Field, PrivateAttr
1832

1933
from openhands.sdk.agent.base import AgentBase
@@ -41,15 +55,16 @@
4155
# so we yield to the event loop and then sleep briefly to allow in-flight
4256
# handlers to finish. Override via ACP_NOTIFICATION_DRAIN_DELAY for slow or
4357
# remote servers.
44-
# TODO: Replace with protocol-level synchronization once ACP supports a
45-
# "turn complete" sentinel notification.
58+
# TODO(https://github.com/agentclientprotocol/agent-client-protocol/issues/554):
59+
# Replace with protocol-level synchronization once ACP supports a
60+
# "turn complete" notification.
4661
_NOTIFICATION_DRAIN_DELAY: float = float(
4762
os.environ.get("ACP_NOTIFICATION_DRAIN_DELAY", "0.1")
4863
)
4964

5065

51-
def _make_sentinel_llm() -> LLM:
52-
"""Create a sentinel LLM that should never be called."""
66+
def _make_dummy_llm() -> LLM:
67+
"""Create a dummy LLM that should never be called directly."""
5368
return LLM(model="acp-managed")
5469

5570

@@ -78,15 +93,12 @@ async def _filter_jsonrpc_lines(source: Any, dest: Any) -> None:
7893
if stripped.startswith(b"{") and b'"jsonrpc"' in line:
7994
dest.feed_data(line)
8095
else:
81-
# Log non-JSON lines at debug level
82-
try:
83-
logger.debug(
84-
"ACP stdout (non-JSON): %s",
85-
line.decode(errors="replace").rstrip(),
86-
)
87-
except Exception:
88-
pass
96+
logger.debug(
97+
"ACP stdout (non-JSON): %s",
98+
line.decode(errors="replace").rstrip(),
99+
)
89100
except Exception:
101+
logger.debug("_filter_jsonrpc_lines stopped", exc_info=True)
90102
dest.feed_eof()
91103

92104

@@ -120,15 +132,6 @@ async def session_update(
120132
update: Any,
121133
**kwargs: Any, # noqa: ARG002
122134
) -> None:
123-
from acp.schema import (
124-
AgentMessageChunk,
125-
AgentThoughtChunk,
126-
TextContentBlock,
127-
ToolCallProgress,
128-
ToolCallStart,
129-
UsageUpdate,
130-
)
131-
132135
if isinstance(update, AgentMessageChunk):
133136
if isinstance(update.content, TextContentBlock):
134137
text = update.content.text
@@ -137,7 +140,7 @@ async def session_update(
137140
try:
138141
self.on_token(text)
139142
except Exception:
140-
pass
143+
logger.debug("on_token callback failed", exc_info=True)
141144
elif isinstance(update, AgentThoughtChunk):
142145
if isinstance(update.content, TextContentBlock):
143146
self.accumulated_thoughts.append(update.content.text)
@@ -163,11 +166,6 @@ async def request_permission(
163166
**kwargs: Any, # noqa: ARG002
164167
) -> Any:
165168
"""Auto-approve all permission requests from the ACP server."""
166-
from acp.schema import (
167-
AllowedOutcome,
168-
RequestPermissionResponse,
169-
)
170-
171169
# Pick the first option (usually "allow once")
172170
option_id = options[0].option_id if options else "allow_once"
173171
logger.info(
@@ -260,7 +258,7 @@ class ACPAgent(AgentBase):
260258
"""
261259

262260
# Override required fields with ACP-appropriate defaults
263-
llm: LLM = Field(default_factory=_make_sentinel_llm)
261+
llm: LLM = Field(default_factory=_make_dummy_llm)
264262
tools: list[Tool] = Field(default_factory=list)
265263
include_default_tools: list[str] = Field(default_factory=list)
266264

@@ -322,10 +320,6 @@ def init_state(
322320
"ACPAgent does not support condenser; "
323321
"the ACP server manages its own context"
324322
)
325-
if self.critic is not None:
326-
raise NotImplementedError(
327-
"ACPAgent does not support critic; "
328-
)
329323
if self.agent_context is not None:
330324
raise NotImplementedError(
331325
"ACPAgent does not support agent_context; "
@@ -354,15 +348,6 @@ def init_state(
354348

355349
def _start_acp_server(self, state: ConversationState) -> None:
356350
"""Start the ACP subprocess and initialize the session."""
357-
import asyncio
358-
359-
from acp.client.connection import (
360-
ClientSideConnection,
361-
)
362-
from acp.transports import (
363-
default_environment,
364-
)
365-
366351
client = _OpenHandsACPClient()
367352
client._llm_ref = self.llm
368353
self._client = client
@@ -449,9 +434,6 @@ def step(
449434
self._client.on_token = on_token
450435

451436
try:
452-
import asyncio
453-
454-
from acp.helpers import text_block
455437

456438
async def _prompt() -> Any:
457439
response = await self._conn.prompt(
@@ -490,7 +472,7 @@ async def _prompt() -> Any:
490472
try:
491473
self.llm.telemetry._stats_update_callback()
492474
except Exception:
493-
pass
475+
logger.debug("Stats update callback failed", exc_info=True)
494476

495477
# Build response message
496478
response_text = "".join(self._client.accumulated_text)

uv.lock

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)