openhands-agent-server 1.26.0__tar.gz → 1.27.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 (61) hide show
  1. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/api.py +12 -5
  3. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_service.py +8 -2
  4. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/models.py +45 -4
  5. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/settings_router.py +17 -7
  6. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  7. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -2
  8. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/pyproject.toml +1 -1
  9. openhands_agent_server-1.26.0/openhands/agent_server/conversation_router_acp.py +0 -185
  10. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__init__.py +0 -0
  11. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__main__.py +0 -0
  12. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  13. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/auth_router.py +0 -0
  14. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/bash_router.py +0 -0
  15. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/bash_service.py +0 -0
  16. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/cloud_proxy_router.py +0 -0
  17. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/config.py +0 -0
  18. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_lease.py +0 -0
  19. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_router.py +0 -0
  20. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/dependencies.py +0 -0
  21. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/desktop_router.py +0 -0
  22. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/desktop_service.py +0 -0
  23. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/Dockerfile +0 -0
  24. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/build.py +0 -0
  25. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
  26. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/env_parser.py +0 -0
  27. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/event_router.py +0 -0
  28. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/event_service.py +0 -0
  29. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/file_router.py +0 -0
  30. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/git_router.py +0 -0
  31. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/hooks_router.py +0 -0
  32. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/hooks_service.py +0 -0
  33. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/llm_router.py +0 -0
  34. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/logging_config.py +0 -0
  35. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/mcp_router.py +0 -0
  36. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/middleware.py +0 -0
  37. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/models.py +0 -0
  38. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/openapi.py +0 -0
  39. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/__init__.py +0 -0
  40. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/store.py +0 -0
  41. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/profiles_router.py +0 -0
  42. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/pub_sub.py +0 -0
  43. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/py.typed +0 -0
  44. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/server_details_router.py +0 -0
  45. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/skills_router.py +0 -0
  46. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/skills_service.py +0 -0
  47. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/sockets.py +0 -0
  48. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/tool_preload_service.py +0 -0
  49. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/tool_router.py +0 -0
  50. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/utils.py +0 -0
  51. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  52. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  53. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_router.py +0 -0
  54. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_service.py +0 -0
  55. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/workspace_router.py +0 -0
  56. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands/agent_server/workspaces_router.py +0 -0
  57. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  58. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  59. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  60. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  61. {openhands_agent_server-1.26.0 → openhands_agent_server-1.27.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.26.0
3
+ Version: 1.27.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
@@ -2,6 +2,7 @@ import asyncio
2
2
  import os
3
3
  import tempfile
4
4
  import traceback
5
+ import uuid
5
6
  from collections.abc import AsyncIterator, Sequence
6
7
  from contextlib import asynccontextmanager, suppress
7
8
  from pathlib import Path
@@ -24,7 +25,6 @@ from openhands.agent_server.config import (
24
25
  get_default_config,
25
26
  )
26
27
  from openhands.agent_server.conversation_router import conversation_router
27
- from openhands.agent_server.conversation_router_acp import conversation_router_acp
28
28
  from openhands.agent_server.conversation_service import (
29
29
  get_default_conversation_service,
30
30
  )
@@ -300,7 +300,6 @@ def _add_api_routes(app: FastAPI, config: Config) -> None:
300
300
  api_router = APIRouter(prefix="/api", dependencies=dependencies)
301
301
  api_router.include_router(event_router)
302
302
  api_router.include_router(conversation_router)
303
- api_router.include_router(conversation_router_acp)
304
303
  api_router.include_router(tool_router)
305
304
  api_router.include_router(bash_router)
306
305
  api_router.include_router(git_router)
@@ -431,18 +430,24 @@ def _add_exception_handlers(api: FastAPI) -> None:
431
430
  request: Request, exc: Exception
432
431
  ) -> JSONResponse:
433
432
  """Handle unhandled exceptions."""
433
+ # Correlation id that ties the 500 a caller receives to the server-side
434
+ # log line (with full traceback) for this failure, so an otherwise
435
+ # opaque 500 can be matched to its traceback in the server logs.
436
+ error_id = uuid.uuid4().hex
434
437
  # Always log that we're in the exception handler for debugging
435
438
  logger.debug(
436
- "Exception handler called for %s %s with %s: %s",
439
+ "Exception handler called for %s %s with %s: %s [error_id=%s]",
437
440
  request.method,
438
441
  request.url.path,
439
442
  type(exc).__name__,
440
443
  str(exc),
444
+ error_id,
441
445
  )
442
446
 
443
447
  content = {
444
448
  "detail": "Internal Server Error",
445
449
  "exception": str(exc),
450
+ "error_id": error_id,
446
451
  }
447
452
  # In DEBUG mode, include stack trace in response
448
453
  if DEBUG:
@@ -458,9 +463,10 @@ def _add_exception_handlers(api: FastAPI) -> None:
458
463
  return await _http_exception_handler(request, http_exc)
459
464
  # If no HTTPException found, treat as unhandled exception
460
465
  logger.error(
461
- "Unhandled ExceptionGroup on %s %s",
466
+ "Unhandled ExceptionGroup on %s %s [error_id=%s]",
462
467
  request.method,
463
468
  request.url.path,
469
+ error_id,
464
470
  exc_info=(type(exc), exc, exc.__traceback__),
465
471
  )
466
472
  return JSONResponse(status_code=500, content=content)
@@ -468,9 +474,10 @@ def _add_exception_handlers(api: FastAPI) -> None:
468
474
  # Logs full stack trace for any unhandled error that FastAPI would
469
475
  # turn into a 500
470
476
  logger.error(
471
- "Unhandled exception on %s %s",
477
+ "Unhandled exception on %s %s [error_id=%s]",
472
478
  request.method,
473
479
  request.url.path,
480
+ error_id,
474
481
  exc_info=(type(exc), exc, exc.__traceback__),
475
482
  )
476
483
  return JSONResponse(status_code=500, content=content)
@@ -1256,9 +1256,15 @@ class WebhookSubscriber(Subscriber):
1256
1256
  if self.session_api_key:
1257
1257
  headers["X-Session-API-Key"] = self.session_api_key
1258
1258
 
1259
- # Convert events to serializable format
1259
+ # Convert events to a JSON-serializable format. mode="json" is required
1260
+ # so types like set and SecretStr become JSON-safe primitives; without
1261
+ # it httpx's encoder raises "Object of type set/SecretStr is not JSON
1262
+ # serializable", every retry fails identically, and the events are
1263
+ # dropped. (Mirrors ConversationWebhookSubscriber.post_conversation_info.)
1260
1264
  event_data = [
1261
- event.model_dump() if hasattr(event, "model_dump") else event.__dict__
1265
+ event.model_dump(mode="json")
1266
+ if hasattr(event, "model_dump")
1267
+ else event.__dict__
1262
1268
  for event in events_to_post
1263
1269
  ]
1264
1270
 
@@ -34,16 +34,23 @@ from openhands.sdk.utils.pydantic_secrets import serialize_secret, validate_secr
34
34
  class SettingsUpdatePayload(TypedDict, total=False):
35
35
  """Typed payload for PersistedSettings.update() method.
36
36
 
37
- The ``*_diff`` dicts are deep-merged via :func:`_deep_merge`: nested
37
+ All three ``*_diff`` dicts are deep-merged via :func:`_deep_merge`: nested
38
38
  objects merge recursively, and a ``None`` value *inside a nested map*
39
39
  deletes that entry (the "unset" primitive) — e.g. send
40
40
  ``{"acp_env": {"NAME": None}}`` to drop one env-var without re-sending the
41
41
  whole map. A ``None`` on a top-level *field* is not treated as delete; it
42
42
  flows to validation as before.
43
+
44
+ ``misc_settings_diff`` is deep-merged into the persisted ``misc_settings``
45
+ block. The agent-server treats ``misc_settings`` as opaque
46
+ frontend-owned data (it persists and merges, but does not interpret), so
47
+ any shape the client chooses is valid; lists are replaced wholesale by
48
+ the deep-merge.
43
49
  """
44
50
 
45
51
  agent_settings_diff: dict[str, Any]
46
52
  conversation_settings_diff: dict[str, Any]
53
+ misc_settings_diff: dict[str, Any]
47
54
  active_profile: str | None
48
55
 
49
56
 
@@ -97,7 +104,7 @@ def _deep_merge(
97
104
  return result
98
105
 
99
106
 
100
- PERSISTED_SETTINGS_SCHEMA_VERSION = 1
107
+ PERSISTED_SETTINGS_SCHEMA_VERSION = 2
101
108
 
102
109
 
103
110
  class PersistedSettings(BaseModel):
@@ -109,6 +116,12 @@ class PersistedSettings(BaseModel):
109
116
 
110
117
  The ``active_profile`` field tracks which LLM profile was last activated,
111
118
  allowing frontends to display which profile is currently in use.
119
+
120
+ The ``misc_settings`` field is an opaque dict the agent-server persists
121
+ on behalf of the frontend. The agent-server never reads its contents and
122
+ has no schema for it; clients are free to store any JSON-serializable
123
+ structure they need (e.g. app/UI preferences, analytics consent, git
124
+ identity used for in-conversation commits, etc.).
112
125
  """
113
126
 
114
127
  schema_version: int = Field(
@@ -124,6 +137,14 @@ class PersistedSettings(BaseModel):
124
137
  default=None,
125
138
  description="Name of the currently active LLM profile.",
126
139
  )
140
+ misc_settings: dict[str, Any] = Field(
141
+ default_factory=dict,
142
+ description=(
143
+ "Opaque dict the agent-server persists on behalf of the frontend. "
144
+ "Updated through misc_settings_diff (deep-merged); contents are "
145
+ "never read or validated by the agent-server."
146
+ ),
147
+ )
127
148
 
128
149
  model_config = ConfigDict(populate_by_name=True)
129
150
 
@@ -173,7 +194,7 @@ class PersistedSettings(BaseModel):
173
194
  agent_update = payload.get("agent_settings_diff")
174
195
  conv_update = payload.get("conversation_settings_diff")
175
196
 
176
- # Phase 1: Validate both updates before any mutations
197
+ # Phase 1: Validate all updates before any mutations
177
198
  new_agent: AgentSettingsConfig | None = None
178
199
  new_conv: ConversationSettings | None = None
179
200
  agent_merged: dict | None = None
@@ -232,11 +253,23 @@ class PersistedSettings(BaseModel):
232
253
  f"Failed to update conversation settings: {type(e).__name__}"
233
254
  ) from None
234
255
 
256
+ # ``misc_settings`` is opaque: deep-merge without schema
257
+ # validation. The agent-server doesn't interpret what's inside,
258
+ # and ``misc_settings`` is not a secret container — the merged
259
+ # dict is therefore stored directly without the post-commit
260
+ # clear-down used by ``agent_settings`` / ``conversation_settings``.
261
+ misc_update = payload.get("misc_settings_diff")
262
+ new_misc: dict[str, Any] | None = None
263
+ if isinstance(misc_update, dict):
264
+ new_misc = _deep_merge(self.misc_settings, misc_update)
265
+
235
266
  # Phase 2: Apply validated changes atomically
236
267
  if new_agent is not None:
237
268
  self.agent_settings = new_agent
238
269
  if new_conv is not None:
239
270
  self.conversation_settings = new_conv
271
+ if new_misc is not None:
272
+ self.misc_settings = new_misc
240
273
 
241
274
  # Update active_profile if explicitly provided (including None to clear)
242
275
  if "active_profile" in payload:
@@ -252,7 +285,14 @@ class PersistedSettings(BaseModel):
252
285
  def from_persisted(
253
286
  cls, data: Any, *, context: dict[str, Any] | None = None
254
287
  ) -> PersistedSettings:
255
- """Load persisted settings, applying top-level and nested migrations."""
288
+ """Load persisted settings.
289
+
290
+ Schema-version history:
291
+
292
+ - **v1**: ``agent_settings`` + ``conversation_settings`` only.
293
+ Missing ``misc_settings`` defaults to an empty dict.
294
+ - **v2** (current): adds the opaque ``misc_settings`` container.
295
+ """
256
296
  if not isinstance(data, dict):
257
297
  return cls.model_validate(data, context=context)
258
298
 
@@ -266,6 +306,7 @@ class PersistedSettings(BaseModel):
266
306
  f"{version} is newer than supported version "
267
307
  f"{PERSISTED_SETTINGS_SCHEMA_VERSION}"
268
308
  )
309
+
269
310
  payload["schema_version"] = PERSISTED_SETTINGS_SCHEMA_VERSION
270
311
  return cls.model_validate(payload, context=context)
271
312
 
@@ -160,6 +160,7 @@ async def get_settings(request: Request) -> SettingsResponse:
160
160
  mode="json"
161
161
  ),
162
162
  llm_api_key_is_set=settings.llm_api_key_is_set,
163
+ misc_settings=settings.misc_settings,
163
164
  )
164
165
 
165
166
 
@@ -169,11 +170,12 @@ async def update_settings(
169
170
  ) -> SettingsResponse:
170
171
  """Update settings with partial changes.
171
172
 
172
- Accepts ``agent_settings_diff`` and/or ``conversation_settings_diff``
173
- for incremental updates. Diffs are deep-merged; nested objects merge
174
- recursively, and a ``null`` value **inside a nested map deletes that
175
- entry** — the "unset" primitive that lets a client remove a single map
176
- key without round-tripping the whole map. To drop one ACP env-var::
173
+ Accepts ``agent_settings_diff``, ``conversation_settings_diff``, and/or
174
+ ``misc_settings_diff`` for incremental updates. All three are deep-merged;
175
+ nested objects merge recursively, and a ``null`` value **inside a nested
176
+ map deletes that entry** — the "unset" primitive that lets a client
177
+ remove a single map key without round-tripping the whole map. To drop one
178
+ ACP env-var::
177
179
 
178
180
  PATCH /api/settings
179
181
  {"agent_settings_diff": {"acp_env": {"STALE_KEY": null}}}
@@ -187,6 +189,11 @@ async def update_settings(
187
189
  is **not** an unset — it flows to model validation as before, so it still
188
190
  fails loudly rather than silently resetting the field to its default.
189
191
 
192
+ ``misc_settings_diff`` is deep-merged into the persisted ``misc_settings``
193
+ block. The agent-server treats ``misc_settings`` as opaque frontend-owned
194
+ data: nested dicts are merged recursively, lists are replaced wholesale,
195
+ and the contents are never read or validated server-side.
196
+
190
197
  Uses file locking to prevent concurrent updates from overwriting each other.
191
198
 
192
199
  Raises:
@@ -201,8 +208,9 @@ async def update_settings(
201
208
  raise HTTPException(
202
209
  status_code=400,
203
210
  detail=(
204
- "At least one of agent_settings_diff or "
205
- "conversation_settings_diff must be provided"
211
+ "At least one of agent_settings_diff, "
212
+ "conversation_settings_diff, or misc_settings_diff "
213
+ "must be provided"
206
214
  ),
207
215
  )
208
216
 
@@ -223,6 +231,7 @@ async def update_settings(
223
231
  "conversation_settings_modified": (
224
232
  "conversation_settings_diff" in update_data
225
233
  ),
234
+ "misc_settings_modified": "misc_settings_diff" in update_data,
226
235
  },
227
236
  )
228
237
  except (ValueError, ValidationError):
@@ -256,6 +265,7 @@ async def update_settings(
256
265
  agent_settings=settings.agent_settings.model_dump(mode="json"),
257
266
  conversation_settings=settings.conversation_settings.model_dump(mode="json"),
258
267
  llm_api_key_is_set=settings.llm_api_key_is_set,
268
+ misc_settings=settings.misc_settings,
259
269
  )
260
270
 
261
271
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.26.0
3
+ Version: 1.27.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
@@ -10,7 +10,6 @@ pyproject.toml
10
10
  ./openhands/agent_server/config.py
11
11
  ./openhands/agent_server/conversation_lease.py
12
12
  ./openhands/agent_server/conversation_router.py
13
- ./openhands/agent_server/conversation_router_acp.py
14
13
  ./openhands/agent_server/conversation_service.py
15
14
  ./openhands/agent_server/dependencies.py
16
15
  ./openhands/agent_server/desktop_router.py
@@ -62,7 +61,6 @@ openhands/agent_server/cloud_proxy_router.py
62
61
  openhands/agent_server/config.py
63
62
  openhands/agent_server/conversation_lease.py
64
63
  openhands/agent_server/conversation_router.py
65
- openhands/agent_server/conversation_router_acp.py
66
64
  openhands/agent_server/conversation_service.py
67
65
  openhands/agent_server/dependencies.py
68
66
  openhands/agent_server/desktop_router.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-agent-server"
3
- version = "1.26.0"
3
+ version = "1.27.0"
4
4
  description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
5
5
 
6
6
  requires-python = ">=3.12"
@@ -1,185 +0,0 @@
1
- """ACP-capable conversation routes for the schema-sensitive endpoints."""
2
-
3
- # Deprecated REST contract: all /api/acp/conversations routes were deprecated
4
- # in v1.22.0 and are scheduled for removal in v1.27.0. The standard
5
- # FastAPI/OpenAPI deprecation marker for routes is ``deprecated=True`` on each
6
- # route decorator; keep matching docstring notices for CI deprecation checks.
7
-
8
- from typing import Annotated
9
- from uuid import UUID
10
-
11
- from fastapi import APIRouter, Body, Depends, HTTPException, Query, Response, status
12
- from pydantic import SecretStr
13
-
14
- from openhands.agent_server.conversation_service import ConversationService
15
- from openhands.agent_server.dependencies import get_conversation_service
16
- from openhands.agent_server.models import (
17
- INCLUDE_SKILLS_PARAM_TITLE,
18
- ACPConversationInfo,
19
- ACPConversationPage,
20
- ConversationSortOrder,
21
- SendMessageRequest,
22
- StartACPConversationRequest,
23
- trim_conversation_response_skills,
24
- )
25
- from openhands.sdk import LLM, Agent, TextContent
26
- from openhands.sdk.agent.acp_agent import ACPAgent
27
- from openhands.sdk.conversation.state import ConversationExecutionStatus
28
- from openhands.sdk.workspace import LocalWorkspace
29
- from openhands.tools.preset.default import get_default_tools
30
-
31
-
32
- conversation_router_acp = APIRouter(
33
- prefix="/acp/conversations",
34
- tags=["ACP Conversations"],
35
- )
36
-
37
- START_ACP_CONVERSATION_EXAMPLES = [
38
- StartACPConversationRequest(
39
- agent=Agent(
40
- llm=LLM(
41
- usage_id="your-llm-service",
42
- model="your-model-provider/your-model-name",
43
- api_key=SecretStr("your-api-key-here"),
44
- ),
45
- tools=get_default_tools(enable_browser=True),
46
- ),
47
- workspace=LocalWorkspace(working_dir="workspace/project"),
48
- initial_message=SendMessageRequest(
49
- role="user", content=[TextContent(text="Flip a coin!")]
50
- ),
51
- ).model_dump(exclude_defaults=True, mode="json"),
52
- StartACPConversationRequest(
53
- agent=ACPAgent(acp_command=["npx", "-y", "claude-agent-acp"]),
54
- workspace=LocalWorkspace(working_dir="workspace/project"),
55
- initial_message=SendMessageRequest(
56
- role="user",
57
- content=[TextContent(text="Inspect the repository and summarize it.")],
58
- ),
59
- ).model_dump(exclude_defaults=True, mode="json"),
60
- ]
61
-
62
-
63
- @conversation_router_acp.get("/search", deprecated=True)
64
- async def search_acp_conversations(
65
- page_id: Annotated[
66
- str | None,
67
- Query(title="Optional next_page_id from the previously returned page"),
68
- ] = None,
69
- limit: Annotated[
70
- int,
71
- Query(title="The max number of results in the page", gt=0, lte=100),
72
- ] = 100,
73
- status: Annotated[
74
- ConversationExecutionStatus | None,
75
- Query(title="Optional filter by conversation execution status"),
76
- ] = None,
77
- sort_order: Annotated[
78
- ConversationSortOrder,
79
- Query(title="Sort order for conversations"),
80
- ] = ConversationSortOrder.CREATED_AT_DESC,
81
- include_skills: Annotated[bool, Query(title=INCLUDE_SKILLS_PARAM_TITLE)] = False,
82
- conversation_service: ConversationService = Depends(get_conversation_service),
83
- ) -> ACPConversationPage:
84
- """Search conversations using the ACP-capable contract.
85
-
86
- Deprecated since v1.22.0 and scheduled for removal in v1.27.0.
87
- Use ``/api/conversations/search`` instead.
88
- """
89
- assert limit > 0
90
- assert limit <= 100
91
- page = await conversation_service.search_acp_conversations(
92
- page_id, limit, status, sort_order
93
- )
94
- if not include_skills:
95
- page = page.model_copy(
96
- update={
97
- "items": [
98
- trim_conversation_response_skills(item) for item in page.items
99
- ]
100
- }
101
- )
102
- return page
103
-
104
-
105
- @conversation_router_acp.get("/count", deprecated=True)
106
- async def count_acp_conversations(
107
- status: Annotated[
108
- ConversationExecutionStatus | None,
109
- Query(title="Optional filter by conversation execution status"),
110
- ] = None,
111
- conversation_service: ConversationService = Depends(get_conversation_service),
112
- ) -> int:
113
- """Count conversations using the ACP-capable contract.
114
-
115
- Deprecated since v1.22.0 and scheduled for removal in v1.27.0.
116
- Use ``/api/conversations/count`` instead.
117
- """
118
- return await conversation_service.count_conversations(status)
119
-
120
-
121
- @conversation_router_acp.get(
122
- "/{conversation_id}",
123
- responses={404: {"description": "Item not found"}},
124
- deprecated=True,
125
- )
126
- async def get_acp_conversation(
127
- conversation_id: UUID,
128
- include_skills: Annotated[bool, Query(title=INCLUDE_SKILLS_PARAM_TITLE)] = False,
129
- conversation_service: ConversationService = Depends(get_conversation_service),
130
- ) -> ACPConversationInfo:
131
- """Get a conversation using the ACP-capable contract.
132
-
133
- Deprecated since v1.22.0 and scheduled for removal in v1.27.0.
134
- Use ``/api/conversations/{conversation_id}`` instead.
135
- """
136
- conversation = await conversation_service.get_acp_conversation(conversation_id)
137
- if conversation is None:
138
- raise HTTPException(status.HTTP_404_NOT_FOUND)
139
- if not include_skills:
140
- conversation = trim_conversation_response_skills(conversation)
141
- return conversation
142
-
143
-
144
- @conversation_router_acp.get("", deprecated=True)
145
- async def batch_get_acp_conversations(
146
- ids: Annotated[list[UUID], Query()],
147
- include_skills: Annotated[bool, Query(title=INCLUDE_SKILLS_PARAM_TITLE)] = False,
148
- conversation_service: ConversationService = Depends(get_conversation_service),
149
- ) -> list[ACPConversationInfo | None]:
150
- """Batch get conversations using the ACP-capable contract.
151
-
152
- Deprecated since v1.22.0 and scheduled for removal in v1.27.0.
153
- Use ``/api/conversations`` instead.
154
- """
155
- assert len(ids) < 100
156
- conversations = await conversation_service.batch_get_acp_conversations(ids)
157
- if not include_skills:
158
- return [
159
- trim_conversation_response_skills(c) if c is not None else None
160
- for c in conversations
161
- ]
162
- return conversations
163
-
164
-
165
- @conversation_router_acp.post("", deprecated=True)
166
- async def start_acp_conversation(
167
- request: Annotated[
168
- StartACPConversationRequest,
169
- Body(examples=START_ACP_CONVERSATION_EXAMPLES),
170
- ],
171
- response: Response,
172
- include_skills: Annotated[bool, Query(title=INCLUDE_SKILLS_PARAM_TITLE)] = False,
173
- conversation_service: ConversationService = Depends(get_conversation_service),
174
- ) -> ACPConversationInfo:
175
- """Start a conversation using the ACP-capable contract.
176
-
177
- Deprecated since v1.22.0 and scheduled for removal in v1.27.0.
178
- Use ``/api/conversations`` instead; it now accepts ACP agents and
179
- ``agent_settings`` payloads.
180
- """
181
- info, is_new = await conversation_service.start_acp_conversation(request)
182
- response.status_code = status.HTTP_201_CREATED if is_new else status.HTTP_200_OK
183
- if not include_skills:
184
- info = trim_conversation_response_skills(info)
185
- return info