openhands-agent-server 1.29.3__tar.gz → 1.30.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.29.3 → openhands_agent_server-1.30.0}/PKG-INFO +1 -1
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/agent_profiles_router.py +79 -102
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/api.py +2 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/config.py +23 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/conversation_router.py +42 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/conversation_service.py +7 -1
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/event_service.py +20 -6
- openhands_agent_server-1.30.0/openhands/agent_server/file_router.py +989 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/openai/router.py +59 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/openai/service.py +3 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/skills_router.py +33 -2
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/skills_service.py +67 -5
- openhands_agent_server-1.30.0/openhands/agent_server/sub_agents_router.py +130 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/SOURCES.txt +2 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/pyproject.toml +1 -1
- openhands_agent_server-1.29.3/openhands/agent_server/file_router.py +0 -333
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/mcp_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.29.3 → openhands_agent_server-1.30.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.30.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
|
|
@@ -12,11 +12,9 @@ MCP references and returns :class:`~openhands.sdk.profiles.AgentProfileDiagnosti
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import copy
|
|
15
|
-
import shlex
|
|
16
15
|
from collections.abc import Iterator
|
|
17
16
|
from contextlib import contextmanager
|
|
18
17
|
from typing import Annotated, Any
|
|
19
|
-
from uuid import UUID, uuid4
|
|
20
18
|
|
|
21
19
|
from fastapi import APIRouter, HTTPException, Path, Request, status
|
|
22
20
|
from pydantic import BaseModel, Field, ValidationError
|
|
@@ -34,20 +32,26 @@ from openhands.agent_server.persistence import (
|
|
|
34
32
|
get_llm_profile_store,
|
|
35
33
|
get_settings_store,
|
|
36
34
|
)
|
|
35
|
+
from openhands.agent_server.profiles_router import MAX_PROFILES
|
|
36
|
+
from openhands.sdk.llm import LLM
|
|
37
|
+
from openhands.sdk.llm.llm_profile_store import (
|
|
38
|
+
ProfileLimitExceeded as LLMProfileLimitExceeded,
|
|
39
|
+
)
|
|
37
40
|
from openhands.sdk.logger import get_logger
|
|
38
41
|
from openhands.sdk.profiles import (
|
|
42
|
+
SEED_PROFILE_NAME,
|
|
39
43
|
ACPAgentProfile,
|
|
40
44
|
AgentProfileDiagnostics,
|
|
41
45
|
AgentProfileStore,
|
|
42
46
|
OpenHandsAgentProfile,
|
|
43
47
|
ProfileLimitExceeded,
|
|
44
|
-
|
|
48
|
+
build_seed_profile,
|
|
45
49
|
resolve_agent_profile_dry_run,
|
|
50
|
+
safe_validation_error_detail,
|
|
51
|
+
save_profile_preserving_identity,
|
|
46
52
|
validate_agent_profile,
|
|
47
53
|
)
|
|
48
54
|
from openhands.sdk.profiles.agent_profile_store import PROFILE_NAME_PATTERN
|
|
49
|
-
from openhands.sdk.settings import AgentSettingsConfig
|
|
50
|
-
from openhands.sdk.settings.model import VerificationSettings
|
|
51
55
|
from openhands.sdk.utils.cipher import Cipher
|
|
52
56
|
from openhands.sdk.utils.pydantic_secrets import decrypt_str_with_cipher_or_keep
|
|
53
57
|
|
|
@@ -58,9 +62,6 @@ agent_profiles_router = APIRouter(prefix="/agent-profiles", tags=["Agent Profile
|
|
|
58
62
|
|
|
59
63
|
MAX_AGENT_PROFILES = 50
|
|
60
64
|
|
|
61
|
-
# Name the lazily-seeded migration profile (and its LLM ref fallback).
|
|
62
|
-
SEED_PROFILE_NAME = "default"
|
|
63
|
-
|
|
64
65
|
ProfileName = Annotated[
|
|
65
66
|
str,
|
|
66
67
|
Path(min_length=1, max_length=64, pattern=PROFILE_NAME_PATTERN),
|
|
@@ -179,65 +180,57 @@ def _decrypt_profile_mcp_tools(
|
|
|
179
180
|
return profile.model_copy(update={"skills": new_skills})
|
|
180
181
|
|
|
181
182
|
|
|
182
|
-
def
|
|
183
|
-
"""
|
|
184
|
-
|
|
185
|
-
Drops ``critic_api_key`` — the profile is secret-free; the critic reuses
|
|
186
|
-
the resolved LLM profile's key.
|
|
187
|
-
"""
|
|
188
|
-
return ProfileVerificationSettings(
|
|
189
|
-
critic_enabled=v.critic_enabled,
|
|
190
|
-
critic_mode=v.critic_mode,
|
|
191
|
-
enable_iterative_refinement=v.enable_iterative_refinement,
|
|
192
|
-
critic_threshold=v.critic_threshold,
|
|
193
|
-
max_refinement_iterations=v.max_refinement_iterations,
|
|
194
|
-
critic_server_url=v.critic_server_url,
|
|
195
|
-
critic_model_name=v.critic_model_name,
|
|
196
|
-
)
|
|
183
|
+
def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
|
|
184
|
+
"""Mirror the live LLM config into a ``SEED_PROFILE_NAME`` LLM profile.
|
|
197
185
|
|
|
186
|
+
``build_seed_profile`` falls back to the literal name ``SEED_PROFILE_NAME``
|
|
187
|
+
when no LLM profile is active, on the assumption that a profile by that
|
|
188
|
+
name exists — but nothing ever created one, so the seeded agent profile's
|
|
189
|
+
``llm_profile_ref`` dangled from birth (#3933). Mirrors the cloud
|
|
190
|
+
``SaasSettingsStore``'s legacy-LLM backfill: materialize the current
|
|
191
|
+
``agent_settings.llm`` under that name so the reference resolves, unless a
|
|
192
|
+
profile is already stored there (never clobber it).
|
|
198
193
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
stable representation of the user's current configuration (the active
|
|
206
|
-
pointer is otherwise just a lightweight id). ``mcp_server_refs=None`` exposes
|
|
207
|
-
all of the user's MCP servers. An OpenHands profile references the active LLM
|
|
208
|
-
profile (falling back to ``"default"`` when none is set — a soft ref the
|
|
209
|
-
resolver checks at materialize time).
|
|
194
|
+
Existence is checked via ``load()``, not ``list()``: the store resolves a
|
|
195
|
+
name straight to a filesystem path, so on a case-insensitive filesystem
|
|
196
|
+
(macOS/Windows) a differently-cased ``Default`` profile already occupies
|
|
197
|
+
the ``default`` path even though it wouldn't case-sensitively match a
|
|
198
|
+
``list()`` membership check — that mismatch would otherwise let ``save()``
|
|
199
|
+
silently clobber it.
|
|
210
200
|
"""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
201
|
+
llm_store = get_llm_profile_store()
|
|
202
|
+
with _store_errors():
|
|
203
|
+
try:
|
|
204
|
+
llm_store.load(SEED_PROFILE_NAME, cipher=cipher)
|
|
205
|
+
return SEED_PROFILE_NAME
|
|
206
|
+
except FileNotFoundError:
|
|
207
|
+
pass
|
|
208
|
+
except ValueError:
|
|
209
|
+
# Something already occupies the name (e.g. corrupted/unparsable
|
|
210
|
+
# file) — never overwrite it; a broken ref surfaces at
|
|
211
|
+
# materialize/launch time instead.
|
|
212
|
+
logger.warning(
|
|
213
|
+
f"Default LLM profile '{SEED_PROFILE_NAME}' exists but "
|
|
214
|
+
"failed to load; leaving it as-is"
|
|
215
|
+
)
|
|
216
|
+
return SEED_PROFILE_NAME
|
|
217
|
+
try:
|
|
218
|
+
llm_store.save(
|
|
219
|
+
SEED_PROFILE_NAME,
|
|
220
|
+
llm,
|
|
221
|
+
include_secrets=True,
|
|
222
|
+
cipher=cipher,
|
|
223
|
+
max_profiles=MAX_PROFILES,
|
|
224
|
+
)
|
|
225
|
+
logger.info(f"Seeded default LLM profile '{SEED_PROFILE_NAME}'")
|
|
226
|
+
except LLMProfileLimitExceeded:
|
|
227
|
+
# Can't mirror the live LLM as a profile; the agent profile's
|
|
228
|
+
# llm_profile_ref will still dangle, but no worse than before.
|
|
229
|
+
logger.warning(
|
|
230
|
+
"Could not seed default LLM profile "
|
|
231
|
+
f"'{SEED_PROFILE_NAME}': profile limit reached"
|
|
232
|
+
)
|
|
233
|
+
return SEED_PROFILE_NAME
|
|
241
234
|
|
|
242
235
|
|
|
243
236
|
def _seed_default_profile(
|
|
@@ -251,13 +244,25 @@ def _seed_default_profile(
|
|
|
251
244
|
The lock spans empty-check + save + pointer write so concurrent first
|
|
252
245
|
requests seed exactly once and the pointer matches the persisted id.
|
|
253
246
|
"""
|
|
254
|
-
with _store_errors(), store.
|
|
247
|
+
with _store_errors(), store.lock():
|
|
255
248
|
# Double-checked under the lock: a concurrent first request may have
|
|
256
249
|
# already seeded (the outer emptiness check in the list endpoint is
|
|
257
250
|
# unlocked).
|
|
258
251
|
if store.list():
|
|
259
252
|
return
|
|
260
|
-
|
|
253
|
+
active_llm_profile = settings.active_profile
|
|
254
|
+
# Falsy check (not `is None`): mirrors build_seed_profile's own
|
|
255
|
+
# `active_llm_profile or SEED_PROFILE_NAME` fallback. A stray empty
|
|
256
|
+
# string (e.g. a hand-edited/legacy settings.json, or a direct
|
|
257
|
+
# PersistedSettings(active_profile="") construction — the HTTP PATCH
|
|
258
|
+
# payload's pattern validator blocks "" but the stored field has no
|
|
259
|
+
# such constraint) is falsy there too, so the backfill must trigger
|
|
260
|
+
# on the same condition or the exact #3933 dangling ref reappears.
|
|
261
|
+
if not active_llm_profile and settings.agent_settings.agent_kind != "acp":
|
|
262
|
+
active_llm_profile = _seed_default_llm_profile(
|
|
263
|
+
settings.agent_settings.llm, cipher
|
|
264
|
+
)
|
|
265
|
+
profile = build_seed_profile(settings.agent_settings, active_llm_profile)
|
|
261
266
|
# Settings persist skills[].mcp_tools encrypted (and never decrypt on
|
|
262
267
|
# load), so decrypt before re-encrypting at save to avoid double-encrypt.
|
|
263
268
|
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
@@ -284,29 +289,6 @@ def _summary_id_for_name(store: AgentProfileStore, name: str) -> str | None:
|
|
|
284
289
|
return None
|
|
285
290
|
|
|
286
291
|
|
|
287
|
-
def _existing_identity(
|
|
288
|
-
store: AgentProfileStore, name: str
|
|
289
|
-
) -> tuple[UUID | None, int | None]:
|
|
290
|
-
"""Return the stable ``(id, revision)`` of the profile under ``name``.
|
|
291
|
-
|
|
292
|
-
Used to keep ``id`` stable across an overwrite — the active pointer is keyed
|
|
293
|
-
on it — and to bump ``revision`` monotonically. Ignores a malformed stored
|
|
294
|
-
id (treated as no prior identity).
|
|
295
|
-
"""
|
|
296
|
-
with _store_errors():
|
|
297
|
-
for summary in store.list_summaries():
|
|
298
|
-
if summary.get("name") != name:
|
|
299
|
-
continue
|
|
300
|
-
sid = summary.get("id")
|
|
301
|
-
rev = summary.get("revision")
|
|
302
|
-
try:
|
|
303
|
-
parsed = UUID(str(sid)) if sid is not None else None
|
|
304
|
-
except (ValueError, TypeError):
|
|
305
|
-
parsed = None
|
|
306
|
-
return parsed, rev if isinstance(rev, int) else None
|
|
307
|
-
return None, None
|
|
308
|
-
|
|
309
|
-
|
|
310
292
|
@agent_profiles_router.get("", response_model=AgentProfileListResponse)
|
|
311
293
|
async def list_agent_profiles(request: Request) -> AgentProfileListResponse:
|
|
312
294
|
"""List all stored agent profiles and the active pointer.
|
|
@@ -393,7 +375,7 @@ async def save_agent_profile(
|
|
|
393
375
|
# MCPConfig error embeds the input (which may carry secrets) in ``msg``.
|
|
394
376
|
raise HTTPException(
|
|
395
377
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
396
|
-
detail=
|
|
378
|
+
detail=safe_validation_error_detail(e),
|
|
397
379
|
)
|
|
398
380
|
except Exception:
|
|
399
381
|
# Any other validation failure (e.g. SkillValidationError from a
|
|
@@ -412,19 +394,14 @@ async def save_agent_profile(
|
|
|
412
394
|
store = get_agent_profile_store()
|
|
413
395
|
# The id is server-managed (the active pointer is keyed on it): overwrite
|
|
414
396
|
# keeps the namesake's id and bumps revision; create mints a fresh id,
|
|
415
|
-
# ignoring any client-supplied one.
|
|
416
|
-
#
|
|
417
|
-
#
|
|
397
|
+
# ignoring any client-supplied one. ``save_profile_preserving_identity``
|
|
398
|
+
# holds the store lock across read + mint + save so two concurrent creates
|
|
399
|
+
# of the same new name can't both mint an id and clobber each other.
|
|
418
400
|
try:
|
|
419
|
-
with _store_errors()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
update={"id": existing_id, "revision": (existing_rev or 0) + 1}
|
|
424
|
-
)
|
|
425
|
-
else:
|
|
426
|
-
profile = profile.model_copy(update={"id": uuid4()})
|
|
427
|
-
store.save(profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES)
|
|
401
|
+
with _store_errors():
|
|
402
|
+
save_profile_preserving_identity(
|
|
403
|
+
store, profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES
|
|
404
|
+
)
|
|
428
405
|
except ProfileLimitExceeded:
|
|
429
406
|
raise HTTPException(
|
|
430
407
|
status_code=status.HTTP_409_CONFLICT,
|
{openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/api.py
RENAMED
|
@@ -60,6 +60,7 @@ from openhands.agent_server.server_details_router import (
|
|
|
60
60
|
from openhands.agent_server.settings_router import settings_router
|
|
61
61
|
from openhands.agent_server.skills_router import skills_router
|
|
62
62
|
from openhands.agent_server.sockets import sockets_router
|
|
63
|
+
from openhands.agent_server.sub_agents_router import sub_agents_router
|
|
63
64
|
from openhands.agent_server.tool_preload_service import get_tool_preload_service
|
|
64
65
|
from openhands.agent_server.tool_router import tool_router
|
|
65
66
|
from openhands.agent_server.vscode_router import vscode_router
|
|
@@ -349,6 +350,7 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
349
350
|
api_router.include_router(vscode_router)
|
|
350
351
|
api_router.include_router(desktop_router)
|
|
351
352
|
api_router.include_router(skills_router)
|
|
353
|
+
api_router.include_router(sub_agents_router)
|
|
352
354
|
api_router.include_router(plugins_router)
|
|
353
355
|
api_router.include_router(hooks_router)
|
|
354
356
|
api_router.include_router(llm_router)
|
{openhands_agent_server-1.29.3 → openhands_agent_server-1.30.0}/openhands/agent_server/config.py
RENAMED
|
@@ -6,6 +6,7 @@ from typing import Any, ClassVar
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
|
8
8
|
|
|
9
|
+
from openhands.agent_server.conversation_lease import DEFAULT_LEASE_TTL_SECONDS
|
|
9
10
|
from openhands.agent_server.env_parser import (
|
|
10
11
|
MISSING,
|
|
11
12
|
_get_default_parsers,
|
|
@@ -13,6 +14,7 @@ from openhands.agent_server.env_parser import (
|
|
|
13
14
|
get_env_parser,
|
|
14
15
|
merge,
|
|
15
16
|
)
|
|
17
|
+
from openhands.sdk.marketplace.registration import MarketplaceRegistration
|
|
16
18
|
from openhands.sdk.utils.cipher import Cipher
|
|
17
19
|
|
|
18
20
|
|
|
@@ -236,6 +238,13 @@ class Config(BaseModel):
|
|
|
236
238
|
"The URL where this agent server instance is available externally"
|
|
237
239
|
),
|
|
238
240
|
)
|
|
241
|
+
registered_marketplaces: list[MarketplaceRegistration] = Field(
|
|
242
|
+
default_factory=list,
|
|
243
|
+
description=(
|
|
244
|
+
"Default marketplace registrations for plugin and skill loading. "
|
|
245
|
+
"Can be configured with OH_REGISTERED_MARKETPLACES as a JSON list."
|
|
246
|
+
),
|
|
247
|
+
)
|
|
239
248
|
deferred_init: bool = Field(
|
|
240
249
|
default=False,
|
|
241
250
|
description=(
|
|
@@ -247,6 +256,20 @@ class Config(BaseModel):
|
|
|
247
256
|
"configuration is delivered later."
|
|
248
257
|
),
|
|
249
258
|
)
|
|
259
|
+
lease_ttl_seconds: float = Field(
|
|
260
|
+
default=DEFAULT_LEASE_TTL_SECONDS,
|
|
261
|
+
ge=0.0,
|
|
262
|
+
description=(
|
|
263
|
+
"How long (in seconds) a conversation ownership lease remains valid "
|
|
264
|
+
"without renewal. The lease prevents two server instances from "
|
|
265
|
+
"concurrently owning the same conversation when storage is shared "
|
|
266
|
+
"across instances. Set to 0 to disable leasing entirely, which is "
|
|
267
|
+
"appropriate for single-instance deployments where concurrent "
|
|
268
|
+
"ownership is impossible. Values between 0 and "
|
|
269
|
+
"LEASE_RENEW_INTERVAL_SECONDS (15 s) are valid but cause the lease "
|
|
270
|
+
"to expire before the first renewal, effectively making it one-shot."
|
|
271
|
+
),
|
|
272
|
+
)
|
|
250
273
|
model_config: ClassVar[ConfigDict] = {"frozen": True}
|
|
251
274
|
|
|
252
275
|
@property
|
|
@@ -42,6 +42,12 @@ from openhands.agent_server.models import (
|
|
|
42
42
|
)
|
|
43
43
|
from openhands.sdk import LLM, Agent, TextContent
|
|
44
44
|
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
45
|
+
from openhands.sdk.marketplace.registry import (
|
|
46
|
+
MarketplaceNotFoundError,
|
|
47
|
+
PluginNotFoundError,
|
|
48
|
+
PluginResolutionError,
|
|
49
|
+
)
|
|
50
|
+
from openhands.sdk.plugin import PluginFetchError
|
|
45
51
|
from openhands.sdk.profiles.resolver import DanglingMcpServerRef, ProfileNotFound
|
|
46
52
|
from openhands.sdk.tool.client_tool import ClientToolRegistrationError
|
|
47
53
|
from openhands.sdk.workspace import LocalWorkspace
|
|
@@ -497,6 +503,42 @@ async def switch_conversation_llm(
|
|
|
497
503
|
return Success()
|
|
498
504
|
|
|
499
505
|
|
|
506
|
+
@conversation_router.post(
|
|
507
|
+
"/{conversation_id}/load_plugin",
|
|
508
|
+
responses={
|
|
509
|
+
400: {"description": "Invalid plugin reference or inactive conversation"},
|
|
510
|
+
404: {"description": "Conversation or plugin not found"},
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
async def load_conversation_plugin(
|
|
514
|
+
conversation_id: UUID,
|
|
515
|
+
plugin_ref: str = Body(..., embed=True),
|
|
516
|
+
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
517
|
+
) -> Success:
|
|
518
|
+
"""Load a plugin from the conversation's registered marketplaces."""
|
|
519
|
+
event_service = await conversation_service.get_event_service(conversation_id)
|
|
520
|
+
if event_service is None:
|
|
521
|
+
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
|
522
|
+
try:
|
|
523
|
+
await event_service.load_plugin(plugin_ref)
|
|
524
|
+
except (PluginNotFoundError, MarketplaceNotFoundError) as e:
|
|
525
|
+
raise HTTPException(
|
|
526
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
527
|
+
detail=str(e),
|
|
528
|
+
)
|
|
529
|
+
except (
|
|
530
|
+
PluginResolutionError,
|
|
531
|
+
PluginFetchError,
|
|
532
|
+
FileNotFoundError,
|
|
533
|
+
ValueError,
|
|
534
|
+
) as e:
|
|
535
|
+
raise HTTPException(
|
|
536
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
537
|
+
detail=str(e),
|
|
538
|
+
)
|
|
539
|
+
return Success()
|
|
540
|
+
|
|
541
|
+
|
|
500
542
|
@conversation_router.post(
|
|
501
543
|
"/{conversation_id}/switch_acp_model",
|
|
502
544
|
responses={
|
|
@@ -13,7 +13,10 @@ import httpx
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
15
|
from openhands.agent_server.config import Config, WebhookSpec
|
|
16
|
-
from openhands.agent_server.conversation_lease import
|
|
16
|
+
from openhands.agent_server.conversation_lease import (
|
|
17
|
+
DEFAULT_LEASE_TTL_SECONDS,
|
|
18
|
+
ConversationLeaseHeldError,
|
|
19
|
+
)
|
|
17
20
|
from openhands.agent_server.event_service import (
|
|
18
21
|
LEASE_RENEW_INTERVAL_SECONDS,
|
|
19
22
|
EventService,
|
|
@@ -442,6 +445,7 @@ class ConversationService:
|
|
|
442
445
|
cipher: Cipher | None = None
|
|
443
446
|
owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
|
|
444
447
|
max_concurrent_runs: int = 10
|
|
448
|
+
lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
|
|
445
449
|
_event_services: dict[UUID, EventService] | None = field(default=None, init=False)
|
|
446
450
|
_conversation_webhook_subscribers: list["ConversationWebhookSubscriber"] = field(
|
|
447
451
|
default_factory=list, init=False
|
|
@@ -1213,6 +1217,7 @@ class ConversationService:
|
|
|
1213
1217
|
),
|
|
1214
1218
|
cipher=config.cipher,
|
|
1215
1219
|
max_concurrent_runs=config.max_concurrent_runs,
|
|
1220
|
+
lease_ttl_seconds=config.lease_ttl_seconds,
|
|
1216
1221
|
)
|
|
1217
1222
|
|
|
1218
1223
|
async def _start_event_service(self, stored: StoredConversation) -> EventService:
|
|
@@ -1225,6 +1230,7 @@ class ConversationService:
|
|
|
1225
1230
|
conversations_dir=self.conversations_dir,
|
|
1226
1231
|
cipher=self.cipher,
|
|
1227
1232
|
owner_instance_id=self.owner_instance_id,
|
|
1233
|
+
lease_ttl_seconds=self.lease_ttl_seconds,
|
|
1228
1234
|
)
|
|
1229
1235
|
# Lease renewal is handled by the centralized
|
|
1230
1236
|
# _renew_all_leases_loop task on ConversationService.
|
|
@@ -9,6 +9,7 @@ from uuid import UUID, uuid4
|
|
|
9
9
|
from pydantic import ValidationError
|
|
10
10
|
|
|
11
11
|
from openhands.agent_server.conversation_lease import (
|
|
12
|
+
DEFAULT_LEASE_TTL_SECONDS,
|
|
12
13
|
ConversationLease,
|
|
13
14
|
ConversationOwnershipLostError,
|
|
14
15
|
)
|
|
@@ -81,6 +82,7 @@ class EventService:
|
|
|
81
82
|
conversations_dir: Path
|
|
82
83
|
cipher: Cipher | None = None
|
|
83
84
|
owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
|
|
85
|
+
lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
|
|
84
86
|
_conversation: LocalConversation | None = field(default=None, init=False)
|
|
85
87
|
_pub_sub: PubSub[Event] = field(
|
|
86
88
|
default_factory=lambda: PubSub[Event](max_subscribers=50), init=False
|
|
@@ -733,12 +735,16 @@ class EventService:
|
|
|
733
735
|
|
|
734
736
|
# self.stored contains an Agent configuration we can instantiate
|
|
735
737
|
self.conversation_dir.mkdir(parents=True, exist_ok=True)
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
738
|
+
# lease_ttl_seconds=0 disables leasing for single-instance deployments
|
|
739
|
+
# where shared-storage stale leases would otherwise block pod restarts.
|
|
740
|
+
if self.lease_ttl_seconds > 0:
|
|
741
|
+
self._lease = ConversationLease(
|
|
742
|
+
conversation_dir=self.conversation_dir,
|
|
743
|
+
owner_instance_id=self.owner_instance_id,
|
|
744
|
+
ttl_seconds=self.lease_ttl_seconds,
|
|
745
|
+
)
|
|
746
|
+
lease_claim = self._lease.claim()
|
|
747
|
+
self._lease_generation = lease_claim.generation
|
|
742
748
|
workspace = self.stored.workspace
|
|
743
749
|
assert isinstance(workspace, LocalWorkspace)
|
|
744
750
|
working_dir = Path(workspace.working_dir)
|
|
@@ -825,6 +831,7 @@ class EventService:
|
|
|
825
831
|
user_id=self.stored.user_id,
|
|
826
832
|
observability_metadata=self.stored.observability_metadata,
|
|
827
833
|
observability_tags=self.stored.observability_tags,
|
|
834
|
+
observability_span_name=self.stored.observability_span_name,
|
|
828
835
|
)
|
|
829
836
|
|
|
830
837
|
conversation.set_confirmation_policy(self.stored.confirmation_policy)
|
|
@@ -1362,6 +1369,13 @@ class EventService:
|
|
|
1362
1369
|
None, self._conversation.set_security_analyzer, security_analyzer
|
|
1363
1370
|
)
|
|
1364
1371
|
|
|
1372
|
+
async def load_plugin(self, plugin_ref: str) -> None:
|
|
1373
|
+
"""Load a marketplace plugin into the active conversation."""
|
|
1374
|
+
if self._conversation is None:
|
|
1375
|
+
raise ValueError("inactive_service")
|
|
1376
|
+
loop = asyncio.get_running_loop()
|
|
1377
|
+
await loop.run_in_executor(None, self._conversation.load_plugin, plugin_ref)
|
|
1378
|
+
|
|
1365
1379
|
async def switch_acp_model(self, model: str) -> None:
|
|
1366
1380
|
"""Switch the model on an ACP conversation.
|
|
1367
1381
|
|