openhands 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhands-1.3.0.dist-info/METADATA +56 -0
- openhands-1.3.0.dist-info/RECORD +43 -0
- openhands-1.3.0.dist-info/WHEEL +4 -0
- openhands-1.3.0.dist-info/entry_points.txt +3 -0
- openhands-1.3.0.dist-info/licenses/LICENSE +21 -0
- openhands_cli/__init__.py +9 -0
- openhands_cli/acp_impl/README.md +68 -0
- openhands_cli/acp_impl/__init__.py +1 -0
- openhands_cli/acp_impl/agent.py +483 -0
- openhands_cli/acp_impl/event.py +512 -0
- openhands_cli/acp_impl/main.py +21 -0
- openhands_cli/acp_impl/test_utils.py +174 -0
- openhands_cli/acp_impl/utils/__init__.py +14 -0
- openhands_cli/acp_impl/utils/convert.py +103 -0
- openhands_cli/acp_impl/utils/mcp.py +66 -0
- openhands_cli/acp_impl/utils/resources.py +189 -0
- openhands_cli/agent_chat.py +236 -0
- openhands_cli/argparsers/main_parser.py +78 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +224 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/locations.py +14 -0
- openhands_cli/pt_style.py +33 -0
- openhands_cli/runner.py +190 -0
- openhands_cli/setup.py +136 -0
- openhands_cli/simple_main.py +71 -0
- openhands_cli/tui/__init__.py +6 -0
- openhands_cli/tui/settings/mcp_screen.py +225 -0
- openhands_cli/tui/settings/settings_screen.py +226 -0
- openhands_cli/tui/settings/store.py +132 -0
- openhands_cli/tui/status.py +110 -0
- openhands_cli/tui/tui.py +120 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/tui/visualizer.py +22 -0
- openhands_cli/user_actions/__init__.py +18 -0
- openhands_cli/user_actions/agent_action.py +82 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +176 -0
- openhands_cli/user_actions/types.py +17 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands_cli/utils.py +122 -0
- openhands_cli/version_check.py +83 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""OpenHands Agent Client Protocol (ACP) server implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from acp import (
|
|
11
|
+
Agent as ACPAgent,
|
|
12
|
+
AgentSideConnection,
|
|
13
|
+
InitializeRequest,
|
|
14
|
+
InitializeResponse,
|
|
15
|
+
NewSessionRequest,
|
|
16
|
+
NewSessionResponse,
|
|
17
|
+
PromptRequest,
|
|
18
|
+
PromptResponse,
|
|
19
|
+
RequestError,
|
|
20
|
+
SessionNotification,
|
|
21
|
+
stdio_streams,
|
|
22
|
+
)
|
|
23
|
+
from acp.schema import (
|
|
24
|
+
AgentCapabilities,
|
|
25
|
+
AgentMessageChunk,
|
|
26
|
+
AuthenticateRequest,
|
|
27
|
+
AuthenticateResponse,
|
|
28
|
+
CancelNotification,
|
|
29
|
+
Implementation,
|
|
30
|
+
LoadSessionRequest,
|
|
31
|
+
LoadSessionResponse,
|
|
32
|
+
McpCapabilities,
|
|
33
|
+
PromptCapabilities,
|
|
34
|
+
SetSessionModelRequest,
|
|
35
|
+
SetSessionModelResponse,
|
|
36
|
+
SetSessionModeRequest,
|
|
37
|
+
SetSessionModeResponse,
|
|
38
|
+
TextContentBlock,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
from openhands.sdk import (
|
|
42
|
+
BaseConversation,
|
|
43
|
+
Conversation,
|
|
44
|
+
Message,
|
|
45
|
+
Workspace,
|
|
46
|
+
)
|
|
47
|
+
from openhands.sdk.event import Event
|
|
48
|
+
from openhands_cli import __version__
|
|
49
|
+
from openhands_cli.acp_impl.event import EventSubscriber
|
|
50
|
+
from openhands_cli.acp_impl.utils import (
|
|
51
|
+
RESOURCE_SKILL,
|
|
52
|
+
convert_acp_mcp_servers_to_agent_format,
|
|
53
|
+
convert_acp_prompt_to_message_content,
|
|
54
|
+
)
|
|
55
|
+
from openhands_cli.locations import CONVERSATIONS_DIR, WORK_DIR
|
|
56
|
+
from openhands_cli.setup import MissingAgentSpec, load_agent_specs
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OpenHandsACPAgent(ACPAgent):
|
|
63
|
+
"""OpenHands Agent Client Protocol implementation."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, conn: AgentSideConnection):
|
|
66
|
+
"""Initialize the OpenHands ACP agent.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
conn: ACP connection for sending notifications
|
|
70
|
+
"""
|
|
71
|
+
self._conn = conn
|
|
72
|
+
# Cache of active conversations to preserve state (pause, confirmation, etc.)
|
|
73
|
+
# across multiple operations on the same session
|
|
74
|
+
self._active_sessions: dict[str, BaseConversation] = {}
|
|
75
|
+
# Track running tasks for each session to ensure proper cleanup on cancel
|
|
76
|
+
self._running_tasks: dict[str, asyncio.Task] = {}
|
|
77
|
+
|
|
78
|
+
logger.info("OpenHands ACP Agent initialized")
|
|
79
|
+
|
|
80
|
+
def _get_or_create_conversation(
|
|
81
|
+
self,
|
|
82
|
+
session_id: str,
|
|
83
|
+
working_dir: str | None = None,
|
|
84
|
+
mcp_servers: dict[str, dict[str, Any]] | None = None,
|
|
85
|
+
) -> BaseConversation:
|
|
86
|
+
"""Get an active conversation from cache or create/load it.
|
|
87
|
+
|
|
88
|
+
This maintains conversation state (pause, confirmation, etc.) across
|
|
89
|
+
multiple operations on the same session.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
session_id: Session/conversation ID (UUID string)
|
|
93
|
+
working_dir: Working directory for workspace (only for new sessions)
|
|
94
|
+
mcp_servers: MCP servers config (only for new sessions)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Cached or newly created/loaded conversation
|
|
98
|
+
"""
|
|
99
|
+
# Check if we already have this conversation active
|
|
100
|
+
if session_id in self._active_sessions:
|
|
101
|
+
logger.debug(f"Using cached conversation for session {session_id}")
|
|
102
|
+
return self._active_sessions[session_id]
|
|
103
|
+
|
|
104
|
+
# Create/load new conversation
|
|
105
|
+
logger.debug(f"Creating new conversation for session {session_id}")
|
|
106
|
+
conversation = self._setup_acp_conversation(
|
|
107
|
+
session_id=session_id,
|
|
108
|
+
working_dir=working_dir,
|
|
109
|
+
mcp_servers=mcp_servers,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Cache it for future operations
|
|
113
|
+
self._active_sessions[session_id] = conversation
|
|
114
|
+
return conversation
|
|
115
|
+
|
|
116
|
+
def _setup_acp_conversation(
|
|
117
|
+
self,
|
|
118
|
+
session_id: str,
|
|
119
|
+
working_dir: str | None = None,
|
|
120
|
+
mcp_servers: dict[str, dict[str, Any]] | None = None,
|
|
121
|
+
) -> BaseConversation:
|
|
122
|
+
"""Set up a conversation for ACP with event streaming support.
|
|
123
|
+
|
|
124
|
+
This function reuses the resume logic from
|
|
125
|
+
openhands_cli.setup.setup_conversation but adapts it for ACP by using
|
|
126
|
+
EventSubscriber instead of CLIVisualizer.
|
|
127
|
+
|
|
128
|
+
The SDK's Conversation class automatically:
|
|
129
|
+
- Loads from disk if conversation_id exists in persistence_dir
|
|
130
|
+
- Creates a new conversation if it doesn't exist
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
session_id: Session/conversation ID (UUID string)
|
|
134
|
+
working_dir: Working directory for the workspace. Defaults to WORK_DIR.
|
|
135
|
+
mcp_servers: Optional MCP servers configuration
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Configured conversation that's either loaded from disk or newly created
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
MissingAgentSpec: If agent configuration is missing
|
|
142
|
+
"""
|
|
143
|
+
# Load agent specs (same as setup_conversation)
|
|
144
|
+
agent = load_agent_specs(
|
|
145
|
+
conversation_id=session_id,
|
|
146
|
+
mcp_servers=mcp_servers,
|
|
147
|
+
skills=[RESOURCE_SKILL],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Validate and setup workspace
|
|
151
|
+
if working_dir is None:
|
|
152
|
+
working_dir = WORK_DIR
|
|
153
|
+
working_path = Path(working_dir)
|
|
154
|
+
|
|
155
|
+
if not working_path.exists():
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Working directory {working_dir} doesn't exist, creating it"
|
|
158
|
+
)
|
|
159
|
+
working_path.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
if not working_path.is_dir():
|
|
162
|
+
raise RequestError.invalid_params(
|
|
163
|
+
{"reason": f"Working directory path is not a directory: {working_dir}"}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
workspace = Workspace(working_dir=str(working_path))
|
|
167
|
+
|
|
168
|
+
# Create event subscriber for streaming updates (ACP-specific)
|
|
169
|
+
subscriber = EventSubscriber(session_id, self._conn)
|
|
170
|
+
|
|
171
|
+
# Get the current event loop for the callback
|
|
172
|
+
loop = asyncio.get_event_loop()
|
|
173
|
+
|
|
174
|
+
def sync_callback(event: Event) -> None:
|
|
175
|
+
"""Synchronous wrapper that schedules async event handling."""
|
|
176
|
+
asyncio.run_coroutine_threadsafe(subscriber(event), loop)
|
|
177
|
+
|
|
178
|
+
# Create conversation with persistence support
|
|
179
|
+
# The SDK automatically loads from disk if conversation_id exists
|
|
180
|
+
conversation = Conversation(
|
|
181
|
+
agent=agent,
|
|
182
|
+
workspace=workspace,
|
|
183
|
+
persistence_dir=CONVERSATIONS_DIR,
|
|
184
|
+
conversation_id=UUID(session_id),
|
|
185
|
+
callbacks=[sync_callback],
|
|
186
|
+
visualizer=None, # No visualizer needed for ACP
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# # Set up security analyzer (same as setup_conversation with confirmation mode)
|
|
190
|
+
# conversation.set_security_analyzer(LLMSecurityAnalyzer())
|
|
191
|
+
# conversation.set_confirmation_policy(AlwaysConfirm())
|
|
192
|
+
# TODO: implement later
|
|
193
|
+
|
|
194
|
+
return conversation
|
|
195
|
+
|
|
196
|
+
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
|
|
197
|
+
"""Initialize the ACP protocol."""
|
|
198
|
+
logger.info(f"Initializing ACP with protocol version: {params.protocolVersion}")
|
|
199
|
+
|
|
200
|
+
# Check if agent is configured
|
|
201
|
+
try:
|
|
202
|
+
load_agent_specs()
|
|
203
|
+
auth_methods = []
|
|
204
|
+
logger.info("Agent configured, no authentication required")
|
|
205
|
+
except MissingAgentSpec:
|
|
206
|
+
# Agent not configured - this shouldn't happen in production
|
|
207
|
+
# but we'll return empty auth methods for now
|
|
208
|
+
auth_methods = []
|
|
209
|
+
logger.warning("Agent not configured - users should run 'openhands' first")
|
|
210
|
+
|
|
211
|
+
return InitializeResponse(
|
|
212
|
+
protocolVersion=params.protocolVersion,
|
|
213
|
+
authMethods=auth_methods,
|
|
214
|
+
agentCapabilities=AgentCapabilities(
|
|
215
|
+
loadSession=True,
|
|
216
|
+
mcpCapabilities=McpCapabilities(http=True, sse=True),
|
|
217
|
+
promptCapabilities=PromptCapabilities(
|
|
218
|
+
audio=False,
|
|
219
|
+
embeddedContext=True,
|
|
220
|
+
image=True,
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
agentInfo=Implementation(
|
|
224
|
+
name="OpenHands CLI ACP Agent",
|
|
225
|
+
version=__version__,
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
async def authenticate(
|
|
230
|
+
self, params: AuthenticateRequest
|
|
231
|
+
) -> AuthenticateResponse | None:
|
|
232
|
+
"""Authenticate the client (no-op for now)."""
|
|
233
|
+
logger.info(f"Authentication requested with method: {params.methodId}")
|
|
234
|
+
return AuthenticateResponse()
|
|
235
|
+
|
|
236
|
+
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
|
|
237
|
+
"""Create a new conversation session."""
|
|
238
|
+
session_id = str(uuid.uuid4())
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
# Convert ACP MCP servers to Agent format
|
|
242
|
+
mcp_servers_dict = None
|
|
243
|
+
if params.mcpServers:
|
|
244
|
+
mcp_servers_dict = convert_acp_mcp_servers_to_agent_format(
|
|
245
|
+
params.mcpServers
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Validate working directory
|
|
249
|
+
working_dir = params.cwd or str(Path.cwd())
|
|
250
|
+
logger.info(f"Using working directory: {working_dir}")
|
|
251
|
+
|
|
252
|
+
# Create conversation and cache it for future operations
|
|
253
|
+
# This reuses the same pattern as openhands --resume
|
|
254
|
+
conversation = self._get_or_create_conversation(
|
|
255
|
+
session_id=session_id,
|
|
256
|
+
working_dir=working_dir,
|
|
257
|
+
mcp_servers=mcp_servers_dict,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
logger.info(
|
|
261
|
+
f"Created new session {session_id} with model: "
|
|
262
|
+
f"{conversation.agent.llm.model}" # type: ignore[attr-defined]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return NewSessionResponse(sessionId=session_id)
|
|
266
|
+
|
|
267
|
+
except MissingAgentSpec as e:
|
|
268
|
+
logger.error(f"Agent not configured: {e}")
|
|
269
|
+
raise RequestError.internal_error(
|
|
270
|
+
{
|
|
271
|
+
"reason": "Agent not configured",
|
|
272
|
+
"details": "Please run 'openhands' to configure the agent first.",
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
except RequestError:
|
|
276
|
+
# Re-raise RequestError as-is
|
|
277
|
+
raise
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Failed to create new session: {e}", exc_info=True)
|
|
280
|
+
raise RequestError.internal_error(
|
|
281
|
+
{"reason": "Failed to create new session", "details": str(e)}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
async def prompt(self, params: PromptRequest) -> PromptResponse:
|
|
285
|
+
"""Handle a prompt request."""
|
|
286
|
+
session_id = params.sessionId
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
# Get or create conversation (preserves state like pause/confirmation)
|
|
290
|
+
conversation = self._get_or_create_conversation(session_id=session_id)
|
|
291
|
+
|
|
292
|
+
# Convert ACP prompt format to OpenHands message content
|
|
293
|
+
message_content = convert_acp_prompt_to_message_content(params.prompt)
|
|
294
|
+
|
|
295
|
+
if not message_content:
|
|
296
|
+
return PromptResponse(stopReason="end_turn")
|
|
297
|
+
|
|
298
|
+
# Send the message with potentially multiple content types
|
|
299
|
+
# (text + images)
|
|
300
|
+
message = Message(role="user", content=message_content)
|
|
301
|
+
conversation.send_message(message)
|
|
302
|
+
|
|
303
|
+
# Run the conversation asynchronously
|
|
304
|
+
# Callbacks are already set up when conversation was created
|
|
305
|
+
# Track the running task so cancel() can wait for proper cleanup
|
|
306
|
+
run_task = asyncio.create_task(asyncio.to_thread(conversation.run))
|
|
307
|
+
self._running_tasks[session_id] = run_task
|
|
308
|
+
try:
|
|
309
|
+
await run_task
|
|
310
|
+
finally:
|
|
311
|
+
# Clean up task tracking
|
|
312
|
+
self._running_tasks.pop(session_id, None)
|
|
313
|
+
|
|
314
|
+
# Return the final response
|
|
315
|
+
return PromptResponse(stopReason="end_turn")
|
|
316
|
+
|
|
317
|
+
except RequestError:
|
|
318
|
+
# Re-raise RequestError as-is
|
|
319
|
+
raise
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.error(f"Error processing prompt: {e}", exc_info=True)
|
|
322
|
+
# Send error notification to client
|
|
323
|
+
await self._conn.sessionUpdate(
|
|
324
|
+
SessionNotification(
|
|
325
|
+
sessionId=session_id,
|
|
326
|
+
update=AgentMessageChunk(
|
|
327
|
+
sessionUpdate="agent_message_chunk",
|
|
328
|
+
content=TextContentBlock(type="text", text=f"Error: {str(e)}"),
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
raise RequestError.internal_error(
|
|
333
|
+
{"reason": "Failed to process prompt", "details": str(e)}
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
async def _wait_for_task_completion(
|
|
337
|
+
self, task: asyncio.Task, session_id: str, timeout: float = 10.0
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Wait for a task to complete and handle cancellation if needed."""
|
|
340
|
+
try:
|
|
341
|
+
await asyncio.wait_for(task, timeout=timeout)
|
|
342
|
+
except TimeoutError:
|
|
343
|
+
logger.warning(
|
|
344
|
+
f"Conversation thread did not stop within timeout for session "
|
|
345
|
+
f"{session_id}"
|
|
346
|
+
)
|
|
347
|
+
task.cancel()
|
|
348
|
+
try:
|
|
349
|
+
await task
|
|
350
|
+
except asyncio.CancelledError:
|
|
351
|
+
pass
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Error while waiting for conversation to stop: {e}")
|
|
354
|
+
raise RequestError.internal_error(
|
|
355
|
+
{
|
|
356
|
+
"reason": "Error during conversation cancellation",
|
|
357
|
+
"details": str(e),
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def cancel(self, params: CancelNotification) -> None:
|
|
362
|
+
"""Cancel the current operation."""
|
|
363
|
+
logger.info(f"Cancel requested for session: {params.sessionId}")
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
conversation = self._get_or_create_conversation(session_id=params.sessionId)
|
|
367
|
+
conversation.pause()
|
|
368
|
+
|
|
369
|
+
running_task = self._running_tasks.get(params.sessionId)
|
|
370
|
+
if not running_task or running_task.done():
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
logger.debug(
|
|
374
|
+
f"Waiting for conversation thread to terminate for session "
|
|
375
|
+
f"{params.sessionId}"
|
|
376
|
+
)
|
|
377
|
+
await self._wait_for_task_completion(running_task, params.sessionId)
|
|
378
|
+
|
|
379
|
+
except RequestError:
|
|
380
|
+
raise
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Failed to cancel session {params.sessionId}: {e}")
|
|
383
|
+
raise RequestError.internal_error(
|
|
384
|
+
{"reason": "Failed to cancel session", "details": str(e)}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
async def loadSession(
|
|
388
|
+
self, params: LoadSessionRequest
|
|
389
|
+
) -> LoadSessionResponse | None:
|
|
390
|
+
"""Load an existing session and replay conversation history.
|
|
391
|
+
|
|
392
|
+
This implements the same logic as 'openhands --resume <session_id>':
|
|
393
|
+
- Uses _setup_acp_conversation which calls the SDK's Conversation constructor
|
|
394
|
+
- The SDK automatically loads from persistence_dir if conversation_id exists
|
|
395
|
+
- Streams the loaded history back to the client
|
|
396
|
+
|
|
397
|
+
Per ACP spec (https://agentclientprotocol.com/protocol/session-setup#loading-sessions):
|
|
398
|
+
- Server should load the session state from persistent storage
|
|
399
|
+
- Replay the conversation history to the client via sessionUpdate notifications
|
|
400
|
+
"""
|
|
401
|
+
session_id = params.sessionId
|
|
402
|
+
logger.info(f"Loading session: {session_id}")
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
# Validate session ID format
|
|
406
|
+
try:
|
|
407
|
+
UUID(session_id)
|
|
408
|
+
except ValueError:
|
|
409
|
+
raise RequestError.invalid_params(
|
|
410
|
+
{"reason": "Invalid session ID format", "sessionId": session_id}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Get or create conversation (loads from disk if not in cache)
|
|
414
|
+
# The SDK's Conversation class automatically loads from disk if the
|
|
415
|
+
# conversation_id exists in persistence_dir
|
|
416
|
+
conversation = self._get_or_create_conversation(session_id=session_id)
|
|
417
|
+
|
|
418
|
+
# Check if there's actually any history to load
|
|
419
|
+
if not conversation.state.events:
|
|
420
|
+
logger.warning(
|
|
421
|
+
f"Session {session_id} has no history (new or empty session)"
|
|
422
|
+
)
|
|
423
|
+
return LoadSessionResponse()
|
|
424
|
+
|
|
425
|
+
# Stream conversation history to client by reusing EventSubscriber
|
|
426
|
+
# This ensures consistent event handling with live conversations
|
|
427
|
+
logger.info(
|
|
428
|
+
f"Streaming {len(conversation.state.events)} events from "
|
|
429
|
+
f"conversation history"
|
|
430
|
+
)
|
|
431
|
+
subscriber = EventSubscriber(session_id, self._conn)
|
|
432
|
+
for event in conversation.state.events:
|
|
433
|
+
await subscriber(event)
|
|
434
|
+
|
|
435
|
+
logger.info(f"Successfully loaded session {session_id}")
|
|
436
|
+
return LoadSessionResponse()
|
|
437
|
+
|
|
438
|
+
except RequestError:
|
|
439
|
+
# Re-raise RequestError as-is
|
|
440
|
+
raise
|
|
441
|
+
except Exception as e:
|
|
442
|
+
logger.error(f"Failed to load session {session_id}: {e}", exc_info=True)
|
|
443
|
+
raise RequestError.internal_error(
|
|
444
|
+
{"reason": "Failed to load session", "details": str(e)}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def setSessionMode(
|
|
448
|
+
self, params: SetSessionModeRequest
|
|
449
|
+
) -> SetSessionModeResponse | None:
|
|
450
|
+
"""Set session mode (no-op for now)."""
|
|
451
|
+
logger.info(f"Set session mode requested: {params.sessionId}")
|
|
452
|
+
return SetSessionModeResponse()
|
|
453
|
+
|
|
454
|
+
async def setSessionModel(
|
|
455
|
+
self, params: SetSessionModelRequest
|
|
456
|
+
) -> SetSessionModelResponse | None:
|
|
457
|
+
"""Set session model (no-op for now)."""
|
|
458
|
+
logger.info(f"Set session model requested: {params.sessionId}")
|
|
459
|
+
return SetSessionModelResponse()
|
|
460
|
+
|
|
461
|
+
async def extMethod(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
462
|
+
"""Extension method (not supported)."""
|
|
463
|
+
logger.info(f"Extension method '{method}' requested with params: {params}")
|
|
464
|
+
return {"error": "extMethod not supported"}
|
|
465
|
+
|
|
466
|
+
async def extNotification(self, method: str, params: dict[str, Any]) -> None:
|
|
467
|
+
"""Extension notification (no-op for now)."""
|
|
468
|
+
logger.info(f"Extension notification '{method}' received with params: {params}")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
async def run_acp_server() -> None:
|
|
472
|
+
"""Run the OpenHands ACP server."""
|
|
473
|
+
logger.info("Starting OpenHands ACP server...")
|
|
474
|
+
|
|
475
|
+
reader, writer = await stdio_streams()
|
|
476
|
+
|
|
477
|
+
def create_agent(conn: AgentSideConnection) -> OpenHandsACPAgent:
|
|
478
|
+
return OpenHandsACPAgent(conn)
|
|
479
|
+
|
|
480
|
+
AgentSideConnection(create_agent, writer, reader)
|
|
481
|
+
|
|
482
|
+
# Keep the server running
|
|
483
|
+
await asyncio.Event().wait()
|