Download apps/desktop/src/api/client.ts from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 9.21 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/desktop/src/api/client.ts
- Command line
-
hf download hf://SaylorTwift/hermes-agent/apps/desktop/src/api/client.ts
-
curl -L -o client.ts https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/desktop/src/api/client.ts
9.21 kB
| import { JsonRpcGatewayClient } from '@hermes/shared' | |
| import type { HermesApiRequest } from '@/global' | |
| // Desktop startup fires a burst of read-only data calls (config, profiles, | |
| // model info/options, cron) the moment the backend passes readiness. On a | |
| // profile-heavy or remote install these can each take tens of seconds β e.g. | |
| // /api/profiles runs list_profiles(), which does a recursive skill-tree walk | |
| // per profile β so the 15s default (DEFAULT_FETCH_TIMEOUT_MS in hardening.ts) | |
| // times out a backend that is alive-but-busy, surfacing as a spurious | |
| // "Timed out connecting to Hermes backend" that hangs the UI (#48504). | |
| // | |
| // Give the boot burst a generous per-call timeout instead of raising the | |
| // global default: interactive/runtime calls and the liveness poll (/api/status) | |
| // keep the short default so a genuinely-dead backend is still detected fast. | |
| export const STARTUP_REQUEST_TIMEOUT_MS = 60_000 | |
| const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000 | |
| // prompt.submit is effectively fire-and-forget: turn completion is signaled by | |
| // stream / message.complete events, NOT by the RPC return. A long turn (MoA | |
| // presets running references + aggregator in series, deep reasoning, large tool | |
| // chains) can legitimately take minutes to ACK, so bounding the ack by the | |
| // generic 30s default surfaces a false "request timed out" toast while the turn | |
| // is still running and will succeed (issue #55024). Match the backend's | |
| // agent-turn ceiling (agent.gateway_timeout = 1800s) so the ack timeout only | |
| // ever fires when the turn itself would have been abandoned server-side. | |
| export const PROMPT_SUBMIT_REQUEST_TIMEOUT_MS = 1_800_000 | |
| export class HermesGateway extends JsonRpcGatewayClient { | |
| constructor() { | |
| super({ | |
| closedErrorMessage: 'Hermes gateway connection closed', | |
| connectErrorMessage: 'Could not connect to Hermes gateway', | |
| createRequestId: nextId => nextId, | |
| notConnectedErrorMessage: 'Hermes gateway is not connected', | |
| requestTimeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS | |
| }) | |
| } | |
| } | |
| // Profile that profile-scoped REST settings (config/env/skills/tools/model/β¦) | |
| // should target. Mirrors $activeGatewayProfile, pushed in from the store via | |
| // setApiRequestProfile so this module needs no store import (avoids a cycle). | |
| // Electron main consumes request.profile as request scope. Local calls whose | |
| // REST handlers accept profile reuse the primary dashboard via ?profile=; | |
| // unscoped handlers retain a profile backend. Remote overrides still route to | |
| // their owning backend. Null β primary, so single-profile users are unaffected. | |
| let _apiProfile: null | string = null | |
| export function setApiRequestProfile(profile: null | string): void { | |
| _apiProfile = profile || null | |
| } | |
| export function profileScoped(profile?: null | string): { profile?: string } { | |
| const selected = profile === undefined ? _apiProfile : profile | |
| return selected ? { profile: selected } : {} | |
| } | |
| /** Profile that profile-scoped REST/WS calls should target (null β primary). | |
| * Read-only twin of setApiRequestProfile for modules (e.g. voice playback) | |
| * that build their own connection URLs and must stay on the same backend. */ | |
| export function getApiRequestProfile(): null | string { | |
| return _apiProfile | |
| } | |
| // Registry connection serving the active gateway (null β the local pool). | |
| // Pushed from store/gateway's setActive β the single seam BOTH | |
| // ensureGatewayProfile and ensureGatewayAgent funnel through β so WS calls | |
| // that dial their own backend (pluginSocket) resolve it through the SAME | |
| // source of truth those paths maintain for $connection. That makes the plugin | |
| // socket follow registry-agent activations too, not just profile switches. | |
| // Same no-store-import contract as _apiProfile (avoids a cycle). | |
| let _apiConnectionId: null | string = null | |
| export function setApiRequestConnection(connectionId: null | string): void { | |
| _apiConnectionId = connectionId || null | |
| } | |
| // Registry connection scope for a REST request. A registered remote gateway | |
| // owns its own state.db β cron jobs and their run sessions live THERE β so | |
| // requests for gateway-owned data must carry the connection id for the main | |
| // process to route them to that host (hermes:api's registry branch). Null | |
| // resolves to no tag, keeping single-source users byte-identical; explicit | |
| // 'local' must remain tagged when the legacy primary points elsewhere. | |
| export function connectionScoped(): { connectionId?: string } { | |
| return _apiConnectionId ? { connectionId: _apiConnectionId } : {} | |
| } | |
| // Whether the window's primary connection is the local pool. Pushed from | |
| // store/session's setConnection (same no-store-import contract as _apiProfile) | |
| // so api/ helpers can name the backend an UNTAGGED request lands on without | |
| // importing the heavy session store β which would close a module cycle | |
| // through @/hermes. | |
| let _apiLocalMode = false | |
| export function setApiRequestLocalMode(local: boolean): void { | |
| _apiLocalMode = local | |
| } | |
| /** The connection an ambient (untagged) request is served by: the registry | |
| * tag when one is active, else `'local'` for the local pool. Identity only β | |
| * never send this as a request pin (an explicit `'local'` bypasses Electron's | |
| * legacy per-profile remote overrides). */ | |
| export function ambientOwnerConnectionId(): string | undefined { | |
| return _apiConnectionId ?? (_apiLocalMode ? 'local' : undefined) | |
| } | |
| /** Send a REST request to the renderer's active registry source. Request-level | |
| * routing may override the active source for an explicitly-owned resource. | |
| * | |
| * Helpers under `api/` go through here rather than calling the preload bridge | |
| * directly, so the connection tag cannot be forgotten on a new one. | |
| * capabilityScoped() now emits an explicit `connectionId` for EVERY object | |
| * pin β `'local'` included β so a pin always overrides the ambient tag spread | |
| * underneath it. (It used to omit the key for 'local', which made the pin | |
| * unable to beat the ambient tag; helpers then had to bypass this wrapper.) */ | |
| export function hermesApi<T>(request: HermesApiRequest): Promise<T> { | |
| return window.hermesDesktop.api<T>({ ...connectionScoped(), ...request }) | |
| } | |
| // ββ Capability scope: (connection, profile) routing for the Capabilities | |
| // surface (skills / toolsets / MCP / hub / env / toolset config) ββββββββββββ | |
| // | |
| // A profile is not a machine-global name β it belongs to ONE gateway. The | |
| // Capabilities surface can be pointed at any (connection, profile) pair | |
| // (SkillsView's scope selector, Bot Mode's fixedProfile/fixedConnection), so | |
| // its REST helpers accept either the legacy string form or an explicit scope | |
| // object: | |
| // | |
| // - `undefined` / string β the legacy profile path, PLUS the active registry | |
| // connection tag (connectionScoped, same contract the cron helpers adopted | |
| // in #87882). Without the tag, a window activated onto a registered remote | |
| // gateway read the LOCAL pool's skills/tools/MCP β the wrong machine. | |
| // - `{ connectionId, profile }` β explicit pin. A non-empty connection id β | |
| // `'local'` INCLUDED β is sent through so Electron's registry resolver | |
| // owns the routing. Dropping the `'local'` pin (the pre-#91564 behavior, | |
| // when an absent id always meant the local pool) silently re-routes a | |
| // "This device" pick to the registry PRIMARY once that primary is a | |
| // remote/cloud/ssh gateway: the v1 fallback route treats a remote registry | |
| // primary as global-remote, so the explicit pin is the ONLY way back to | |
| // this machine (see apiRequestRegistryConnectionId in Electron main). | |
| export type ProfileScope = undefined | null | string | { connectionId?: null | string; profile?: null | string } | |
| export function capabilityScoped(scope?: ProfileScope): { connectionId?: string; profile?: string } { | |
| if (scope && typeof scope === 'object') { | |
| const profile = (scope.profile ?? '').trim() | |
| const connectionId = (scope.connectionId ?? '').trim() | |
| return { | |
| ...(profile ? { profile } : {}), | |
| ...(connectionId ? { connectionId } : {}) | |
| } | |
| } | |
| return { ...profileScoped(scope), ...connectionScoped() } | |
| } | |
| /** Stable cache-key for a capability scope: `profile` for the ambient/legacy | |
| * path, `connectionId::profile` for ANY explicit pin β `local` included. An | |
| * explicit "This device" pick and the ambient path are no longer guaranteed | |
| * to hit the same backend (a remote registry PRIMARY makes the ambient path | |
| * remote), so sharing the bare-profile cache row between them painted one | |
| * machine's config under the other's scope (AGENTS.md scope-in-key rule). */ | |
| export function profileScopeKey(scope?: ProfileScope): string { | |
| if (scope && typeof scope === 'object') { | |
| const profile = (scope.profile ?? '').trim() || 'default' | |
| const connectionId = (scope.connectionId ?? '').trim() | |
| return connectionId ? `${connectionId}::${profile}` : profile | |
| } | |
| return (scope ?? '').trim() || 'default' | |
| } | |
| /** Registry connection id that connection-scoped WS calls should target | |
| * (null β the local pool). Read-only twin of setApiRequestConnection. */ | |
| export function getApiRequestConnection(): null | string { | |
| return _apiConnectionId | |
| } | |