openhands-agent-server 1.25.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.
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/PKG-INFO +1 -1
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/api.py +12 -5
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_service.py +8 -2
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/mcp_router.py +142 -12
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/models.py +45 -4
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/settings_router.py +17 -7
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/skills_service.py +3 -3
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -2
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/pyproject.toml +1 -1
- openhands_agent_server-1.25.0/openhands/agent_server/conversation_router_acp.py +0 -185
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/cloud_proxy_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/event_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/file_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.25.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.
|
|
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
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/api.py
RENAMED
|
@@ -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(
|
|
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
|
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/mcp_router.py
RENAMED
|
@@ -6,9 +6,12 @@ to settings, where a misconfiguration would otherwise surface only at
|
|
|
6
6
|
conversation start (and there manifest as a noisy traceback that aborts
|
|
7
7
|
agent initialization).
|
|
8
8
|
|
|
9
|
-
The endpoint
|
|
10
|
-
connection, lists the advertised tools,
|
|
11
|
-
|
|
9
|
+
The endpoint never mutates server state or touches stored settings: it
|
|
10
|
+
spins up the MCP connection, lists the advertised tools, optionally invokes
|
|
11
|
+
one caller-chosen tool (``tool_call``), then tears the connection down.
|
|
12
|
+
The optional tool call exists because listing tools does not exercise the
|
|
13
|
+
credentials many servers only use inside tool handlers (e.g. the Slack MCP
|
|
14
|
+
server starts fine with a bogus token); callers must pick a read-only tool.
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
from __future__ import annotations
|
|
@@ -16,12 +19,16 @@ from __future__ import annotations
|
|
|
16
19
|
import asyncio
|
|
17
20
|
from typing import Annotated, Any, Literal
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
import mcp.types
|
|
23
|
+
from fastapi import APIRouter, Request
|
|
20
24
|
from pydantic import BaseModel, Field, model_validator
|
|
21
25
|
|
|
26
|
+
from openhands.agent_server._secrets_exposure import get_cipher
|
|
22
27
|
from openhands.sdk.logger import get_logger
|
|
23
28
|
from openhands.sdk.mcp import create_mcp_tools
|
|
24
29
|
from openhands.sdk.mcp.exceptions import MCPError, MCPTimeoutError
|
|
30
|
+
from openhands.sdk.utils.cipher import Cipher
|
|
31
|
+
from openhands.sdk.utils.pydantic_secrets import decrypt_str_with_cipher_or_keep
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
logger = get_logger(__name__)
|
|
@@ -85,6 +92,22 @@ class _RemoteMCPServerSpec(BaseModel):
|
|
|
85
92
|
return out
|
|
86
93
|
|
|
87
94
|
|
|
95
|
+
class MCPToolCallSpec(BaseModel):
|
|
96
|
+
"""A single tool invocation to run as part of the connection test.
|
|
97
|
+
|
|
98
|
+
Listing tools does not exercise the credentials many servers only use
|
|
99
|
+
inside tool handlers, so callers can name one tool to invoke after the
|
|
100
|
+
listing succeeds. Callers are responsible for choosing a read-only tool;
|
|
101
|
+
the endpoint executes it verbatim.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
name: str = Field(..., min_length=1, description="Name of the tool to invoke")
|
|
105
|
+
arguments: dict[str, Any] = Field(
|
|
106
|
+
default_factory=dict,
|
|
107
|
+
description="Arguments passed to the tool unchanged.",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
88
111
|
class MCPTestRequest(BaseModel):
|
|
89
112
|
"""Body for ``POST /api/mcp/test``."""
|
|
90
113
|
|
|
@@ -108,6 +131,15 @@ class MCPTestRequest(BaseModel):
|
|
|
108
131
|
le=120,
|
|
109
132
|
description="Seconds to wait for connection + tools/list to complete.",
|
|
110
133
|
)
|
|
134
|
+
tool_call: MCPToolCallSpec | None = Field(
|
|
135
|
+
default=None,
|
|
136
|
+
description=(
|
|
137
|
+
"Optional read-only tool to invoke after listing succeeds, so "
|
|
138
|
+
"callers can verify credentials the server only exercises on "
|
|
139
|
+
"tool invocation. Its outcome is reported verbatim in "
|
|
140
|
+
"`tool_result` without affecting `ok`."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
111
143
|
|
|
112
144
|
@model_validator(mode="after")
|
|
113
145
|
def _strip_name(self) -> MCPTestRequest:
|
|
@@ -117,6 +149,19 @@ class MCPTestRequest(BaseModel):
|
|
|
117
149
|
return self
|
|
118
150
|
|
|
119
151
|
|
|
152
|
+
class MCPToolCallResult(BaseModel):
|
|
153
|
+
"""Verbatim outcome of the requested ``tool_call``.
|
|
154
|
+
|
|
155
|
+
The endpoint stays provider-neutral: many servers report upstream
|
|
156
|
+
failures (e.g. Slack's ``{"ok": false, "error": "invalid_auth"}``)
|
|
157
|
+
as ordinary text content with ``isError`` unset, so interpreting the
|
|
158
|
+
payload is the caller's job.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
is_error: bool = Field(description="The MCP-level isError flag of the result.")
|
|
162
|
+
text: str = Field(description="Concatenated text content of the result.")
|
|
163
|
+
|
|
164
|
+
|
|
120
165
|
class MCPTestSuccess(BaseModel):
|
|
121
166
|
"""Response when the candidate server connects and lists its tools."""
|
|
122
167
|
|
|
@@ -125,6 +170,10 @@ class MCPTestSuccess(BaseModel):
|
|
|
125
170
|
default_factory=list,
|
|
126
171
|
description="Names of tools advertised by the MCP server.",
|
|
127
172
|
)
|
|
173
|
+
tool_result: MCPToolCallResult | None = Field(
|
|
174
|
+
default=None,
|
|
175
|
+
description=("Outcome of the requested `tool_call`, when one was supplied."),
|
|
176
|
+
)
|
|
128
177
|
|
|
129
178
|
|
|
130
179
|
class MCPTestFailure(BaseModel):
|
|
@@ -151,18 +200,81 @@ MCPTestResponse = MCPTestSuccess | MCPTestFailure
|
|
|
151
200
|
# ---------------------------------------------------------------------------
|
|
152
201
|
|
|
153
202
|
|
|
154
|
-
def
|
|
203
|
+
def _decrypt_mapping(cipher: Cipher | None, mapping: dict[str, str]) -> dict[str, str]:
|
|
204
|
+
"""Decrypt Fernet-encrypted values round-tripped from settings.
|
|
205
|
+
|
|
206
|
+
The GUI fetches stored settings with ``X-Expose-Secrets: encrypted`` and
|
|
207
|
+
forwards the ciphertext unchanged so the edit flow can test the *real*
|
|
208
|
+
stored credentials without ever seeing them. Plaintext values (the
|
|
209
|
+
common case: freshly typed input) pass through untouched.
|
|
210
|
+
"""
|
|
211
|
+
if cipher is None:
|
|
212
|
+
return dict(mapping)
|
|
213
|
+
return {
|
|
214
|
+
key: decrypt_str_with_cipher_or_keep(
|
|
215
|
+
cipher, value, description="MCP test env/headers"
|
|
216
|
+
)
|
|
217
|
+
for key, value in mapping.items()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _server_to_fastmcp_dict(
|
|
222
|
+
spec: _StdioMCPServerSpec | _RemoteMCPServerSpec, cipher: Cipher | None
|
|
223
|
+
) -> dict:
|
|
155
224
|
if isinstance(spec, _StdioMCPServerSpec):
|
|
156
225
|
out: dict[str, Any] = {"command": spec.command, "args": list(spec.args)}
|
|
157
226
|
if spec.env:
|
|
158
|
-
out["env"] =
|
|
227
|
+
out["env"] = _decrypt_mapping(cipher, spec.env)
|
|
159
228
|
if spec.cwd:
|
|
160
229
|
out["cwd"] = spec.cwd
|
|
161
230
|
return out
|
|
162
|
-
|
|
231
|
+
remote = spec.to_fastmcp_dict()
|
|
232
|
+
if "headers" in remote:
|
|
233
|
+
remote["headers"] = _decrypt_mapping(cipher, remote["headers"])
|
|
234
|
+
return remote
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _run_tool_call(
|
|
238
|
+
client: Any, spec: MCPToolCallSpec, tool_names: list[str], timeout: float
|
|
239
|
+
) -> MCPToolCallResult:
|
|
240
|
+
"""Invoke the requested tool on the connected client.
|
|
241
|
+
|
|
242
|
+
Uses ``call_tool_mcp`` (not ``call_tool``, which raises on ``isError``)
|
|
243
|
+
so in-band failures come back as data -- mirrors ``MCPToolExecutor``.
|
|
244
|
+
A timeout is reported as an errored result rather than failing the
|
|
245
|
+
whole test: the server did connect and list, which is still useful.
|
|
246
|
+
"""
|
|
247
|
+
if spec.name not in tool_names:
|
|
248
|
+
return MCPToolCallResult(
|
|
249
|
+
is_error=True,
|
|
250
|
+
text=(
|
|
251
|
+
f"Tool {spec.name!r} not advertised by server "
|
|
252
|
+
f"(available: {', '.join(tool_names) or 'none'})"
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
try:
|
|
256
|
+
result: mcp.types.CallToolResult = client.call_async_from_sync(
|
|
257
|
+
client.call_tool_mcp,
|
|
258
|
+
name=spec.name,
|
|
259
|
+
arguments=spec.arguments,
|
|
260
|
+
timeout=timeout,
|
|
261
|
+
)
|
|
262
|
+
except TimeoutError:
|
|
263
|
+
return MCPToolCallResult(
|
|
264
|
+
is_error=True,
|
|
265
|
+
text=f"Tool {spec.name!r} call timed out after {timeout} seconds",
|
|
266
|
+
)
|
|
267
|
+
text = "\n".join(
|
|
268
|
+
block.text
|
|
269
|
+
for block in result.content
|
|
270
|
+
if isinstance(block, mcp.types.TextContent)
|
|
271
|
+
)
|
|
272
|
+
return MCPToolCallResult(is_error=bool(result.isError), text=text)
|
|
163
273
|
|
|
164
274
|
|
|
165
|
-
def _probe_mcp_server(
|
|
275
|
+
def _probe_mcp_server(
|
|
276
|
+
request: MCPTestRequest, cipher: Cipher | None
|
|
277
|
+
) -> MCPTestResponse:
|
|
166
278
|
"""Synchronous probe -- safe to run inside ``run_in_executor``.
|
|
167
279
|
|
|
168
280
|
``create_mcp_tools`` already runs its own event loop in a background
|
|
@@ -171,14 +283,22 @@ def _probe_mcp_server(request: MCPTestRequest) -> MCPTestResponse:
|
|
|
171
283
|
threadpool first.
|
|
172
284
|
"""
|
|
173
285
|
|
|
174
|
-
config = {
|
|
286
|
+
config = {
|
|
287
|
+
"mcpServers": {request.name: _server_to_fastmcp_dict(request.server, cipher)}
|
|
288
|
+
}
|
|
175
289
|
|
|
176
290
|
try:
|
|
177
291
|
# ``create_mcp_tools`` returns a client that owns a background loop
|
|
178
292
|
# and a (possibly long-lived) subprocess. Use the context-manager
|
|
179
293
|
# form so we always tear it down, even when listing succeeded.
|
|
180
294
|
with create_mcp_tools(config, timeout=request.timeout) as client:
|
|
181
|
-
|
|
295
|
+
tool_names = [tool.name for tool in client.tools]
|
|
296
|
+
tool_result: MCPToolCallResult | None = None
|
|
297
|
+
if request.tool_call is not None:
|
|
298
|
+
tool_result = _run_tool_call(
|
|
299
|
+
client, request.tool_call, tool_names, request.timeout
|
|
300
|
+
)
|
|
301
|
+
return MCPTestSuccess(tools=tool_names, tool_result=tool_result)
|
|
182
302
|
except MCPTimeoutError as exc:
|
|
183
303
|
logger.info("MCP test timed out for server %r: %s", request.name, exc)
|
|
184
304
|
return MCPTestFailure(error=str(exc), error_kind="timeout")
|
|
@@ -215,11 +335,21 @@ def _probe_mcp_server(request: MCPTestRequest) -> MCPTestResponse:
|
|
|
215
335
|
"Attempt to connect to a candidate MCP server and list its tools, "
|
|
216
336
|
"without persisting any settings. Useful for validating user input "
|
|
217
337
|
"in 'add MCP server' flows before storing the config. "
|
|
338
|
+
"Optionally invokes one caller-chosen (read-only) tool via "
|
|
339
|
+
"`tool_call` and reports its outcome in `tool_result`, so callers "
|
|
340
|
+
"can verify credentials that are only exercised on tool invocation. "
|
|
341
|
+
"Encrypted `env`/`headers` values round-tripped from settings are "
|
|
342
|
+
"decrypted before the connection is attempted. "
|
|
218
343
|
"Returns 200 with `ok=false` for connection / timeout failures "
|
|
219
344
|
"(those are expected during validation, not server errors)."
|
|
220
345
|
),
|
|
221
346
|
)
|
|
222
|
-
async def test_mcp_server(
|
|
347
|
+
async def test_mcp_server(
|
|
348
|
+
request: MCPTestRequest, http_request: Request
|
|
349
|
+
) -> MCPTestResponse:
|
|
223
350
|
"""Probe a single MCP server config and report whether it works."""
|
|
351
|
+
# Resolve the cipher here: the threadpool function below must not
|
|
352
|
+
# reach back into ``http_request.app.state``.
|
|
353
|
+
cipher = get_cipher(http_request)
|
|
224
354
|
loop = asyncio.get_running_loop()
|
|
225
|
-
return await loop.run_in_executor(None, _probe_mcp_server, request)
|
|
355
|
+
return await loop.run_in_executor(None, _probe_mcp_server, request, cipher)
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
173
|
-
for incremental updates.
|
|
174
|
-
recursively, and a ``null`` value **inside a nested
|
|
175
|
-
entry** — the "unset" primitive that lets a client
|
|
176
|
-
key without round-tripping the whole map. To drop one
|
|
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
|
|
205
|
-
"conversation_settings_diff
|
|
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
|
|
|
@@ -40,7 +40,7 @@ from openhands.sdk.skills import (
|
|
|
40
40
|
)
|
|
41
41
|
from openhands.sdk.skills.skill import (
|
|
42
42
|
DEFAULT_MARKETPLACE_PATH,
|
|
43
|
-
|
|
43
|
+
PUBLIC_SKILLS_REF,
|
|
44
44
|
PUBLIC_SKILLS_REPO,
|
|
45
45
|
_invalidate_public_skills_cache,
|
|
46
46
|
load_skills_from_dir,
|
|
@@ -391,7 +391,7 @@ def sync_public_skills() -> tuple[bool, str]:
|
|
|
391
391
|
try:
|
|
392
392
|
cache_dir = get_skills_cache_dir()
|
|
393
393
|
result = update_skills_repository(
|
|
394
|
-
PUBLIC_SKILLS_REPO,
|
|
394
|
+
PUBLIC_SKILLS_REPO, PUBLIC_SKILLS_REF, cache_dir
|
|
395
395
|
)
|
|
396
396
|
|
|
397
397
|
if result:
|
|
@@ -634,7 +634,7 @@ def _fetch_catalog_entries(marketplace_path: str) -> list[_CatalogEntry]:
|
|
|
634
634
|
"""
|
|
635
635
|
cache_dir = get_skills_cache_dir()
|
|
636
636
|
repo_path = update_skills_repository(
|
|
637
|
-
PUBLIC_SKILLS_REPO,
|
|
637
|
+
PUBLIC_SKILLS_REPO, PUBLIC_SKILLS_REF, cache_dir
|
|
638
638
|
)
|
|
639
639
|
|
|
640
640
|
if repo_path is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-agent-server
|
|
3
|
-
Version: 1.
|
|
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,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
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__init__.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/env_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/git_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/llm_router.py
RENAMED
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/middleware.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/models.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/openapi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/pub_sub.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/sockets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.25.0 → openhands_agent_server-1.27.0}/openhands/agent_server/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|