Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clients/typescript/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ await conversation.close();

**Factory Functions**:

**Ergonomic API note**: Keep `ConversationManager` as the main server-scoped entry point. Server/LLM/settings/skills/tools/VSCode/desktop operations should be reachable through manager namespaces such as `manager.server`, `manager.llm`, and `manager.desktop`; ACP-specific operations should be reachable via `manager.acp`.
**Ergonomic API note**: Keep `ConversationManager` as the main server-scoped entry point. Server/LLM/settings/skills/tools/VSCode operations should be reachable through manager namespaces such as `manager.server`, `manager.llm`, and `manager.vscode`; ACP-specific operations should be reachable via `manager.acp`.

- `createConversation({ type, agent, workspace, options })` - Explicit type selection
- `createConversationAuto(agent, workspace, options)` - Auto-detect based on workspace type
Expand Down
4 changes: 2 additions & 2 deletions clients/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clients/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openhands/typescript-client",
"version": "1.48.0",
"version": "1.49.0",
"description": "TypeScript client for OpenHands Agent Server",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,6 @@ describe('Deterministic API Integration Tests', () => {
expect(Array.isArray(subAgents.agents)).toBe(true);
expect(subAgents.agents.every((agent) => agent.is_builtin)).toBe(true);
expect(typeof vscodeStatus.enabled).toBe('boolean');

try {
const desktopUrl = await manager.desktop.getUrl();
expect(desktopUrl === null || typeof desktopUrl === 'string').toBe(true);
} catch (error) {
expect(error).toBeInstanceOf(HttpError);
expect((error as HttpError).status).toBe(503);
}
},
config.testTimeout
);
Expand Down
30 changes: 0 additions & 30 deletions clients/typescript/src/client/desktop-client.ts

This file was deleted.

3 changes: 0 additions & 3 deletions clients/typescript/src/client/openhands-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { AgentProfilesClient } from './agent-profiles-client';
import { BashClient } from './bash-client';
import { ConversationClient } from './conversation-client';
import { DesktopClient } from './desktop-client';
import { FileClient } from './file-client';
import { HooksClient } from './hooks-client';
import { HttpClient, type ResponseType } from './http-client';
Expand Down Expand Up @@ -136,7 +135,6 @@ export class AgentServerClient extends OpenHandsClient {
readonly plugins: PluginsClient;
readonly tools: ToolClient;
readonly vscode: VSCodeClient;
readonly desktop: DesktopClient;
readonly shared: SharedClient;
readonly llm: LLMMetadataClient;
readonly workspaces: WorkspacesClient;
Expand Down Expand Up @@ -171,7 +169,6 @@ export class AgentServerClient extends OpenHandsClient {
this.plugins = new PluginsClient(clientOptions);
this.tools = new ToolClient(clientOptions);
this.vscode = new VSCodeClient(clientOptions);
this.desktop = new DesktopClient(clientOptions);
this.shared = new SharedClient(clientOptions);
this.llm = new LLMMetadataClient(clientOptions);
this.workspaces = new WorkspacesClient(clientOptions);
Expand Down
2 changes: 0 additions & 2 deletions clients/typescript/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export { SubAgentsClient } from './client/sub-agents-client';
export { PluginsClient } from './client/plugins-client';
export { ToolClient } from './client/tool-client';
export { VSCodeClient } from './client/vscode-client';
export { DesktopClient } from './client/desktop-client';
export { SharedClient } from './client/shared-client';
export { WorkspacesClient } from './client/workspaces-client';
export { AgentServerClient, OpenHandsClient } from './client/openhands-client';
Expand Down Expand Up @@ -71,7 +70,6 @@ export type { SubAgentsClientOptions } from './client/sub-agents-client';
export type { PluginsClientOptions } from './client/plugins-client';
export type { ToolClientOptions } from './client/tool-client';
export type { VSCodeClientOptions, GetVSCodeUrlOptions } from './client/vscode-client';
export type { DesktopClientOptions } from './client/desktop-client';
export type { SharedClientOptions, SharedEventSearchOptions } from './client/shared-client';
export type {
DeleteWorkspaceResponse,
Expand Down
4 changes: 0 additions & 4 deletions clients/typescript/src/conversation/conversation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import { HttpClient } from '../client/http-client';
import { AgentProfilesClient } from '../client/agent-profiles-client';
import { DesktopClient } from '../client/desktop-client';
import { FileClient } from '../client/file-client';
import { HooksClient } from '../client/hooks-client';
import { LLMMetadataClient } from '../client/llm-client';
Expand Down Expand Up @@ -90,7 +89,6 @@ export class ConversationManager {
public readonly subAgents: SubAgentsClient;
public readonly tools: ToolClient;
public readonly vscode: VSCodeClient;
public readonly desktop: DesktopClient;
public readonly files: FileClient;
public readonly workspaces: WorkspacesClient;
public readonly shared: SharedClient;
Expand Down Expand Up @@ -123,7 +121,6 @@ export class ConversationManager {
this.subAgents = new SubAgentsClient(clientOptions);
this.tools = new ToolClient(clientOptions);
this.vscode = new VSCodeClient(clientOptions);
this.desktop = new DesktopClient(clientOptions);
this.files = new FileClient(clientOptions);
this.workspaces = new WorkspacesClient(clientOptions);
this.shared = new SharedClient(clientOptions);
Expand Down Expand Up @@ -408,7 +405,6 @@ export class ConversationManager {
this.subAgents.close();
this.tools.close();
this.vscode.close();
this.desktop.close();
this.files.close();
this.workspaces.close();
this.shared.close();
Expand Down
1 change: 0 additions & 1 deletion clients/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ export type {
TogglePluginResponse,
PluginActionResponse,
RefreshPluginResponse,
DesktopUrlResponse,
VSCodeUrlResponse,
VSCodeStatusResponse,
ProfileInfo,
Expand Down
4 changes: 0 additions & 4 deletions clients/typescript/src/models/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,6 @@ export interface RefreshPluginResponse {
plugin: InstalledPluginInfo;
}

export interface DesktopUrlResponse {
url: string | null;
}

export interface VSCodeUrlResponse {
url: string | null;
}
Expand Down
42 changes: 26 additions & 16 deletions openhands-agent-server/openhands/agent_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
Config,
get_default_config,
)
from openhands.agent_server.conversation_router import conversation_router
from openhands.agent_server.conversation_registry import (
create_conversation_registry,
)
from openhands.agent_server.conversation_router import (
conversation_catalog_router,
conversation_router,
)
from openhands.agent_server.conversation_service import (
CredentialBindingActivationRequired,
get_default_conversation_service,
Expand All @@ -38,8 +44,6 @@
check_session_api_key,
check_workspace_session,
)
from openhands.agent_server.desktop_router import desktop_router
from openhands.agent_server.event_router import event_router
from openhands.agent_server.file_router import file_discovery_router, file_router
from openhands.agent_server.git_router import git_router
from openhands.agent_server.hooks_router import hooks_router
Expand All @@ -60,16 +64,13 @@
from openhands.agent_server.provider_connections_router import (
provider_connections_router,
)
from openhands.agent_server.runtime_router import create_runtime_router
from openhands.agent_server.server_details_router import (
get_server_info,
mark_initialization_complete,
server_details_router,
)
from openhands.agent_server.session_socket import session_router
from openhands.agent_server.settings_router import settings_router
from openhands.agent_server.skills_router import skills_router
from openhands.agent_server.sockets import sockets_router
from openhands.agent_server.sub_agents_router import sub_agents_router
from openhands.agent_server.telemetry import (
build_telemetry_sink,
Expand All @@ -92,7 +93,6 @@
from openhands.agent_server.tool_router import tool_router
from openhands.agent_server.vscode_router import vscode_router
from openhands.agent_server.vscode_service import get_vscode_service
from openhands.agent_server.workspace_router import workspace_router
from openhands.agent_server.workspaces_router import workspaces_router
from openhands.sdk.logger import DEBUG, get_logger
from openhands.sdk.utils.redact import sanitize_dict
Expand Down Expand Up @@ -160,6 +160,10 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:

config: Config = api.state.config
deferred = config.deferred_init
conversation_registry = getattr(
api.state, "conversation_registry", None
) or create_conversation_registry(config)
api.state.conversation_registry = conversation_registry

# Deferred pods boot with telemetry disabled and are rebuilt by
# InitService, so they emit `server_started` there instead.
Expand Down Expand Up @@ -254,6 +258,11 @@ async def stop_tool_preload_service():
bash_svc = get_default_bash_event_service()
api.state.bash_event_service = bash_svc

conversation_registry.configure_service(service)
# Runtime cleanup must precede external-catalog recovery so stale
# runtime owners cannot lose their expired leases to the outer service.
await conversation_registry.start()

async with service:
api.state.conversation_service = service

Expand All @@ -273,6 +282,7 @@ async def stop_tool_preload_service():
try:
yield
finally:
await conversation_registry.shutdown()
if retention_task is not None:
retention_task.cancel()
with suppress(asyncio.CancelledError):
Expand Down Expand Up @@ -388,6 +398,7 @@ def _find_http_exception(exc: BaseExceptionGroup) -> HTTPException | None:

def _add_api_routes(app: FastAPI) -> None:
"""Add all API routes to the FastAPI application."""
conversation_registry = app.state.conversation_registry
app.include_router(server_details_router)

# The /api/init endpoint bypasses both the session-key auth and the
Expand All @@ -413,16 +424,17 @@ def _add_api_routes(app: FastAPI) -> None:

api_router = APIRouter(prefix="/api", dependencies=dependencies)
api_router.include_router(file_discovery_router)
api_router.include_router(create_runtime_router())
api_router.include_router(event_router)
# Collection routes must precede runtime catch-alls such as
# ``/conversations/{conversation_id}``.
api_router.include_router(conversation_catalog_router)
conversation_registry.add_execution_routes(api_router)
api_router.include_router(conversation_router)
api_router.include_router(credential_binding_router)
api_router.include_router(tool_router)
api_router.include_router(bash_router)
api_router.include_router(git_router)
api_router.include_router(file_router)
api_router.include_router(vscode_router)
api_router.include_router(desktop_router)
api_router.include_router(skills_router)
api_router.include_router(sub_agents_router)
api_router.include_router(plugins_router)
Expand All @@ -438,8 +450,6 @@ def _add_api_routes(app: FastAPI) -> None:
# /api/auth/* mints workspace cookies and requires the header to bootstrap,
# so it lives under the header-only auth group.
api_router.include_router(auth_router)
app.include_router(api_router)

app.include_router(openai_router, dependencies=[Depends(check_openai_api_key)])

# Workspace static-file routes get their own auth group that accepts
Expand All @@ -450,12 +460,11 @@ def _add_api_routes(app: FastAPI) -> None:
workspace_api_router = APIRouter(
prefix="/api", dependencies=[Depends(check_workspace_session)]
)
workspace_api_router.include_router(workspace_router)
workspace_api_router.include_router(conversation_registry.workspace_router)
app.include_router(workspace_api_router)
app.include_router(api_router)

app.include_router(sockets_router)

app.include_router(session_router)
app.include_router(conversation_registry.sockets_router)


def _setup_static_files(app: FastAPI, config: Config) -> None:
Expand Down Expand Up @@ -683,6 +692,7 @@ def create_app(config: Config | None = None) -> FastAPI:
config = get_default_config()
app = _create_fastapi_instance(config)
app.state.config = config
app.state.conversation_registry = create_conversation_registry(config)

_add_api_routes(app)
_setup_static_files(app, config)
Expand Down
7 changes: 7 additions & 0 deletions openhands-agent-server/openhands/agent_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,13 @@ class Config(BaseModel):
"The URL where this agent server instance is available externally"
),
)
conversation_runtime: Literal["local", "docker"] = "local"
conversation_image: str = "ghcr.io/openhands/agent-server:latest-python"
conversation_container_memory: str | None = "4g"
conversation_container_cpus: float | None = Field(default=2.0, gt=0)
conversation_container_pids_limit: int | None = Field(default=512, gt=0)
conversation_container_startup_timeout: float = Field(default=120, gt=0)

acp_skill_sourcing: ACPSkillSourcing = Field(
default="native",
description=(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Select and manage the configured conversation runtime."""

from __future__ import annotations

from typing import TYPE_CHECKING

from fastapi import APIRouter

from openhands.agent_server.config import Config


if TYPE_CHECKING:
from openhands.agent_server.conversation_service import ConversationService


class ConversationRegistry:
"""Route and lifecycle adapter for host-local conversations."""

def __init__(self, config: Config) -> None:
self.config = config

def configure_service(self, service: ConversationService) -> None:
"""Connect runtime-specific persistence to the shared catalog."""

async def start(self) -> None:
"""Start resources owned by this registry."""

async def shutdown(self) -> None:
"""Stop resources owned by this registry."""

def add_execution_routes(self, router: APIRouter) -> None:
from openhands.agent_server.event_router import event_router
from openhands.agent_server.runtime_router import create_runtime_router

router.include_router(create_runtime_router())
router.include_router(event_router)

@property
def workspace_router(self) -> APIRouter:
from openhands.agent_server.workspace_router import workspace_router

return workspace_router

@property
def conversation_sockets_router(self) -> APIRouter:
from openhands.agent_server.sockets import conversation_sockets_router

return conversation_sockets_router

@property
def session_sockets_router(self) -> APIRouter:
from openhands.agent_server.session_socket import session_router

return session_router

@property
def sockets_router(self) -> APIRouter:
from openhands.agent_server.sockets import bash_sockets_router

router = APIRouter()
router.include_router(self.conversation_sockets_router)
router.include_router(self.session_sockets_router)
router.include_router(bash_sockets_router)
return router


def create_conversation_registry(config: Config) -> ConversationRegistry:
if config.conversation_runtime == "docker":
from openhands.agent_server.docker_runtime.registry import (
DockerConversationRegistry,
)

return DockerConversationRegistry(config)
return ConversationRegistry(config)
Loading
Loading