Skip to content

Commit bc7ea21

Browse files
Co1linxingyaowwopenhands-agent
authored
fix: several issues related to scalability (#1619)
Co-authored-by: Xingyao Wang <[email protected]> Co-authored-by: openhands <[email protected]>
1 parent 4d217c1 commit bc7ea21

File tree

7 files changed

+54
-17
lines changed

7 files changed

+54
-17
lines changed

openhands-sdk/openhands/sdk/agent/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,5 +503,5 @@ def tools_map(self) -> dict[str, ToolDefinition]:
503503
RuntimeError: If the agent has not been initialized.
504504
"""
505505
if not self._initialized:
506-
raise RuntimeError("Agent not initialized; call initialize() before use")
506+
raise RuntimeError("Agent not initialized; call _initialize() before use")
507507
return self._tools

openhands-sdk/openhands/sdk/conversation/conversation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def __new__(
7474
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
7575
) = DefaultConversationVisualizer,
7676
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
77+
delete_on_close: bool = False,
7778
) -> "LocalConversation": ...
7879

7980
@overload
@@ -96,6 +97,7 @@ def __new__(
9697
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
9798
) = DefaultConversationVisualizer,
9899
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
100+
delete_on_close: bool = False,
99101
) -> "RemoteConversation": ...
100102

101103
def __new__(
@@ -118,6 +120,7 @@ def __new__(
118120
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
119121
) = DefaultConversationVisualizer,
120122
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
123+
delete_on_close: bool = False,
121124
) -> BaseConversation:
122125
from openhands.sdk.conversation.impl.local_conversation import LocalConversation
123126
from openhands.sdk.conversation.impl.remote_conversation import (
@@ -143,6 +146,7 @@ def __new__(
143146
visualizer=visualizer,
144147
workspace=workspace,
145148
secrets=secrets,
149+
delete_on_close=delete_on_close,
146150
)
147151

148152
return LocalConversation(
@@ -159,4 +163,5 @@ def __new__(
159163
workspace=workspace,
160164
persistence_dir=persistence_dir,
161165
secrets=secrets,
166+
delete_on_close=delete_on_close,
162167
)

openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class LocalConversation(BaseConversation):
6565
llm_registry: LLMRegistry
6666
_cleanup_initiated: bool
6767
_hook_processor: HookEventProcessor | None
68+
delete_on_close: bool = True
6869
# Plugin lazy loading state
6970
_plugin_specs: list[PluginSource] | None
7071
_resolved_plugins: list[ResolvedPluginSource] | None
@@ -90,6 +91,7 @@ def __init__(
9091
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
9192
) = DefaultConversationVisualizer,
9293
secrets: Mapping[str, SecretValue] | None = None,
94+
delete_on_close: bool = True,
9395
cipher: Cipher | None = None,
9496
**_: object,
9597
):
@@ -242,6 +244,7 @@ def _default_callback(e):
242244

243245
atexit.register(self.close)
244246
self._start_observability_span(str(desired_id))
247+
self.delete_on_close = delete_on_close
245248

246249
@property
247250
def id(self) -> ConversationID:
@@ -708,20 +711,23 @@ def close(self) -> None:
708711
except AttributeError:
709712
# Object may be partially constructed; span fields may be missing.
710713
pass
711-
try:
712-
tools_map = self.agent.tools_map
713-
except (AttributeError, RuntimeError):
714-
# Agent not initialized or partially constructed
715-
return
716-
for tool in tools_map.values():
714+
if self.delete_on_close:
717715
try:
718-
executable_tool = tool.as_executable()
719-
executable_tool.executor.close()
720-
except NotImplementedError:
721-
# Tool has no executor, skip it without erroring
722-
continue
723-
except Exception as e:
724-
logger.warning(f"Error closing executor for tool '{tool.name}': {e}")
716+
tools_map = self.agent.tools_map
717+
except (AttributeError, RuntimeError):
718+
# Agent not initialized or partially constructed
719+
return
720+
for tool in tools_map.values():
721+
try:
722+
executable_tool = tool.as_executable()
723+
executable_tool.executor.close()
724+
except NotImplementedError:
725+
# Tool has no executor, skip it without erroring
726+
continue
727+
except Exception as e:
728+
logger.warning(
729+
f"Error closing executor for tool '{tool.name}': {e}"
730+
)
725731

726732
def ask_agent(self, question: str) -> str:
727733
"""Ask the agent a simple, stateless question and get a direct LLM response.

openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ class RemoteConversation(BaseConversation):
555555
_client: httpx.Client
556556
_hook_processor: HookEventProcessor | None
557557
_cleanup_initiated: bool
558+
delete_on_close: bool = False
558559

559560
def __init__(
560561
self,
@@ -573,6 +574,7 @@ def __init__(
573574
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
574575
) = DefaultConversationVisualizer,
575576
secrets: Mapping[str, SecretValue] | None = None,
577+
delete_on_close: bool = False,
576578
**_: object,
577579
) -> None:
578580
"""Remote conversation proxy that talks to an agent server.
@@ -765,6 +767,7 @@ def __init__(
765767
)
766768
self._hook_processor = HookEventProcessor(hook_manager=hook_manager)
767769
self._hook_processor.run_session_start()
770+
self.delete_on_close = delete_on_close
768771

769772
def _create_llm_completion_log_callback(self) -> ConversationCallbackType:
770773
"""Create a callback that writes LLM completion logs to client filesystem."""
@@ -1134,6 +1137,13 @@ def close(self) -> None:
11341137
pass
11351138

11361139
self._end_observability_span()
1140+
if self.delete_on_close:
1141+
try:
1142+
# trigger server-side delete_conversation to release resources
1143+
# like tmux sessions
1144+
_send_request(self._client, "DELETE", f"/api/conversations/{self.id}")
1145+
except Exception:
1146+
pass
11371147

11381148
def __del__(self) -> None:
11391149
try:

openhands-sdk/openhands/sdk/workspace/remote/base.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,17 @@ def client(self) -> httpx.Client:
5050
if client is None:
5151
# Configure reasonable timeouts for HTTP requests
5252
# - connect: 10 seconds to establish connection
53-
# - read: 60 seconds to read response (for LLM operations)
53+
# - read: 600 seconds (10 minutes) to read response (for LLM operations)
5454
# - write: 10 seconds to send request
5555
# - pool: 10 seconds to get connection from pool
56-
timeout = httpx.Timeout(connect=10.0, read=60.0, write=10.0, pool=10.0)
56+
timeout = httpx.Timeout(
57+
connect=10.0, read=self.read_timeout, write=10.0, pool=10.0
58+
)
5759
client = httpx.Client(
58-
base_url=self.host, timeout=timeout, headers=self._headers
60+
base_url=self.host,
61+
timeout=timeout,
62+
headers=self._headers,
63+
limits=httpx.Limits(max_connections=self.max_connections),
5964
)
6065
self._client = client
6166
return client

openhands-sdk/openhands/sdk/workspace/remote/remote_workspace_mixin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ class RemoteWorkspaceMixin(BaseModel):
2525
working_dir: str = Field(
2626
description="The working directory for agent operations and tool execution."
2727
)
28+
read_timeout: float = Field(
29+
default=600.0,
30+
description="Timeout in seconds for reading operations of httpx.Client.",
31+
)
32+
max_connections: int | None = Field(
33+
default=None,
34+
description="Maximum number of connections for httpx.Client. "
35+
"None means no limit, useful for running many conversations in parallel.",
36+
)
2837

2938
def model_post_init(self, context: Any) -> None:
3039
# Set up remote host

openhands-workspace/openhands/workspace/docker/workspace.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ def _start_container(self, image: str, context: Any) -> None:
235235
"--platform",
236236
self.platform,
237237
"--rm",
238+
"--ulimit",
239+
"nofile=65536:65536", # prevent "too many open files" errors
238240
"--name",
239241
f"agent-server-{uuid.uuid4()}",
240242
*flags,

0 commit comments

Comments
 (0)