openhands-agent-server 1.31.0__tar.gz → 1.31.2__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.31.0 → openhands_agent_server-1.31.2}/PKG-INFO +1 -1
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/agent_profiles_router.py +2 -69
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_service.py +22 -2
- openhands_agent_server-1.31.2/openhands/agent_server/event_compat.py +50 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/event_router.py +17 -4
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/event_service.py +22 -14
- openhands_agent_server-1.31.2/openhands/agent_server/mcp_oauth_store.py +352 -0
- openhands_agent_server-1.31.2/openhands/agent_server/mcp_router.py +824 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/models.py +9 -3
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/profiles_router.py +3 -7
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/settings_router.py +3 -2
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/sockets.py +3 -2
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/sub_agents_router.py +11 -2
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/SOURCES.txt +4 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/pyproject.toml +1 -1
- openhands_agent_server-1.31.0/openhands/agent_server/mcp_router.py +0 -355
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/api.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/file_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openai/service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/skills_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.31.0 → openhands_agent_server-1.31.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-agent-server
|
|
3
|
-
Version: 1.31.
|
|
3
|
+
Version: 1.31.2
|
|
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,7 +12,6 @@ MCP references and returns :class:`~openhands.sdk.profiles.AgentProfileDiagnosti
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
-
import copy
|
|
16
15
|
from collections.abc import Iterator
|
|
17
16
|
from contextlib import contextmanager
|
|
18
17
|
from typing import Annotated, Any
|
|
@@ -42,10 +41,8 @@ from openhands.sdk.llm.llm_profile_store import (
|
|
|
42
41
|
from openhands.sdk.logger import get_logger
|
|
43
42
|
from openhands.sdk.profiles import (
|
|
44
43
|
SEED_PROFILE_NAME,
|
|
45
|
-
ACPAgentProfile,
|
|
46
44
|
AgentProfileDiagnostics,
|
|
47
45
|
AgentProfileStore,
|
|
48
|
-
OpenHandsAgentProfile,
|
|
49
46
|
ProfileLimitExceeded,
|
|
50
47
|
build_seed_profile,
|
|
51
48
|
resolve_agent_profile_dry_run,
|
|
@@ -55,7 +52,6 @@ from openhands.sdk.profiles import (
|
|
|
55
52
|
)
|
|
56
53
|
from openhands.sdk.profiles.agent_profile_store import PROFILE_NAME_PATTERN
|
|
57
54
|
from openhands.sdk.utils.cipher import Cipher
|
|
58
|
-
from openhands.sdk.utils.pydantic_secrets import decrypt_str_with_cipher_or_keep
|
|
59
55
|
|
|
60
56
|
|
|
61
57
|
logger = get_logger(__name__)
|
|
@@ -134,54 +130,6 @@ def _store_errors() -> Iterator[None]:
|
|
|
134
130
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
135
131
|
|
|
136
132
|
|
|
137
|
-
def _decrypt_mcp_tools(tools: dict[str, Any], cipher: Cipher) -> dict[str, Any]:
|
|
138
|
-
"""Return a copy of an ``mcp_tools`` dict with env/headers Fernet tokens
|
|
139
|
-
decrypted. Non-Fernet (plaintext) values pass through unchanged."""
|
|
140
|
-
servers = tools.get("mcpServers")
|
|
141
|
-
if not isinstance(servers, dict):
|
|
142
|
-
return tools
|
|
143
|
-
out = copy.deepcopy(tools)
|
|
144
|
-
for server in out["mcpServers"].values():
|
|
145
|
-
if not isinstance(server, dict):
|
|
146
|
-
continue
|
|
147
|
-
for key in ("env", "headers"):
|
|
148
|
-
mapping = server.get(key)
|
|
149
|
-
if isinstance(mapping, dict):
|
|
150
|
-
server[key] = {
|
|
151
|
-
k: decrypt_str_with_cipher_or_keep(
|
|
152
|
-
cipher, v, description="MCP env/headers"
|
|
153
|
-
)
|
|
154
|
-
for k, v in mapping.items()
|
|
155
|
-
}
|
|
156
|
-
return out
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _decrypt_profile_mcp_tools(
|
|
160
|
-
profile: OpenHandsAgentProfile | ACPAgentProfile, cipher: Cipher | None
|
|
161
|
-
) -> OpenHandsAgentProfile | ACPAgentProfile:
|
|
162
|
-
"""Decrypt Fernet ``skills[].mcp_tools`` env/headers (no-op without a cipher).
|
|
163
|
-
|
|
164
|
-
The store masks/encrypts these on save but has no symmetric load-time
|
|
165
|
-
validator, so values arrive as ciphertext on both GET (at-rest) and save
|
|
166
|
-
(client round-trip). Decrypting here lets GET re-mask from plaintext and
|
|
167
|
-
stops save from double-encrypting an already-encrypted value.
|
|
168
|
-
"""
|
|
169
|
-
if cipher is None:
|
|
170
|
-
return profile
|
|
171
|
-
skills = getattr(profile, "skills", None)
|
|
172
|
-
if not skills:
|
|
173
|
-
return profile
|
|
174
|
-
new_skills = [
|
|
175
|
-
skill.model_copy(
|
|
176
|
-
update={"mcp_tools": _decrypt_mcp_tools(skill.mcp_tools, cipher)}
|
|
177
|
-
)
|
|
178
|
-
if skill.mcp_tools
|
|
179
|
-
else skill
|
|
180
|
-
for skill in skills
|
|
181
|
-
]
|
|
182
|
-
return profile.model_copy(update={"skills": new_skills})
|
|
183
|
-
|
|
184
|
-
|
|
185
133
|
def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
|
|
186
134
|
"""Mirror the live LLM config into a ``SEED_PROFILE_NAME`` LLM profile.
|
|
187
135
|
|
|
@@ -265,9 +213,6 @@ def _seed_default_profile(
|
|
|
265
213
|
settings.agent_settings.llm, cipher
|
|
266
214
|
)
|
|
267
215
|
profile = build_seed_profile(settings.agent_settings, active_llm_profile)
|
|
268
|
-
# Settings persist skills[].mcp_tools encrypted (and never decrypt on
|
|
269
|
-
# load), so decrypt before re-encrypting at save to avoid double-encrypt.
|
|
270
|
-
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
271
216
|
store.save(profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES)
|
|
272
217
|
|
|
273
218
|
profile_id = str(profile.id)
|
|
@@ -342,10 +287,6 @@ async def get_agent_profile(
|
|
|
342
287
|
detail=f"Agent profile '{name}' not found",
|
|
343
288
|
)
|
|
344
289
|
|
|
345
|
-
# The store leaves skills[].mcp_tools encrypted on load; decrypt to plaintext
|
|
346
|
-
# so the expose serializer can correctly redact / re-encrypt / reveal them.
|
|
347
|
-
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
348
|
-
|
|
349
290
|
context = build_expose_context(expose_mode, cipher)
|
|
350
291
|
with translate_missing_cipher():
|
|
351
292
|
payload = profile.model_dump(mode="json", context=context)
|
|
@@ -374,7 +315,8 @@ async def save_agent_profile(
|
|
|
374
315
|
except ValidationError as e:
|
|
375
316
|
# Match FastAPI's request-validation shape (``detail`` is a list of error
|
|
376
317
|
# objects), but surface only ``loc``/``type`` — a nested mcp_tools
|
|
377
|
-
#
|
|
318
|
+
# FastMCP config error embeds the input (which may carry secrets) in
|
|
319
|
+
# ``msg``.
|
|
378
320
|
raise HTTPException(
|
|
379
321
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
380
322
|
detail=safe_validation_error_detail(e),
|
|
@@ -388,11 +330,6 @@ async def save_agent_profile(
|
|
|
388
330
|
detail="Invalid agent profile",
|
|
389
331
|
)
|
|
390
332
|
|
|
391
|
-
# A client editing a profile fetched with X-Expose-Secrets: encrypted posts
|
|
392
|
-
# back Fernet tokens; decrypt them so the save re-encrypts the original
|
|
393
|
-
# secret once rather than double-encrypting the token.
|
|
394
|
-
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
395
|
-
|
|
396
333
|
store = get_agent_profile_store()
|
|
397
334
|
# The id is server-managed (the active pointer is keyed on it): overwrite
|
|
398
335
|
# keeps the namesake's id and bumps revision; create mints a fresh id,
|
|
@@ -564,10 +501,6 @@ async def materialize_agent_profile(
|
|
|
564
501
|
detail=f"Agent profile '{name}' not found",
|
|
565
502
|
)
|
|
566
503
|
|
|
567
|
-
# The store leaves skills[].mcp_tools encrypted on load; decrypt so the
|
|
568
|
-
# resolver builds settings from plaintext (not ciphertext) values.
|
|
569
|
-
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
570
|
-
|
|
571
504
|
config = get_config(request)
|
|
572
505
|
settings = get_settings_store(config).load() or PersistedSettings()
|
|
573
506
|
mcp_config = settings.agent_settings.mcp_config
|
|
@@ -48,12 +48,15 @@ from openhands.sdk.event import MessageEvent
|
|
|
48
48
|
from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
|
|
49
49
|
from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
|
|
50
50
|
from openhands.sdk.git.utils import run_git_command, validate_git_repository
|
|
51
|
+
from openhands.sdk.mcp.utils import MCPToolProvider
|
|
52
|
+
from openhands.sdk.tool import BROWSER_TOOL_NAME, Tool, is_tool_usable
|
|
51
53
|
from openhands.sdk.tool.client_tool import register_client_tools
|
|
52
54
|
from openhands.sdk.utils.cipher import Cipher
|
|
53
55
|
from openhands.sdk.workspace import LocalWorkspace
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
if TYPE_CHECKING:
|
|
59
|
+
from openhands.sdk.mcp.config import MCPServer
|
|
57
60
|
from openhands.sdk.subagent.schema import AgentDefinition
|
|
58
61
|
|
|
59
62
|
CONVERSATION_WORKTREE_ROOT = Path("/tmp/conversation-worktrees")
|
|
@@ -249,14 +252,14 @@ logger = logging.getLogger(__name__)
|
|
|
249
252
|
def _resolve_agent_from_profile(
|
|
250
253
|
profile_id: "UUID",
|
|
251
254
|
cipher: "Cipher | None",
|
|
252
|
-
mcp_config: "
|
|
255
|
+
mcp_config: "dict[str, MCPServer]",
|
|
253
256
|
) -> "tuple[AgentBase, LaunchedAgentProfile]":
|
|
254
257
|
"""Load and resolve an agent profile by id, returning the built agent + provenance.
|
|
255
258
|
|
|
256
259
|
Runs synchronously (call via ``asyncio.to_thread`` from async context).
|
|
257
260
|
|
|
258
261
|
Args:
|
|
259
|
-
mcp_config: Global MCP
|
|
262
|
+
mcp_config: Global MCP servers already loaded by the caller using the
|
|
260
263
|
server's cipher. Passed explicitly so this free function never
|
|
261
264
|
touches the settings-store singleton (which may not have been
|
|
262
265
|
initialised with the correct cipher yet).
|
|
@@ -312,6 +315,17 @@ def _resolve_agent_from_profile(
|
|
|
312
315
|
raise ValueError(f"Profile '{profile_name}' failed to resolve: {exc}") from exc
|
|
313
316
|
|
|
314
317
|
agent = settings_config.create_agent()
|
|
318
|
+
# Browser is deliberately absent from the deterministic SDK default
|
|
319
|
+
# (environment-dependent); this server knows its runtime, so it injects
|
|
320
|
+
# browser when usable. An explicit profile.tools list is authoritative.
|
|
321
|
+
if (
|
|
322
|
+
profile.agent_kind == "openhands"
|
|
323
|
+
and profile.tools is None
|
|
324
|
+
and is_tool_usable(BROWSER_TOOL_NAME)
|
|
325
|
+
):
|
|
326
|
+
agent = agent.model_copy(
|
|
327
|
+
update={"tools": [*agent.tools, Tool(name=BROWSER_TOOL_NAME)]}
|
|
328
|
+
)
|
|
315
329
|
launched = LaunchedAgentProfile(
|
|
316
330
|
agent_profile_id=profile.id,
|
|
317
331
|
revision=profile.revision,
|
|
@@ -461,6 +475,7 @@ class ConversationService:
|
|
|
461
475
|
webhook_specs: list[WebhookSpec] = field(default_factory=list)
|
|
462
476
|
session_api_key: str | None = field(default=None)
|
|
463
477
|
cipher: Cipher | None = None
|
|
478
|
+
mcp_tool_provider: MCPToolProvider | None = None
|
|
464
479
|
owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
|
|
465
480
|
max_concurrent_runs: int = 10
|
|
466
481
|
lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
|
|
@@ -1256,6 +1271,9 @@ class ConversationService:
|
|
|
1256
1271
|
def get_instance(cls, config: Config) -> "ConversationService":
|
|
1257
1272
|
# Initialise the settings-store singleton with the server cipher before
|
|
1258
1273
|
# any conversation handler can call get_settings_store() without config.
|
|
1274
|
+
from openhands.agent_server.mcp_oauth_store import (
|
|
1275
|
+
create_settings_backed_mcp_tool_provider,
|
|
1276
|
+
)
|
|
1259
1277
|
from openhands.agent_server.persistence import get_settings_store
|
|
1260
1278
|
|
|
1261
1279
|
get_settings_store(config)
|
|
@@ -1266,6 +1284,7 @@ class ConversationService:
|
|
|
1266
1284
|
config.session_api_keys[0] if config.session_api_keys else None
|
|
1267
1285
|
),
|
|
1268
1286
|
cipher=config.cipher,
|
|
1287
|
+
mcp_tool_provider=create_settings_backed_mcp_tool_provider(config),
|
|
1269
1288
|
max_concurrent_runs=config.max_concurrent_runs,
|
|
1270
1289
|
lease_ttl_seconds=config.lease_ttl_seconds,
|
|
1271
1290
|
)
|
|
@@ -1279,6 +1298,7 @@ class ConversationService:
|
|
|
1279
1298
|
stored=stored,
|
|
1280
1299
|
conversations_dir=self.conversations_dir,
|
|
1281
1300
|
cipher=self.cipher,
|
|
1301
|
+
mcp_tool_provider=self.mcp_tool_provider,
|
|
1282
1302
|
owner_instance_id=self.owner_instance_id,
|
|
1283
1303
|
lease_ttl_seconds=self.lease_ttl_seconds,
|
|
1284
1304
|
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Compatibility helpers for event transport payloads."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Final
|
|
4
|
+
|
|
5
|
+
from openhands.sdk.event import Event
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_LEGACY_TOOL_KINDS: Final[set[str]] = {
|
|
9
|
+
"ClientTool",
|
|
10
|
+
"FileEditorTool",
|
|
11
|
+
"FinishTool",
|
|
12
|
+
"InvokeSkillTool",
|
|
13
|
+
"MCPToolDefinition",
|
|
14
|
+
"SwitchLLMTool",
|
|
15
|
+
"TaskTool",
|
|
16
|
+
"TaskToolSet",
|
|
17
|
+
"TaskTrackerTool",
|
|
18
|
+
"TerminalTool",
|
|
19
|
+
"ThinkTool",
|
|
20
|
+
"WorkflowTool",
|
|
21
|
+
"WorkflowToolSet",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def event_transport_dump(event: Event | dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
"""Serialize an event for HTTP/WebSocket clients across SDK versions.
|
|
27
|
+
|
|
28
|
+
Some deployed clients run older SDKs than the agent-server. They parse event
|
|
29
|
+
payloads with strict pydantic unions, so additive fields and new tool kinds
|
|
30
|
+
can make otherwise completed conversations look failed client-side. Keep
|
|
31
|
+
persistence untouched and apply this compatibility envelope only at the API
|
|
32
|
+
transport boundary.
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(event, dict):
|
|
35
|
+
dumped = dict(event)
|
|
36
|
+
else:
|
|
37
|
+
dumped = event.model_dump(mode="json", exclude_none=True)
|
|
38
|
+
dumped.pop("parent_id", None)
|
|
39
|
+
|
|
40
|
+
tools = dumped.get("tools")
|
|
41
|
+
if isinstance(tools, list):
|
|
42
|
+
dumped["tools"] = [
|
|
43
|
+
tool
|
|
44
|
+
for tool in tools
|
|
45
|
+
if not isinstance(tool, dict)
|
|
46
|
+
or tool.get("kind") is None
|
|
47
|
+
or tool.get("kind") in _LEGACY_TOOL_KINDS
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
return dumped
|
|
@@ -4,7 +4,7 @@ Local Event router for OpenHands SDK.
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from typing import Annotated
|
|
7
|
+
from typing import Annotated, Any, cast
|
|
8
8
|
|
|
9
9
|
from fastapi import (
|
|
10
10
|
APIRouter,
|
|
@@ -13,12 +13,13 @@ from fastapi import (
|
|
|
13
13
|
Query,
|
|
14
14
|
status,
|
|
15
15
|
)
|
|
16
|
+
from starlette.responses import JSONResponse
|
|
16
17
|
|
|
17
18
|
from openhands.agent_server.dependencies import get_event_service
|
|
19
|
+
from openhands.agent_server.event_compat import event_transport_dump
|
|
18
20
|
from openhands.agent_server.event_service import EventService
|
|
19
21
|
from openhands.agent_server.models import (
|
|
20
22
|
ConfirmationResponseRequest,
|
|
21
|
-
EventPage,
|
|
22
23
|
EventSortOrder,
|
|
23
24
|
SendMessageRequest,
|
|
24
25
|
Success,
|
|
@@ -99,7 +100,7 @@ async def search_conversation_events(
|
|
|
99
100
|
Query(title="Filter: event timestamp < this datetime"),
|
|
100
101
|
] = None,
|
|
101
102
|
event_service: EventService = Depends(get_event_service),
|
|
102
|
-
) ->
|
|
103
|
+
) -> JSONResponse:
|
|
103
104
|
"""Search / List local events"""
|
|
104
105
|
assert limit > 0
|
|
105
106
|
assert limit <= 100
|
|
@@ -114,9 +115,21 @@ async def search_conversation_events(
|
|
|
114
115
|
normalize_datetime_to_server_timezone(timestamp__lt) if timestamp__lt else None
|
|
115
116
|
)
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
page = await event_service.search_events(
|
|
118
119
|
page_id, limit, kind, source, body, sort_order, normalized_gte, normalized_lt
|
|
119
120
|
)
|
|
121
|
+
if isinstance(page, dict):
|
|
122
|
+
items = cast(list[Any], page.get("items", []))
|
|
123
|
+
next_page_id = cast(str | None, page.get("next_page_id"))
|
|
124
|
+
else:
|
|
125
|
+
items = page.items
|
|
126
|
+
next_page_id = page.next_page_id
|
|
127
|
+
return JSONResponse(
|
|
128
|
+
{
|
|
129
|
+
"items": [event_transport_dump(event) for event in items],
|
|
130
|
+
"next_page_id": next_page_id,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
@event_router.get("/count", responses={404: {"description": "Conversation not found"}})
|
|
@@ -55,6 +55,7 @@ from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
|
|
|
55
55
|
from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
|
|
56
56
|
from openhands.sdk.git.utils import run_git_command, validate_git_repository
|
|
57
57
|
from openhands.sdk.llm.streaming import LLMStreamChunk
|
|
58
|
+
from openhands.sdk.mcp.utils import MCPToolProvider
|
|
58
59
|
from openhands.sdk.security.analyzer import SecurityAnalyzerBase
|
|
59
60
|
from openhands.sdk.security.confirmation_policy import ConfirmationPolicyBase
|
|
60
61
|
from openhands.sdk.utils.async_utils import AsyncCallbackWrapper
|
|
@@ -81,6 +82,7 @@ class EventService:
|
|
|
81
82
|
stored: StoredConversation
|
|
82
83
|
conversations_dir: Path
|
|
83
84
|
cipher: Cipher | None = None
|
|
85
|
+
mcp_tool_provider: MCPToolProvider | None = None
|
|
84
86
|
owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
|
|
85
87
|
lease_ttl_seconds: float = DEFAULT_LEASE_TTL_SECONDS
|
|
86
88
|
_conversation: LocalConversation | None = field(default=None, init=False)
|
|
@@ -579,21 +581,26 @@ class EventService:
|
|
|
579
581
|
# conversation's synchronous FIFOLock cannot block the server event loop.
|
|
580
582
|
if self._conversation:
|
|
581
583
|
state_update_event = await self._create_state_update_event()
|
|
584
|
+
else:
|
|
585
|
+
state_update_event = ConversationStateUpdateEvent(
|
|
586
|
+
key="execution_status",
|
|
587
|
+
value=ConversationExecutionStatus.IDLE,
|
|
588
|
+
)
|
|
582
589
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
590
|
+
try:
|
|
591
|
+
await asyncio.wait_for(
|
|
592
|
+
subscriber(state_update_event),
|
|
593
|
+
timeout=INITIAL_STATE_PUSH_TIMEOUT_SECONDS,
|
|
594
|
+
)
|
|
595
|
+
except TimeoutError:
|
|
596
|
+
# Subscriber stays registered; only the initial-state push is
|
|
597
|
+
# dropped. Subsequent publishes go through pub_sub and may
|
|
598
|
+
# still block there if the subscriber remains wedged.
|
|
599
|
+
logger.warning(
|
|
600
|
+
f"Initial state push to subscriber {subscriber_id} timed "
|
|
601
|
+
f"out after {INITIAL_STATE_PUSH_TIMEOUT_SECONDS}s."
|
|
602
|
+
)
|
|
603
|
+
# Non-timeout errors propagate to caller (e.g. webhook failures).
|
|
597
604
|
|
|
598
605
|
return subscriber_id
|
|
599
606
|
|
|
@@ -832,6 +839,7 @@ class EventService:
|
|
|
832
839
|
observability_metadata=self.stored.observability_metadata,
|
|
833
840
|
observability_tags=self.stored.observability_tags,
|
|
834
841
|
observability_span_name=self.stored.observability_span_name,
|
|
842
|
+
mcp_tool_provider=self.mcp_tool_provider,
|
|
835
843
|
)
|
|
836
844
|
|
|
837
845
|
conversation.set_confirmation_policy(self.stored.confirmation_policy)
|