openhands-agent-server 1.47.0__tar.gz → 1.49.0__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 (95) hide show
  1. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/PKG-INFO +2 -2
  2. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/api.py +28 -15
  3. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/bash_router.py +14 -3
  4. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/bash_service.py +18 -2
  5. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/config.py +7 -0
  6. openhands_agent_server-1.49.0/openhands/agent_server/conversation_registry.py +74 -0
  7. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_router.py +29 -3
  8. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_service.py +122 -19
  9. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/dependencies.py +9 -1
  10. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/docker/Dockerfile +114 -33
  11. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/docker/build.py +8 -10
  12. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/__init__.py +1 -0
  13. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/mediation.py +112 -0
  14. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/provisioning.py +163 -0
  15. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/proxy.py +253 -0
  16. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/registry.py +319 -0
  17. openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/routers.py +345 -0
  18. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/event_router.py +1 -3
  19. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/event_service.py +11 -0
  20. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/file_router.py +27 -9
  21. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/init_router.py +14 -1
  22. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/mcp_router.py +14 -42
  23. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/models.py +25 -0
  24. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/models.py +7 -2
  25. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/profiles_router.py +1 -1
  26. openhands_agent_server-1.49.0/openhands/agent_server/runtime_router.py +105 -0
  27. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/server_details_router.py +15 -4
  28. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/settings_router.py +16 -2
  29. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/sockets.py +4 -3
  30. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/PKG-INFO +2 -2
  31. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/SOURCES.txt +16 -2
  32. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/requires.txt +1 -1
  33. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/pyproject.toml +3 -3
  34. openhands_agent_server-1.47.0/openhands/agent_server/desktop_router.py +0 -28
  35. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/__init__.py +0 -0
  36. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/__main__.py +0 -0
  37. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/_secret_redaction.py +0 -0
  38. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  39. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/agent_profiles_router.py +0 -0
  40. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/auth_router.py +0 -0
  41. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
  42. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/installed.py +0 -0
  43. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
  44. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions_router.py +0 -0
  45. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_lease.py +0 -0
  46. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/credential_binding.py +0 -0
  47. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/env_parser.py +0 -0
  48. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/git_router.py +0 -0
  49. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/hooks_router.py +0 -0
  50. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/hooks_service.py +0 -0
  51. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/llm_router.py +0 -0
  52. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/logging_config.py +0 -0
  53. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  54. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/middleware.py +0 -0
  55. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/__init__.py +0 -0
  56. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/models.py +0 -0
  57. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/router.py +0 -0
  58. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/service.py +0 -0
  59. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openapi.py +0 -0
  60. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/__init__.py +0 -0
  61. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/store.py +0 -0
  62. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/plugins_router.py +0 -0
  63. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/plugins_service.py +0 -0
  64. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/provider_connections_router.py +0 -0
  65. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/pub_sub.py +0 -0
  66. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/py.typed +0 -0
  67. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/session_protocol.py +0 -0
  68. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/session_socket.py +0 -0
  69. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/skills_router.py +0 -0
  70. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/skills_service.py +0 -0
  71. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/sub_agents_router.py +0 -0
  72. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/__init__.py +0 -0
  73. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/factory.py +0 -0
  74. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
  75. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/models.py +0 -0
  76. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/policy.py +0 -0
  77. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
  78. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
  79. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/service.py +0 -0
  80. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/sink.py +0 -0
  81. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
  82. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry_types.py +0 -0
  83. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/tool_preload_service.py +0 -0
  84. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/tool_router.py +0 -0
  85. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/utils.py +0 -0
  86. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  87. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  88. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_router.py +0 -0
  89. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_service.py +0 -0
  90. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/workspace_router.py +0 -0
  91. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands/agent_server/workspaces_router.py +0 -0
  92. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  93. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  94. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  95. {openhands_agent_server-1.47.0 → openhands_agent_server-1.49.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.47.0
3
+ Version: 1.49.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -15,7 +15,7 @@ Requires-Dist: openhands-sdk
15
15
  Requires-Dist: openai<3,>=2.33.0
16
16
  Requires-Dist: pydantic>=2
17
17
  Requires-Dist: sqlalchemy>=2
18
- Requires-Dist: uvicorn>=0.31.1
18
+ Requires-Dist: uvicorn>=0.52.4
19
19
  Requires-Dist: websockets>=12
20
20
  Requires-Dist: wsproto>=1.2.0
21
21
  Provides-Extra: posthog
@@ -26,7 +26,13 @@ from openhands.agent_server.config import (
26
26
  Config,
27
27
  get_default_config,
28
28
  )
29
- from openhands.agent_server.conversation_router import conversation_router
29
+ from openhands.agent_server.conversation_registry import (
30
+ create_conversation_registry,
31
+ )
32
+ from openhands.agent_server.conversation_router import (
33
+ conversation_catalog_router,
34
+ conversation_router,
35
+ )
30
36
  from openhands.agent_server.conversation_service import (
31
37
  CredentialBindingActivationRequired,
32
38
  get_default_conversation_service,
@@ -38,9 +44,7 @@ from openhands.agent_server.dependencies import (
38
44
  check_session_api_key,
39
45
  check_workspace_session,
40
46
  )
41
- from openhands.agent_server.desktop_router import desktop_router
42
- from openhands.agent_server.event_router import event_router
43
- from openhands.agent_server.file_router import file_router
47
+ from openhands.agent_server.file_router import file_discovery_router, file_router
44
48
  from openhands.agent_server.git_router import git_router
45
49
  from openhands.agent_server.hooks_router import hooks_router
46
50
  from openhands.agent_server.init_router import (
@@ -65,10 +69,8 @@ from openhands.agent_server.server_details_router import (
65
69
  mark_initialization_complete,
66
70
  server_details_router,
67
71
  )
68
- from openhands.agent_server.session_socket import session_router
69
72
  from openhands.agent_server.settings_router import settings_router
70
73
  from openhands.agent_server.skills_router import skills_router
71
- from openhands.agent_server.sockets import sockets_router
72
74
  from openhands.agent_server.sub_agents_router import sub_agents_router
73
75
  from openhands.agent_server.telemetry import (
74
76
  build_telemetry_sink,
@@ -91,7 +93,6 @@ from openhands.agent_server.tool_preload_service import get_tool_preload_service
91
93
  from openhands.agent_server.tool_router import tool_router
92
94
  from openhands.agent_server.vscode_router import vscode_router
93
95
  from openhands.agent_server.vscode_service import get_vscode_service
94
- from openhands.agent_server.workspace_router import workspace_router
95
96
  from openhands.agent_server.workspaces_router import workspaces_router
96
97
  from openhands.sdk.logger import DEBUG, get_logger
97
98
  from openhands.sdk.utils.redact import sanitize_dict
@@ -159,6 +160,10 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
159
160
 
160
161
  config: Config = api.state.config
161
162
  deferred = config.deferred_init
163
+ conversation_registry = getattr(
164
+ api.state, "conversation_registry", None
165
+ ) or create_conversation_registry(config)
166
+ api.state.conversation_registry = conversation_registry
162
167
 
163
168
  # Deferred pods boot with telemetry disabled and are rebuilt by
164
169
  # InitService, so they emit `server_started` there instead.
@@ -253,6 +258,11 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
253
258
  bash_svc = get_default_bash_event_service()
254
259
  api.state.bash_event_service = bash_svc
255
260
 
261
+ conversation_registry.configure_service(service)
262
+ # Runtime cleanup must precede external-catalog recovery so stale
263
+ # runtime owners cannot lose their expired leases to the outer service.
264
+ await conversation_registry.start()
265
+
256
266
  async with service:
257
267
  api.state.conversation_service = service
258
268
 
@@ -272,6 +282,7 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
272
282
  try:
273
283
  yield
274
284
  finally:
285
+ await conversation_registry.shutdown()
275
286
  if retention_task is not None:
276
287
  retention_task.cancel()
277
288
  with suppress(asyncio.CancelledError):
@@ -387,6 +398,7 @@ def _find_http_exception(exc: BaseExceptionGroup) -> HTTPException | None:
387
398
 
388
399
  def _add_api_routes(app: FastAPI) -> None:
389
400
  """Add all API routes to the FastAPI application."""
401
+ conversation_registry = app.state.conversation_registry
390
402
  app.include_router(server_details_router)
391
403
 
392
404
  # The /api/init endpoint bypasses both the session-key auth and the
@@ -411,7 +423,11 @@ def _add_api_routes(app: FastAPI) -> None:
411
423
  ]
412
424
 
413
425
  api_router = APIRouter(prefix="/api", dependencies=dependencies)
414
- api_router.include_router(event_router)
426
+ api_router.include_router(file_discovery_router)
427
+ # Collection routes must precede runtime catch-alls such as
428
+ # ``/conversations/{conversation_id}``.
429
+ api_router.include_router(conversation_catalog_router)
430
+ conversation_registry.add_execution_routes(api_router)
415
431
  api_router.include_router(conversation_router)
416
432
  api_router.include_router(credential_binding_router)
417
433
  api_router.include_router(tool_router)
@@ -419,7 +435,6 @@ def _add_api_routes(app: FastAPI) -> None:
419
435
  api_router.include_router(git_router)
420
436
  api_router.include_router(file_router)
421
437
  api_router.include_router(vscode_router)
422
- api_router.include_router(desktop_router)
423
438
  api_router.include_router(skills_router)
424
439
  api_router.include_router(sub_agents_router)
425
440
  api_router.include_router(plugins_router)
@@ -435,8 +450,6 @@ def _add_api_routes(app: FastAPI) -> None:
435
450
  # /api/auth/* mints workspace cookies and requires the header to bootstrap,
436
451
  # so it lives under the header-only auth group.
437
452
  api_router.include_router(auth_router)
438
- app.include_router(api_router)
439
-
440
453
  app.include_router(openai_router, dependencies=[Depends(check_openai_api_key)])
441
454
 
442
455
  # Workspace static-file routes get their own auth group that accepts
@@ -447,12 +460,11 @@ def _add_api_routes(app: FastAPI) -> None:
447
460
  workspace_api_router = APIRouter(
448
461
  prefix="/api", dependencies=[Depends(check_workspace_session)]
449
462
  )
450
- workspace_api_router.include_router(workspace_router)
463
+ workspace_api_router.include_router(conversation_registry.workspace_router)
451
464
  app.include_router(workspace_api_router)
465
+ app.include_router(api_router)
452
466
 
453
- app.include_router(sockets_router)
454
-
455
- app.include_router(session_router)
467
+ app.include_router(conversation_registry.sockets_router)
456
468
 
457
469
 
458
470
  def _setup_static_files(app: FastAPI, config: Config) -> None:
@@ -680,6 +692,7 @@ def create_app(config: Config | None = None) -> FastAPI:
680
692
  config = get_default_config()
681
693
  app = _create_fastapi_instance(config)
682
694
  app.state.config = config
695
+ app.state.conversation_registry = create_conversation_registry(config)
683
696
 
684
697
  _add_api_routes(app)
685
698
  _setup_static_files(app, config)
@@ -2,6 +2,7 @@
2
2
 
3
3
  import logging
4
4
  from datetime import datetime
5
+ from pathlib import Path
5
6
  from typing import Annotated, Literal, cast
6
7
  from uuid import UUID
7
8
 
@@ -52,13 +53,11 @@ async def search_bash_events(
52
53
  ] = None,
53
54
  limit: Annotated[
54
55
  int,
55
- Query(title="The max number of results in the page", gt=0, lte=100),
56
+ Query(title="The max number of results in the page", gt=0, le=100),
56
57
  ] = 100,
57
58
  bash_event_service: BashEventService = Depends(get_bash_event_service),
58
59
  ) -> BashEventPage:
59
60
  """Search / List bash event events"""
60
- assert limit > 0
61
- assert limit <= 100
62
61
 
63
62
  return await bash_event_service.search_bash_events(
64
63
  kind__eq=kind__eq,
@@ -104,6 +103,7 @@ async def start_bash_command(
104
103
  ) -> BashCommand:
105
104
  """Execute a bash command in the background"""
106
105
  update_last_execution_time()
106
+ _validate_cwd(request, bash_event_service)
107
107
  command, _ = await bash_event_service.start_bash_command(request)
108
108
  return command
109
109
 
@@ -115,6 +115,7 @@ async def execute_bash_command(
115
115
  ) -> BashOutput:
116
116
  """Execute a bash command and wait for a result"""
117
117
  update_last_execution_time()
118
+ _validate_cwd(request, bash_event_service)
118
119
  command, task = await bash_event_service.start_bash_command(request)
119
120
  await task
120
121
  page = await bash_event_service.search_bash_events(command_id__eq=command.id)
@@ -129,3 +130,13 @@ async def clear_all_bash_events(
129
130
  """Clear all bash events from storage"""
130
131
  count = await bash_event_service.clear_all_events()
131
132
  return {"cleared_count": count}
133
+
134
+
135
+ def _validate_cwd(request: ExecuteBashRequest, service: BashEventService) -> None:
136
+ if service.default_cwd is not None and request.cwd is not None:
137
+ if (
138
+ not Path(request.cwd)
139
+ .resolve()
140
+ .is_relative_to(Path(service.default_cwd).resolve())
141
+ ):
142
+ raise HTTPException(422, "cwd must be inside the conversation workspace")
@@ -31,6 +31,9 @@ class BashEventService:
31
31
  will not be visible to the agent."""
32
32
 
33
33
  bash_events_dir: Path = field()
34
+ default_cwd: str | None = None
35
+ _tasks: set[asyncio.Task] = field(default_factory=set, init=False)
36
+ _closed: bool = field(default=False, init=False)
34
37
  _pub_sub: PubSub[BashEventBase] = field(
35
38
  default_factory=lambda: PubSub[BashEventBase](max_subscribers=50),
36
39
  init=False,
@@ -255,13 +258,17 @@ class BashEventService:
255
258
  self, request: ExecuteBashRequest
256
259
  ) -> tuple[BashCommand, asyncio.Task]:
257
260
  """Execute a bash command. The output will be published separately."""
258
- command = BashCommand(**request.model_dump())
261
+ if self._closed:
262
+ raise RuntimeError("Bash event service is closed")
263
+ cwd = request.cwd or self.default_cwd
264
+ command = BashCommand(**{**request.model_dump(), "cwd": cwd})
259
265
  self._save_event_to_file(command)
260
266
  await self._pub_sub(command)
261
267
 
262
268
  # Execute the bash command in a background task
263
269
  task = asyncio.create_task(self._execute_bash_command(command))
264
-
270
+ self._tasks.add(task)
271
+ task.add_done_callback(self._tasks.discard)
265
272
  return command, task
266
273
 
267
274
  async def _execute_bash_command(self, command: BashCommand) -> None:
@@ -347,6 +354,10 @@ class BashEventService:
347
354
  timeout=command.timeout,
348
355
  )
349
356
  exit_code = process.returncode
357
+ except asyncio.CancelledError:
358
+ self._signal_process_group(process, signal.SIGKILL)
359
+ await process.wait()
360
+ raise
350
361
  except TimeoutError:
351
362
  # Send SIGTERM to the whole process group so user-installed
352
363
  # cleanup traps can run, then escalate to SIGKILL if needed.
@@ -509,6 +520,11 @@ class BashEventService:
509
520
 
510
521
  async def close(self):
511
522
  """Close the bash event service and clean up resources."""
523
+ self._closed = True
524
+ tasks = list(self._tasks)
525
+ for task in tasks:
526
+ task.cancel()
527
+ await asyncio.gather(*tasks, return_exceptions=True)
512
528
  await self._pub_sub.close()
513
529
 
514
530
  async def __aenter__(self):
@@ -346,6 +346,13 @@ class Config(BaseModel):
346
346
  "The URL where this agent server instance is available externally"
347
347
  ),
348
348
  )
349
+ conversation_runtime: Literal["local", "docker"] = "local"
350
+ conversation_image: str = "ghcr.io/openhands/agent-server:latest-python"
351
+ conversation_container_memory: str | None = "4g"
352
+ conversation_container_cpus: float | None = Field(default=2.0, gt=0)
353
+ conversation_container_pids_limit: int | None = Field(default=512, gt=0)
354
+ conversation_container_startup_timeout: float = Field(default=120, gt=0)
355
+
349
356
  acp_skill_sourcing: ACPSkillSourcing = Field(
350
357
  default="native",
351
358
  description=(
@@ -0,0 +1,74 @@
1
+ """Select and manage the configured conversation runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from fastapi import APIRouter
8
+
9
+ from openhands.agent_server.config import Config
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from openhands.agent_server.conversation_service import ConversationService
14
+
15
+
16
+ class ConversationRegistry:
17
+ """Route and lifecycle adapter for host-local conversations."""
18
+
19
+ def __init__(self, config: Config) -> None:
20
+ self.config = config
21
+
22
+ def configure_service(self, service: ConversationService) -> None:
23
+ """Connect runtime-specific persistence to the shared catalog."""
24
+
25
+ async def start(self) -> None:
26
+ """Start resources owned by this registry."""
27
+
28
+ async def shutdown(self) -> None:
29
+ """Stop resources owned by this registry."""
30
+
31
+ def add_execution_routes(self, router: APIRouter) -> None:
32
+ from openhands.agent_server.event_router import event_router
33
+ from openhands.agent_server.runtime_router import create_runtime_router
34
+
35
+ router.include_router(create_runtime_router())
36
+ router.include_router(event_router)
37
+
38
+ @property
39
+ def workspace_router(self) -> APIRouter:
40
+ from openhands.agent_server.workspace_router import workspace_router
41
+
42
+ return workspace_router
43
+
44
+ @property
45
+ def conversation_sockets_router(self) -> APIRouter:
46
+ from openhands.agent_server.sockets import conversation_sockets_router
47
+
48
+ return conversation_sockets_router
49
+
50
+ @property
51
+ def session_sockets_router(self) -> APIRouter:
52
+ from openhands.agent_server.session_socket import session_router
53
+
54
+ return session_router
55
+
56
+ @property
57
+ def sockets_router(self) -> APIRouter:
58
+ from openhands.agent_server.sockets import bash_sockets_router
59
+
60
+ router = APIRouter()
61
+ router.include_router(self.conversation_sockets_router)
62
+ router.include_router(self.session_sockets_router)
63
+ router.include_router(bash_sockets_router)
64
+ return router
65
+
66
+
67
+ def create_conversation_registry(config: Config) -> ConversationRegistry:
68
+ if config.conversation_runtime == "docker":
69
+ from openhands.agent_server.docker_runtime.registry import (
70
+ DockerConversationRegistry,
71
+ )
72
+
73
+ return DockerConversationRegistry(config)
74
+ return ConversationRegistry(config)
@@ -31,6 +31,8 @@ from openhands.agent_server.models import (
31
31
  AskAgentResponse,
32
32
  ConversationInfo,
33
33
  ConversationPage,
34
+ ConversationRuntimeInfo,
35
+ ConversationRuntimeStatus,
34
36
  ConversationSortOrder,
35
37
  ForkConversationRequest,
36
38
  NavigateConversationRequest,
@@ -61,6 +63,7 @@ from openhands.sdk.workspace import LocalWorkspace
61
63
  from openhands.tools.preset.default import get_default_tools
62
64
 
63
65
 
66
+ conversation_catalog_router = APIRouter(prefix="/conversations", tags=["Conversations"])
64
67
  conversation_router = APIRouter(prefix="/conversations", tags=["Conversations"])
65
68
 
66
69
  # Examples
@@ -86,6 +89,7 @@ START_CONVERSATION_EXAMPLES = [
86
89
  # Read methods
87
90
 
88
91
 
92
+ @conversation_catalog_router.get("/search", include_in_schema=False)
89
93
  @conversation_router.get("/search")
90
94
  async def search_conversations(
91
95
  page_id: Annotated[
@@ -94,7 +98,7 @@ async def search_conversations(
94
98
  ] = None,
95
99
  limit: Annotated[
96
100
  int,
97
- Query(title="The max number of results in the page", gt=0, lte=100),
101
+ Query(title="The max number of results in the page", gt=0, le=100),
98
102
  ] = 100,
99
103
  status: Annotated[
100
104
  ConversationExecutionStatus | None,
@@ -108,8 +112,6 @@ async def search_conversations(
108
112
  conversation_service: ConversationService = Depends(get_conversation_service),
109
113
  ) -> ConversationPage:
110
114
  """Search / List conversations"""
111
- assert limit > 0
112
- assert limit <= 100
113
115
  page = await conversation_service.search_conversations(
114
116
  page_id, limit, status, sort_order
115
117
  )
@@ -128,6 +130,7 @@ async def search_conversations(
128
130
  return page
129
131
 
130
132
 
133
+ @conversation_catalog_router.get("/count", include_in_schema=False)
131
134
  @conversation_router.get("/count")
132
135
  async def count_conversations(
133
136
  status: Annotated[
@@ -158,6 +161,29 @@ async def get_conversation(
158
161
  return conversation
159
162
 
160
163
 
164
+ @conversation_router.get("/{conversation_id}/runtime")
165
+ async def get_local_conversation_runtime(
166
+ conversation_id: UUID,
167
+ conversation_service: ConversationService = Depends(get_conversation_service),
168
+ ) -> ConversationRuntimeInfo:
169
+ """Inspect the always-available in-process runtime."""
170
+ if await conversation_service.get_conversation(conversation_id) is None:
171
+ raise HTTPException(status.HTTP_404_NOT_FOUND)
172
+ return ConversationRuntimeInfo(
173
+ runtime_status=ConversationRuntimeStatus.AVAILABLE,
174
+ can_resume=True,
175
+ )
176
+
177
+
178
+ @conversation_router.post("/{conversation_id}/runtime/reprovision")
179
+ async def reprovision_local_conversation_runtime(
180
+ conversation_id: UUID,
181
+ conversation_service: ConversationService = Depends(get_conversation_service),
182
+ ) -> ConversationRuntimeInfo:
183
+ """Return local runtime state; local mode has no infrastructure to provision."""
184
+ return await get_local_conversation_runtime(conversation_id, conversation_service)
185
+
186
+
161
187
  @conversation_router.get(
162
188
  "/{conversation_id}/agent_final_response",
163
189
  responses={404: {"description": "Conversation not found"}},