| import { |
| ServerClient, |
| SettingsClient, |
| } from "@openhands/typescript-client/clients"; |
| import type { ServerInfo as BaseServerInfo } from "@openhands/typescript-client"; |
| import { getAgentServerClientOptions } from "#/api/agent-server-client-options"; |
| import { isAuthRequired } from "#/api/agent-server-config"; |
| import { |
| getActiveBackend, |
| getEffectiveLocalBackend, |
| isNoBackend, |
| } from "#/api/backend-registry/active-store"; |
| import type { Backend } from "#/api/backend-registry/types"; |
| import defaults from "../../config/defaults.json"; |
|
|
| const AGENT_SERVER_INFO_TIMEOUT_MS = 5000; |
| const UNKNOWN_AGENT_SERVER_VERSION = "unknown"; |
|
|
| export const MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION = |
| defaults.compatibility.minimumAgentServer; |
| export const AGENT_SERVER_UNSUPPORTED_VERSION_ERROR_CODE = |
| "AGENT_SERVER_UNSUPPORTED_VERSION"; |
| export const AGENT_SERVER_UNKNOWN_VERSION_ERROR_CODE = |
| "AGENT_SERVER_UNKNOWN_VERSION"; |
|
|
| |
| |
| |
| |
| |
| |
| export const INVALID_BACKEND_API_KEY_ERROR = "Invalid API key"; |
|
|
| export interface AgentServerInfo extends BaseServerInfo { |
| sdk_version?: string; |
| usable_tools?: string[] | null; |
| runtime_services?: unknown; |
| } |
|
|
| let cachedAgentServerInfo: AgentServerInfo | null = null; |
| let cachedAgentServerInfoHost: string | null = null; |
|
|
| const getAdvertisedTools = (serverInfo: AgentServerInfo | null) => { |
| if (Array.isArray(serverInfo?.usable_tools)) { |
| return serverInfo.usable_tools; |
| } |
| return null; |
| }; |
|
|
| export class AgentServerUnavailableError extends Error { |
| readonly details: string | null; |
| readonly noBackendConfigured: boolean; |
|
|
| constructor( |
| details?: string | null, |
| options?: { noBackendConfigured?: boolean }, |
| ) { |
| const noBackendConfigured = options?.noBackendConfigured ?? false; |
| super( |
| noBackendConfigured |
| ? "No agent server backend is configured yet. Add a backend to get started." |
| : "Could not connect to the configured agent server. Make sure it is running and reachable, then reload the page.", |
| ); |
| this.name = "AgentServerUnavailableError"; |
| this.details = details ?? null; |
| this.noBackendConfigured = noBackendConfigured; |
| } |
| } |
|
|
| export const isAgentServerUnavailableError = ( |
| error: unknown, |
| ): error is AgentServerUnavailableError => |
| error instanceof AgentServerUnavailableError || |
| (typeof error === "object" && |
| error !== null && |
| "name" in error && |
| (error.name === "AgentServerUnavailableError" || |
| error.name === "AgentServerUnsupportedVersionError" || |
| error.name === "AgentServerUnknownVersionError")); |
|
|
| export class AgentServerUnsupportedVersionError extends AgentServerUnavailableError { |
| readonly code = AGENT_SERVER_UNSUPPORTED_VERSION_ERROR_CODE; |
| readonly actualVersion: string; |
| readonly requiredVersion = MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION; |
|
|
| constructor(actualVersion: string) { |
| const message = `Agent Canvas requires agent-server ${MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION} or newer; this backend is running ${actualVersion}. Please upgrade the agent-server backend.`; |
| super(message); |
| this.name = "AgentServerUnsupportedVersionError"; |
| this.message = message; |
| this.actualVersion = actualVersion; |
| } |
| } |
|
|
| export class AgentServerUnknownVersionError extends AgentServerUnavailableError { |
| readonly code = AGENT_SERVER_UNKNOWN_VERSION_ERROR_CODE; |
| readonly actualVersion: string | null; |
| readonly requiredVersion = MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION; |
|
|
| constructor(actualVersion: string | null) { |
| const reported = actualVersion ? ` It reported "${actualVersion}".` : ""; |
| const message = |
| `Could not determine this backend's agent-server version.${reported} ` + |
| `Agent Canvas requires agent-server ${MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION} ` + |
| "or newer, but this backend did not return a valid version from " + |
| "/server_info. Restart or rebuild the agent-server backend, then try again."; |
| super(message); |
| this.name = "AgentServerUnknownVersionError"; |
| this.message = message; |
| this.actualVersion = actualVersion; |
| } |
| } |
|
|
| export const isAgentServerUnsupportedVersionError = ( |
| error: unknown, |
| ): error is AgentServerUnsupportedVersionError => |
| error instanceof AgentServerUnsupportedVersionError || |
| (typeof error === "object" && |
| error !== null && |
| "code" in error && |
| error.code === AGENT_SERVER_UNSUPPORTED_VERSION_ERROR_CODE); |
|
|
| export const isAgentServerUnknownVersionError = ( |
| error: unknown, |
| ): error is AgentServerUnknownVersionError => |
| error instanceof AgentServerUnknownVersionError || |
| (typeof error === "object" && |
| error !== null && |
| "code" in error && |
| error.code === AGENT_SERVER_UNKNOWN_VERSION_ERROR_CODE); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const isAgentServerAuthError = (error: unknown): boolean => |
| isAuthRequired() && isSdkHttpStatusError(error, 401); |
|
|
| export function clearCachedAgentServerInfo() { |
| cachedAgentServerInfo = null; |
| cachedAgentServerInfoHost = null; |
| } |
|
|
| export function getCachedAgentServerInfo(options?: { |
| host?: string | null; |
| }): AgentServerInfo | null { |
| if (options?.host && options.host !== cachedAgentServerInfoHost) { |
| return null; |
| } |
| return cachedAgentServerInfo; |
| } |
|
|
| export function isAgentServerToolAvailable(toolName: string) { |
| const availableTools = getAdvertisedTools(cachedAgentServerInfo); |
| if (!Array.isArray(availableTools)) { |
| return true; |
| } |
| return availableTools.includes(toolName); |
| } |
|
|
| export function isSdkHttpError(error: unknown) { |
| return ( |
| error instanceof Error && |
| error.name === "HttpError" && |
| "status" in error && |
| typeof error.status === "number" |
| ); |
| } |
|
|
| |
| |
| |
| |
| export function isSdkHttpStatusError(error: unknown, status: number): boolean { |
| return ( |
| isSdkHttpError(error) && (error as { status: number }).status === status |
| ); |
| } |
|
|
| function normalizeAgentServerInfoVersion(version: unknown): string | null { |
| if (typeof version !== "string") return null; |
| const trimmed = version.trim(); |
| return trimmed || null; |
| } |
|
|
| function getRawAgentServerSdkVersion( |
| serverInfo: AgentServerInfo, |
| ): string | null { |
| return normalizeAgentServerInfoVersion(serverInfo.sdk_version); |
| } |
|
|
| function getRawAgentServerVersion(serverInfo: AgentServerInfo): string | null { |
| return ( |
| normalizeAgentServerInfoVersion(serverInfo.version) ?? |
| getRawAgentServerSdkVersion(serverInfo) |
| ); |
| } |
|
|
| function getComparableAgentServerVersion( |
| serverInfo: AgentServerInfo, |
| ): string | null { |
| const version = getRawAgentServerVersion(serverInfo); |
| if (!version || version.toLowerCase() === UNKNOWN_AGENT_SERVER_VERSION) { |
| return null; |
| } |
| return version; |
| } |
|
|
| export function getDisplayAgentServerVersion( |
| serverInfo: AgentServerInfo, |
| ): string | null { |
| const version = getComparableAgentServerVersion(serverInfo); |
| if (!version || !parseAgentServerVersion(version)) { |
| return null; |
| } |
| return version; |
| } |
|
|
| export function getDisplayAgentServerSdkVersion( |
| serverInfo: AgentServerInfo, |
| ): string | null { |
| const version = |
| getRawAgentServerSdkVersion(serverInfo) ?? |
| getComparableAgentServerVersion(serverInfo); |
| if (!version || version.toLowerCase() === UNKNOWN_AGENT_SERVER_VERSION) { |
| return null; |
| } |
| return version; |
| } |
|
|
| export function getCachedAgentServerSdkVersion( |
| host?: string | null, |
| ): string | null { |
| if (!cachedAgentServerInfo) return null; |
| if (host && getEffectiveLocalBackend()?.host !== host) return null; |
| return getDisplayAgentServerSdkVersion(cachedAgentServerInfo); |
| } |
|
|
| export function getCachedAgentServerVersion( |
| host?: string | null, |
| ): string | null { |
| if (!cachedAgentServerInfo) return null; |
| if (host && getEffectiveLocalBackend()?.host !== host) return null; |
| return getDisplayAgentServerVersion(cachedAgentServerInfo); |
| } |
|
|
| export function compareAgentServerVersions(actual: string, required: string) { |
| const parsedActual = parseAgentServerVersion(actual); |
| const parsedRequired = parseAgentServerVersion(required); |
|
|
| if (!parsedActual || !parsedRequired) { |
| return null; |
| } |
|
|
| for (const key of ["major", "minor", "patch"] as const) { |
| if (parsedActual[key] > parsedRequired[key]) { |
| return 1; |
| } |
| if (parsedActual[key] < parsedRequired[key]) { |
| return -1; |
| } |
| } |
|
|
| if (parsedActual.prerelease && !parsedRequired.prerelease) { |
| return -1; |
| } |
| if (!parsedActual.prerelease && parsedRequired.prerelease) { |
| return 1; |
| } |
| if (parsedActual.prerelease && parsedRequired.prerelease) { |
| return parsedActual.prerelease.localeCompare(parsedRequired.prerelease); |
| } |
|
|
| return 0; |
| } |
|
|
| function parseAgentServerVersion(version: string) { |
| const trimmed = version.trim().replace(/^v/, ""); |
| const [withoutBuild] = trimmed.split("+"); |
| const [core, prerelease] = withoutBuild.split("-", 2); |
| const parts = core.split("."); |
|
|
| if (parts.length !== 3) { |
| return null; |
| } |
|
|
| const [major, minor, patch] = parts.map((part) => Number(part)); |
| if ( |
| ![major, minor, patch].every((part) => Number.isInteger(part) && part >= 0) |
| ) { |
| return null; |
| } |
|
|
| return { major, minor, patch, prerelease }; |
| } |
|
|
| export function assertAgentServerVersionIsSupported( |
| serverInfo: AgentServerInfo, |
| ) { |
| const actualVersion = getComparableAgentServerVersion(serverInfo); |
| if (!actualVersion) { |
| clearCachedAgentServerInfo(); |
| throw new AgentServerUnknownVersionError( |
| getRawAgentServerVersion(serverInfo), |
| ); |
| } |
|
|
| const comparison = compareAgentServerVersions( |
| actualVersion, |
| MINIMUM_COMPATIBLE_AGENT_SERVER_VERSION, |
| ); |
|
|
| if (comparison === null) { |
| clearCachedAgentServerInfo(); |
| throw new AgentServerUnknownVersionError(actualVersion); |
| } |
|
|
| if (comparison < 0) { |
| clearCachedAgentServerInfo(); |
| throw new AgentServerUnsupportedVersionError(actualVersion); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function validateLocalBackend( |
| backend: Pick<Backend, "host" | "apiKey">, |
| timeout: number, |
| ): Promise<string | null> { |
| const clientOptions = getAgentServerClientOptions({ |
| host: backend.host, |
| sessionApiKey: backend.apiKey || null, |
| timeout, |
| }); |
|
|
| try { |
| await new SettingsClient(clientOptions).getSettings(); |
| const serverInfo = await new ServerClient(clientOptions).getServerInfo(); |
| assertAgentServerVersionIsSupported(serverInfo); |
| return getDisplayAgentServerVersion(serverInfo); |
| } catch (error) { |
| if (isSdkHttpStatusError(error, 401)) { |
| throw new Error(INVALID_BACKEND_API_KEY_ERROR); |
| } |
| throw error; |
| } |
| } |
|
|
| export async function loadAgentServerInfo() { |
| |
| |
| |
| |
| const local = getEffectiveLocalBackend(); |
| if (!local) { |
| clearCachedAgentServerInfo(); |
|
|
| |
| |
| |
| if (isNoBackend(getActiveBackend().backend)) { |
| throw new AgentServerUnavailableError("No backend configured", { |
| noBackendConfigured: true, |
| }); |
| } |
|
|
| |
| return null; |
| } |
|
|
| const clientOptions = getAgentServerClientOptions({ |
| host: local.host, |
| sessionApiKey: local.apiKey || null, |
| timeout: AGENT_SERVER_INFO_TIMEOUT_MS, |
| }); |
| let serverInfo: AgentServerInfo; |
|
|
| try { |
| serverInfo = (await new ServerClient( |
| clientOptions, |
| ).getServerInfo()) as AgentServerInfo; |
| } catch (error) { |
| clearCachedAgentServerInfo(); |
| |
| |
| |
| if (isSdkHttpStatusError(error, 401)) { |
| throw error; |
| } |
|
|
| const details = error instanceof Error ? error.message : null; |
| throw new AgentServerUnavailableError(details); |
| } |
|
|
| assertAgentServerVersionIsSupported(serverInfo); |
|
|
| |
| |
| |
| |
| if (isAuthRequired()) { |
| try { |
| await new SettingsClient(clientOptions).getSettings(); |
| } catch (error) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (isSdkHttpStatusError(error, 401)) { |
| throw error; |
| } |
|
|
| console.warn( |
| "[agent-server] getSettings() probe failed (non-401):", |
| error, |
| ); |
| } |
| } |
|
|
| cachedAgentServerInfo = serverInfo; |
| cachedAgentServerInfoHost = clientOptions.host; |
| return serverInfo; |
| } |
|
|