openhands-agent-server 1.7.2__tar.gz → 1.7.4__tar.gz

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.
Files changed (43) hide show
  1. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/conversation_service.py +58 -0
  3. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/env_parser.py +34 -4
  4. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/event_service.py +21 -0
  5. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/models.py +8 -0
  6. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/sockets.py +2 -2
  7. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  8. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/pyproject.toml +1 -1
  9. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/__init__.py +0 -0
  10. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/__main__.py +0 -0
  11. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/api.py +0 -0
  12. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/bash_router.py +0 -0
  13. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/bash_service.py +0 -0
  14. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/config.py +0 -0
  15. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/conversation_router.py +0 -0
  16. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/dependencies.py +0 -0
  17. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/desktop_router.py +0 -0
  18. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/desktop_service.py +0 -0
  19. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/docker/Dockerfile +0 -0
  20. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/docker/build.py +0 -0
  21. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/docker/wallpaper.svg +0 -0
  22. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/event_router.py +0 -0
  23. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/file_router.py +0 -0
  24. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/git_router.py +0 -0
  25. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/logging_config.py +0 -0
  26. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/middleware.py +0 -0
  27. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/openapi.py +0 -0
  28. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/pub_sub.py +0 -0
  29. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/py.typed +0 -0
  30. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/server_details_router.py +0 -0
  31. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/tool_preload_service.py +0 -0
  32. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/tool_router.py +0 -0
  33. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/utils.py +0 -0
  34. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  35. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  36. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/vscode_router.py +0 -0
  37. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands/agent_server/vscode_service.py +0 -0
  38. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
  39. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  40. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  41. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/requires.txt +0 -0
  42. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/openhands_agent_server.egg-info/top_level.txt +0 -0
  43. {openhands_agent_server-1.7.2 → openhands_agent_server-1.7.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.19
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import importlib
2
3
  import logging
3
4
  from dataclasses import dataclass, field
4
5
  from pathlib import Path
@@ -208,6 +209,32 @@ class ConversationService:
208
209
  )
209
210
  return conversation_info, False
210
211
 
212
+ # Dynamically register tools from client's registry
213
+ if request.tool_module_qualnames:
214
+ import importlib
215
+
216
+ for tool_name, module_qualname in request.tool_module_qualnames.items():
217
+ try:
218
+ # Import the module to trigger tool auto-registration
219
+ importlib.import_module(module_qualname)
220
+ logger.debug(
221
+ f"Tool '{tool_name}' registered via module '{module_qualname}'"
222
+ )
223
+ except ImportError as e:
224
+ logger.warning(
225
+ f"Failed to import module '{module_qualname}' for tool "
226
+ f"'{tool_name}': {e}. Tool will not be available."
227
+ )
228
+ # Continue even if some tools fail to register
229
+ # The agent will fail gracefully if it tries to use unregistered
230
+ # tools
231
+ if request.tool_module_qualnames:
232
+ logger.info(
233
+ f"Dynamically registered {len(request.tool_module_qualnames)} "
234
+ f"tools for conversation {conversation_id}: "
235
+ f"{list(request.tool_module_qualnames.keys())}"
236
+ )
237
+
211
238
  stored = StoredConversation(id=conversation_id, **request.model_dump())
212
239
  event_service = await self._start_event_service(stored)
213
240
  initial_message = request.initial_message
@@ -378,6 +405,34 @@ class ConversationService:
378
405
  "cipher": self.cipher,
379
406
  },
380
407
  )
408
+ # Dynamically register tools when resuming persisted conversations
409
+ if stored.tool_module_qualnames:
410
+ for (
411
+ tool_name,
412
+ module_qualname,
413
+ ) in stored.tool_module_qualnames.items():
414
+ try:
415
+ # Import the module to trigger tool auto-registration
416
+ importlib.import_module(module_qualname)
417
+ logger.debug(
418
+ f"Tool '{tool_name}' registered via module "
419
+ f"'{module_qualname}' when resuming conversation "
420
+ f"{stored.id}"
421
+ )
422
+ except ImportError as e:
423
+ logger.warning(
424
+ f"Failed to import module '{module_qualname}' for "
425
+ f"tool '{tool_name}' when resuming conversation "
426
+ f"{stored.id}: {e}. Tool will not be available."
427
+ )
428
+ # Continue even if some tools fail to register
429
+ if stored.tool_module_qualnames:
430
+ logger.info(
431
+ f"Dynamically registered "
432
+ f"{len(stored.tool_module_qualnames)} tools when "
433
+ f"resuming conversation {stored.id}: "
434
+ f"{list(stored.tool_module_qualnames.keys())}"
435
+ )
381
436
  await self._start_event_service(stored)
382
437
  except Exception:
383
438
  logger.exception(
@@ -447,6 +502,9 @@ class ConversationService:
447
502
 
448
503
  try:
449
504
  await event_service.start()
505
+ # Save metadata immediately after successful start to ensure persistence
506
+ # even if the system is not shut down gracefully
507
+ await event_service.save_meta()
450
508
  except Exception:
451
509
  # Clean up the event service if startup fails
452
510
  await event_service.close()
@@ -17,7 +17,10 @@ from uuid import UUID
17
17
 
18
18
  from pydantic import BaseModel, SecretStr, TypeAdapter
19
19
 
20
- from openhands.sdk.utils.models import DiscriminatedUnionMixin
20
+ from openhands.sdk.utils.models import (
21
+ DiscriminatedUnionMixin,
22
+ get_known_concrete_subclasses,
23
+ )
21
24
 
22
25
 
23
26
  # Define Missing type
@@ -268,6 +271,26 @@ class UnionEnvParser(EnvParser):
268
271
  output.write("\n")
269
272
 
270
273
 
274
+ @dataclass
275
+ class DiscriminatedUnionEnvParser(EnvParser):
276
+ parsers: dict[str, EnvParser]
277
+
278
+ def from_env(self, key: str) -> JsonType:
279
+ kind = os.environ.get(f"{key}_KIND", MISSING)
280
+ if kind is MISSING:
281
+ return MISSING
282
+ assert isinstance(kind, str)
283
+ parser = self.parsers[kind]
284
+ parser_result = parser.from_env(key)
285
+ assert isinstance(parser_result, dict)
286
+ parser_result["kind"] = kind
287
+ return parser_result
288
+
289
+ def to_env(self, key: str, value: Any, output: IO):
290
+ parser = self.parsers[value.kind]
291
+ parser.to_env(key, value, output)
292
+
293
+
271
294
  @dataclass
272
295
  class DelayedParser(EnvParser):
273
296
  """Delayed parser for circular dependencies"""
@@ -341,9 +364,16 @@ def get_env_parser(target_type: type, parsers: dict[type, EnvParser]) -> EnvPars
341
364
  if issubclass(target_type, DiscriminatedUnionMixin) and (
342
365
  inspect.isabstract(target_type) or ABC in target_type.__bases__
343
366
  ):
344
- serializable_type = target_type.get_serializable_type()
345
- if serializable_type != target_type:
346
- return get_env_parser(target_type.get_serializable_type(), parsers)
367
+ delayed = DelayedParser()
368
+ parsers[target_type] = delayed # Prevent circular dependency
369
+ sub_parsers = {
370
+ c.__name__: get_env_parser(c, parsers)
371
+ for c in get_known_concrete_subclasses(target_type)
372
+ }
373
+ parser = DiscriminatedUnionEnvParser(sub_parsers)
374
+ delayed.parser = parser
375
+ parsers[target_type] = parser
376
+ return parser
347
377
  if issubclass(target_type, BaseModel): # type: ignore
348
378
  delayed = DelayedParser()
349
379
  parsers[target_type] = delayed # Prevent circular dependency
@@ -19,6 +19,7 @@ from openhands.sdk.conversation.state import (
19
19
  ConversationExecutionStatus,
20
20
  ConversationState,
21
21
  )
22
+ from openhands.sdk.event import AgentErrorEvent
22
23
  from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
23
24
  from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
24
25
  from openhands.sdk.security.analyzer import SecurityAnalyzerBase
@@ -439,6 +440,26 @@ class EventService:
439
440
  # Setup stats streaming for remote execution
440
441
  self._setup_stats_streaming(self._conversation.agent)
441
442
 
443
+ # If the execution_status was "running" while serialized, then the
444
+ # conversation can't possibly be running - something is wrong
445
+ state = self._conversation.state
446
+ if state.execution_status == ConversationExecutionStatus.RUNNING:
447
+ state.execution_status = ConversationExecutionStatus.ERROR
448
+ # Add error event for the first unmatched action to inform the agent
449
+ unmatched_actions = ConversationState.get_unmatched_actions(state.events)
450
+ if unmatched_actions:
451
+ first_action = unmatched_actions[0]
452
+ error_event = AgentErrorEvent(
453
+ tool_name=first_action.tool_name,
454
+ tool_call_id=first_action.tool_call_id,
455
+ error=(
456
+ "A restart occurred while this tool was in progress. "
457
+ "This may indicate a fatal memory error or system crash. "
458
+ "The tool execution was interrupted and did not complete."
459
+ ),
460
+ )
461
+ self._conversation._on_event(error_event)
462
+
442
463
  # Publish initial state update
443
464
  await self._publish_state_update()
444
465
 
@@ -98,6 +98,14 @@ class StartConversationRequest(BaseModel):
98
98
  default_factory=dict,
99
99
  description="Secrets available in the conversation",
100
100
  )
101
+ tool_module_qualnames: dict[str, str] = Field(
102
+ default_factory=dict,
103
+ description=(
104
+ "Mapping of tool names to their module qualnames from the client's "
105
+ "registry. These modules will be dynamically imported on the server "
106
+ "to register the tools for this conversation."
107
+ ),
108
+ )
101
109
 
102
110
 
103
111
  class StoredConversation(StartConversationRequest):
@@ -139,7 +139,7 @@ async def bash_events_socket(
139
139
 
140
140
  async def _send_event(event: Event, websocket: WebSocket):
141
141
  try:
142
- dumped = event.model_dump()
142
+ dumped = event.model_dump(mode="json")
143
143
  await websocket.send_json(dumped)
144
144
  except Exception:
145
145
  logger.exception("error_sending_event:{event}", stack_info=True)
@@ -157,7 +157,7 @@ class _WebSocketSubscriber(Subscriber):
157
157
 
158
158
  async def _send_bash_event(event: BashEventBase, websocket: WebSocket):
159
159
  try:
160
- dumped = event.model_dump()
160
+ dumped = event.model_dump(mode="json")
161
161
  await websocket.send_json(dumped)
162
162
  except Exception:
163
163
  logger.exception("error_sending_event:{event}", stack_info=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.7.2
3
+ Version: 1.7.4
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.19
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-agent-server"
3
- version = "1.7.2"
3
+ version = "1.7.4"
4
4
  description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
5
5
 
6
6
  requires-python = ">=3.12"