hermes-hub/docs/UI_STATE_CONTRACT.md

28 KiB
Raw Blame History

Hermes Hub UI state contract

  • Contract version: 1.2
  • Published against: 9ffc815ed59f7fd364e176ffef20bf29d545bc46
  • Contract owner: antigravity/release-readiness
  • Consumer: codex/ui-redesign

This document describes the backend state that the native UI may render. It is descriptive of the code at the published commit. Fields marked real are backed by persisted configuration, authentication metadata, runtime health or provider responses. Fields marked derived are computed from real fields. Fields marked estimated or placeholder must be labelled as such or hidden by the UI.


1. Snapshot boundary

HubSnapshot

Defined in router/state_store.py as a frozen dataclass.

Field Type Reality and meaning
generation int Real local sequence. Monotonically increases for every accepted rebuild within one process. Starts at 1; an empty bootstrap snapshot uses 0.
seq int Real request sequence token. Monotonically increasing counter of the latest completed refresh request. Guaranteed to equal or exceed generation.
timestamp float Real local time (time.time()) when the snapshot was built.
profiles_by_provider dict[str, list[ProfileViewModel]] Derived normalized profiles grouped by provider.
all_profiles dict[str, ProfileViewModel] Derived map keyed by profile_id. Profile IDs are unique by this map.
readiness SystemReadiness Derived readiness summary.
agents list[AgentViewModel] Derived current role assignments with active quota and session tracking.
providers list[ProviderSummary] Derived provider summaries.
routing dict[str, RolePipeline] Derived routing pipelines keyed by role ID.
quotas dict[str, QuotaSnapshot] Keyed by profile_id; see provider truth matrix below.
metrics dict[str, Any] Real local diagnostics: generation, sequence, build duration, profile counts and refresh counters.
is_stale bool True for uninitialized bootstrap snapshots or when background refresh is overdue (> 300s).

Consistency guarantees:

  • The store publishes one snapshot reference after building it under an RLock; readers never observe a partially-built snapshot.
  • generation and seq are public comparison keys for the UI.
  • Stale background worker responses (seq < _latest_applied_seq) are strictly rejected and discarded.
  • get_snapshot() returns the cached snapshot without blocking disk scans.

2. Account and health models

ProfileViewModel

Field Type Optional Reality and meaning
profile_id str no Real configuration slot/profile ID.
display_name str no Real configured name when present; otherwise localized slot fallback.
account_identity str no Best available identifier: email → display name → provider account ID → profile ID.
provider str no Real normalized provider ID.
provider_display_name str no Derived localized/display label.
assigned_roles list[str] no Derived from router config.
primary_role str yes Derived/configured. May be absent for spare slots.
is_main_account bool no Real local profile preference.
is_main_orchestrator bool no Derived from orchestrator chain.
auth_state str no Normalized auth state (AUTHENTICATED, AUTH_REQUIRED, AUTH_EXPIRED, NOT_CONFIGURED).
health_state str no Normalized health state (healthy, quota_exhausted, rate_limited, cooldown, disabled, cold_spare, not_configured, unhealthy).
health_label_ru str no Derived presentation label.
model_states dict[str, ModelFamilyHealth] no Derived from local health tracker/runtime observations.
cooldown_remaining_sec int no Derived local runtime state. Zero when healthy.
last_checked_at str yes Real local check time string (%H:%M:%S).
enabled bool no Real config state.
is_cold_spare bool no Derived/configured.
is_empty_slot bool no Derived placeholder slot with no configured auth.
email str no Extracted from saved auth/JWT claims; empty string if unavailable.
plan str no Display text (e.g. "Тариф: MAX", "Тариф: PRO").
plan_code str no Normalized code (PRO, PLUS, MAX, SUPERGROK, UNKNOWN).
plan_source str no Real provenance: "provider_api", "jwt_claim", "provider_auth", "inferred", "unknown". UI uses this to display PlanBadge only when trustworthy (!= "unknown").
quota_snapshot QuotaSnapshot yes Associated quota snapshot object.
preferred_models list[str] no Real config/model-discovery values.

3. Quota models

QuotaSnapshot

Field Type Reality and meaning
account_id str Real local profile/account key.
provider str Real normalized provider ID.
buckets list[QuotaBucket] Separate capacity pools; never combine them into one percent.
fetched_at timezone-aware datetime Real local collection time.
stale_after_seconds int Local cache TTL, default 300 seconds.
source str Provenance: "runtime_event", "jwt_claim", "provider_auth", "baseline", "unconfigured".
unavailable_reason Optional[str] Human-readable reason when data cannot be collected.
is_estimated property True for baseline/unconfigured; False for verified runtime events and provider claims.

QuotaBucket

Field Type Notes
id str Stable bucket key (antigravity.claude.5h, antigravity.gemini.5h, codex.primary.weekly, claude.session.5h, grok.frequent_tasks).
display_name str User-facing label ("Claude 5h", "Gemini 5h", "Codex Weekly").
model_family Optional[str] Family selector ("claude", "gemini", "gpt", "grok", "opencode").
used_percent Optional[float] 0.0100.0 or None if unmeasured.
remaining_percent Optional[float] 0.0100.0 or None if unmeasured.
used_absolute Optional[int] Absolute units used if reported.
remaining_absolute Optional[int] Absolute units remaining.
limit_absolute Optional[int] Absolute maximum limit.
reset_at Optional[datetime] UTC reset timestamp.
reset_in_seconds Optional[int] Seconds until quota reset.
period Optional[str] "5h", "7d", "30d", "sliding".
status str "healthy", "warning", "exhausted", "unknown".

Provider Truth Matrix at v1.3

Provider Buckets emitted Values Reset Source / UI treatment
Antigravity antigravity.claude.5h, antigravity.gemini.5h, antigravity.claude.7d, antigravity.gemini.7d Measured Cloud Code capacity pool percentages (or per-model pool). On 429: exact 0% remaining. Measured from Cloud Code resetTime. On 429: extracted from server response. Live: provider_api, is_estimated=False. 429 event: runtime_event, is_estimated=False.
OpenAI Codex codex.session, codex.weekly Probes /models endpoint with stored credentials, refresh token on 401. Values: None with explicit unavailable_reason. None. No synthetic reset times. Live: provider_api, is_estimated=False, unavailable_reason: "OpenAI Codex не предоставляет остаток через публичный API".
OpenCode Go opencode.5h, opencode.7d, opencode.30d Probes /models and /usage. When usage returned: measured percent & USD amounts. If 401/403/404: None with explicit reason. Measured reset_at from /usage or None. Live: provider_api, is_estimated=False. When usage endpoint unexposed: unavailable_reason: "OpenCode Go не предоставляет остаток через публичный API".
Claude claude.session, claude.weekly Values: None with explicit unavailable_reason. None. No synthetic reset times. Live: provider_api, is_estimated=False, unavailable_reason: "Claude не предоставляет остаток через публичный API".
Grok grok.weekly, grok.chat, grok.build, grok.frequent_tasks, grok.normal_tasks Values: None with explicit unavailable_reason. None. No synthetic reset times. Live: provider_api, is_estimated=False, unavailable_reason: "Grok не предоставляет остаток через публичный API".

Provider Sorting Guarantee in Snapshot

HubSnapshot.providers is guaranteed to be deterministically ordered by:

  1. connected_count descending (providers with active authenticated accounts appear first)
  2. total_slots descending
  3. provider_name ascending (alphabetical tie-breaker)

4. Team and routing models

AgentViewModel

Field Type Description
role_id str Logical role ("orchestrator", "coder-primary", "reviewer", etc.).
role_name_ru str Localized role title ("Главный оркестратор", "Кодер 1").
role_description_ru str Localized role description.
assigned_profile_id Optional[str] Active profile ID assigned to this role.
assigned_display_name Optional[str] Display name of assigned profile.
provider str Active provider ID.
provider_display_name str Localized provider name.
model str Selected active model.
account_identity str Masked identity of the active account.
routing_position str "Primary", "Fallback 1", "Fallback 2".
status str "healthy", "quota_exhausted", "auth_required", etc.
status_label_ru str Localized status text ("Работает", "Исчерпан").
is_active bool True if healthy and receiving requests.
is_main_orchestrator bool True if role is orchestrator.
cooldown_remaining_sec int Active cooldown in seconds.
session_id Optional[str] Active affinity session bound to this agent.
active_quota_status str Status of governing quota bucket ("healthy", "warning", "exhausted").
active_quota_label str Human-readable quota state (e.g. "Осталось 85%", "Доступна").

PipelineNode & RolePipeline

Each PipelineNode represents one failover step in a role's route:

Field Type Description
profile_id str Profile ID for this step.
display_name str Slot display name.
provider str Localized provider name.
model str Model configured for this step.
account_identity str Masked account identity for this node.
status str Health status of this node ("healthy", "quota_exhausted").
status_label_ru str Localized status text.
quota_status str Quota health status ("healthy", "exhausted").
is_active bool True if this node is currently handling traffic.
cooldown_remaining_sec int Cooldown in seconds.
failover_reason Optional[str] Real reason why traffic switched from this node (e.g. "Primary исчерпал квоту (429)", "Требуется авторизация"). None for active node or standby reserve.

RolePipeline: role_id, role_name_ru, default_model, max_failover, session_affinity, active_profile_id, nodes: List[PipelineNode].


5. Event bus contract

Callbacks receive (event_name: str, payload: Any). All events carry active generation and seq tokens.

Event Constant Name String Payload Contract (v1.2) Canonical Publisher Site
EVENT_ACCOUNT_UPDATED "ACCOUNT_UPDATED" {"profile_id": str, "profile": ProfileViewModel, "generation": int, "seq": int} HubStateStore.apply_delta_account_updated
EVENT_ACCOUNT_ADDED "ACCOUNT_ADDED" {"provider": str, "profile_id": str, "profile": ProfileViewModel, "generation": int, "seq": int} HubStateStore.apply_delta_account_added
EVENT_ACCOUNT_REMOVED "ACCOUNT_REMOVED" {"provider": str, "profile_id": str, "generation": int, "seq": int} HubStateStore.apply_delta_account_removed
EVENT_ACCOUNT_AUTH_CHANGED "ACCOUNT_AUTH_CHANGED" {"provider": str, "profile_id": str, "auth_state": str, "generation": int, "seq": int} HubStateStore.publish_auth_changed
EVENT_QUOTA_UPDATED "QUOTA_UPDATED" {"provider": str, "profile_id": str, "snapshot": QuotaSnapshot, "quota_snapshot": QuotaSnapshot, "generation": int, "seq": int} HubStateStore.apply_delta_quota_updated
EVENT_ROUTING_UPDATED "ROUTING_UPDATED" {"role_id": str, "active_profile_id": str, "pipeline": RolePipeline, "generation": int, "seq": int} HubStateStore.apply_delta_route_changed, RouterEngine.route_request
EVENT_AGENT_UPDATED "AGENT_UPDATED" {"role_id": str, "agent": AgentViewModel, "generation": int, "seq": int} HubStateStore.apply_delta_route_changed
EVENT_SYSTEM_READINESS_CHANGED "SYSTEM_READINESS_CHANGED" {"readiness": SystemReadiness, "generation": int, "seq": int} HubStateStore.refresh
EVENT_REFRESH_STARTED "REFRESH_STARTED" {"key": str, "seq": int} HermesRefreshScheduler._execute_task
EVENT_REFRESH_COMPLETED "REFRESH_COMPLETED" {"generation": int, "seq": int, "duration_ms": float} HubStateStore.refresh
EVENT_REFRESH_FAILED "REFRESH_FAILED" {"key": str, "error": str, "seq": int} HermesRefreshScheduler._execute_task

6. Closed Gaps & Audit Status

Gap ID Description Status Implementation Details / Commit
Gap 1 & 2 Antigravity Claude vs Gemini bucket isolation & truthful 429 reset parsing Closed Multi-bucket model-family isolation in quota_collector.py with exact reset timestamps parsed on runtime 429 events (2035c14).
Gap 3 Public seq in HubSnapshot Closed HubSnapshot.seq exposed to UI; matches accepted refresh token (2035c14).
Gap 5 Stale state policy (is_stale) Closed Explicit policy: is_stale=True for uninitialized bootstrap snapshots or when age exceeds 300 seconds (state_store.py).
Gap 6 Plan provenance for PlanBadge Closed ProfileViewModel.plan_source added ("provider_api", "jwt_claim", "provider_auth", "inferred", "unknown"). UI displays badge only when trustworthy (2035c14).
Gap 7 AgentViewModel active session and quota Closed session_id, active_quota_status, and active_quota_label added (2035c14).
Gap 8 PipelineNode identity, quota, and failover reason Closed account_identity, quota_status, and real failover_reason added (2035c14).
Gap 9 Canonical publishers for all declared events Closed Every declared event constant has a dedicated, verified publisher in state_store.py / router_engine.py. Dead event constants removed (2035c14).
Gap 10 Scheduler async quota race Closed Scheduler triggers complete quota collection before invoking snapshot rebuild (2035c14).
Gap 11 Stale response protection verification Closed seq recorded only on completion; late responses strictly dropped with test proof (2035c14).
Gap 12 Empirical Call Telemetry & Metrics Closed (Self-Measured) TelemetryService captures real call latency, exact token usage reported in provider usage, failover events, and USD cost (when user pricing is defined). All metrics carry source: "own_measurement". When no calls exist in the query window, values are None (has_data=False), never fake zeros (antigravity/telemetry).
Gap 13 Host Hardware Indicators (psutil) Closed (Self-Measured) HostMetricsService captures real host CPU (%), RAM (MB/%), Disk (GB/%), and network I/O with source: "host_measurement" (antigravity/dashboard-data).

7. Active Limitations & Backend Constraints

The following constraints are active in the backend and must be strictly respected by the UI:

Gap ID Limitation Constraint & UI Requirement
Gap 4 Shallow Immutability of Snapshot HubSnapshot is defined with dataclass(frozen=True) which prevents attribute reassignments. However, contained lists and dictionaries remain standard mutable Python collections. UI Constraint: The UI must treat HubSnapshot and all nested view models as strictly read-only and must never mutate any collection or object in place.
Gap 14 External Provider Server Internals & SLA The backend cannot measure external provider server-side RPS, external datacenter SLA uptime percentages, task priority queue subsystems, or scheduled maintenance windows (these concepts do not exist in Hermes Hub). UI Constraint: The UI must display Н (Нет данных) or omit these cards. The UI must never generate fictional numbers or render mock graphs.

8. Telemetry, Host Metrics, and Active Calls Contract

The backend exposes real empirical metrics via TelemetryService.get().get_breakdown(...), HostMetricsService.collect(), and HubSnapshot.metrics:

8.1 Call Telemetry & Routing Distribution (source: "own_measurement")

Accessible at HubSnapshot.metrics["telemetry"]:

  • global: Overall TelemetryAggregates dictionary across all calls in the window (default 24h).
  • by_provider: {provider_id: TelemetryAggregates} including call_share (e.g. 0.45, 0.35, 0.20), median latency latency_p50_ms, and total_calls.
  • by_role: {role_id: TelemetryAggregates} with total_calls, latency_p50_ms, and total_tokens.
Field Type Provenance Description / Absence Behavior
source str "own_measurement" Identifies measurements taken by Hermes Hub router itself.
has_data bool Empirical True if at least 1 router call occurred in the window; False if no calls recorded.
total_calls int Empirical Total number of invocation attempts in the window.
successful_calls int Empirical Count of successful invocations (including successful failovers).
failed_calls int Empirical Count of terminal failures.
call_share Optional[float] Computed Ratio of filtered calls to total window calls (0.0 to 1.0), or None if no calls in window.
error_rate Optional[float] Computed Ratio of failed calls to total calls (0.0 to 1.0), or None if has_data=False.
latency_p50_ms Optional[float] Empirical Median invocation latency in milliseconds, or None if has_data=False.
latency_p95_ms Optional[float] Empirical 95th percentile invocation latency in milliseconds, or None if has_data=False.
latency_max_ms Optional[float] Empirical Maximum invocation latency in milliseconds, or None if has_data=False.
total_prompt_tokens Optional[int] Provider usage Sum of prompt tokens reported by providers, or None if no usage returned.
total_completion_tokens Optional[int] Provider usage Sum of completion tokens reported by providers, or None if no usage returned.
total_tokens Optional[int] Provider usage Sum of all tokens reported by providers, or None if no usage returned.
total_cost_usd Optional[float] User Pricing Computed USD cost based on user-configured pricing in router_profiles.yaml, or None if no pricing configured.
failovers_count int Empirical Number of failover switches from initial profile.
failover_reasons Dict[str, int] Empirical Histogram of failover triggers ("quota_exhausted", "rate_limited", "auth_required", etc.).

8.2 Host System Metrics (source: "host_measurement")

Accessible at HubSnapshot.metrics["host"]:

Field Type Provenance Description / Absence Behavior
source str "host_measurement" Measured directly on the local machine via psutil.
has_data bool Empirical True if psutil data is available; False if unavailable or on error.
cpu_percent Optional[float] psutil Host CPU utilization percentage (0.0 to 100.0), or None if unavailable.
memory_percent Optional[float] psutil Host RAM utilization percentage (0.0 to 100.0), or None if unavailable.
memory_used_mb Optional[float] psutil Used physical memory in megabytes.
memory_total_mb Optional[float] psutil Total physical memory in megabytes.
disk_percent Optional[float] psutil Root disk partition utilization percentage (0.0 to 100.0), or None if unavailable.
disk_used_gb Optional[float] psutil Used disk storage in gigabytes.
disk_total_gb Optional[float] psutil Total disk storage in gigabytes.
net_speed_mbps Optional[float] psutil Live total network throughput in Megabits per second (Mbps) computed across sampling intervals, or None on initial sample.
net_sent_mbps Optional[float] psutil Live outbound network throughput in Megabits per second (Mbps).
net_recv_mbps Optional[float] psutil Live inbound network throughput in Megabits per second (Mbps).
net_bytes_sent Optional[int] psutil Cumulative bytes sent since host boot (raw counter).
net_bytes_recv Optional[int] psutil Cumulative bytes received since host boot (raw counter).

Measurement Note (CPU Warm-up): The first CPU measurement is pre-warmed during service initialization to prevent cold-start 0.0% artifacts. Subsequent measurements read the differential counters non-blockingly.

8.3 Active Calls Telemetry (source: "own_measurement")

  • HubSnapshot.metrics["active_calls_total"]: int (Total ongoing concurrency leases managed across all profiles by RouterEngine).
  • HubSnapshot.metrics["active_calls_by_profile"]: Dict[str, int] (Active concurrency leases per profile ID).
  • ProfileViewModel.active_leases: int (Current number of active leases for this specific profile).

9. Configuration Preservation Status

Component Status Behavior & Details
Header Comments & Structure Supported All leading YAML comments, document banners, and blank lines before the first dictionary key (existing_comments) are preserved across file writes.
Inline Section Annotations Partially Supported Inline dictionary comments (such as comments inside profiles, roles, or pricing) are normalized during canonical YAML serialization (safe_dump).

10. Граница между учётными системами Hub и Hermes (System Boundaries)

Hub и Hermes используют разные множества профилей и настроек. Чтобы интерфейс корректно отображал происходящее и не вводил пользователя в заблуждение, важно понимать границу между ними.

10.1 Что Hub получает от Hermes на каждом вызове

При вызове ntigravity_llm_execution (через middleware) Hub фактически получает от Hermes только следующие данные:

  • ask_id
  • urn_id
  • pi_request_id
  • session_id
  • platform
  • model
  • provider
  • ase_url
  • pi_mode
  • pi_call_count

equest (payload: список сообщений, temperature и т.д.)

Явно: роли агента среди передаваемых данных нет.

10.2 Чего Hub не видит

Hub не имеет доступа к внутреннему контексту Hermes. В частности, Hub не видит:

  • Профиль Hermes, которым выполняется текущий вызов (например, gy-05, worker-fast, deepseek и др.).
  • Настройки конфигурации задач (delegate_task, max_concurrent_children и т.д.).
  • Состав и иерархию субагентов Hermes.

10.3 Чем Hub управляет

Hub является независимой системой и полностью управляет:

  • Собственными профилями (например, g-w1, g-orch-fallback, codex-orch, opengo-, claude-, grok-*).
  • Цепочками отказоустойчивости (failover), привязанными к его собственным профилям.
  • Квотами и авторизацией своих аккаунтов.

10.4 Чем Hub не управляет

Hub не управляет ничем из перечисленного в пункте 10.2. Он не может изменять состав агентов Hermes, перенастраивать профили Hermes или управлять делегированием задач.


11. Варианты связывания профилей Hub и Hermes

Поскольку один и тот же аккаунт пользователя может существовать в двух системах под разными именами (например, gy-05 в Hermes и g-w2 в Hub), существуют следующие варианты их связывания. Внимание: ни один из вариантов не должен реализовываться без явного решения владельца продукта.

Вариант Что становится возможным Что ломается / Риски Объем работы
1. Сопоставление по идентичности аккаунта (email) Автоматическое связывание большинства профилей без ручной настройки. Профили без email (например, worker-fast, deepseek, API-ключи OpenCode) не могут быть сопоставлены. Надежность зависит от гарантий Hermes по предоставлению идентичности. ~34 дня. Требует извлечения identity на стороне Hermes и передачи в Hub.
2. Чтение профилей Hermes (Single Source of Truth) Единый источник истины: Hub перестает вести свой список профилей и полностью отражает конфигурацию Hermes. Теряются сущности, специфичные для Hub: цепочки отказоустойчивости (failover chains), распределение ролей Hub. Квоты сложнее привязывать к профилям. ~812 дней. Требует глубокого рефакторинга конфигурации и движка роутинга Hub.
3. Явная таблица соответствия (Profile Mapping Table) Полный контроль и предсказуемость. Владелец может вручную связать любой профиль Hermes (например, gy-05) с профилем Hub (g-w2). Требует ручной настройки от пользователя в UI Hub. ~45 дней. Требует добавления конфигурации hermes_profile_map в
outer_profiles.yaml и поддержки в UI.

Рекомендация: Наиболее безопасным и предсказуемым является Вариант 3 (Явная таблица соответствия). Он сохраняет независимость систем (сохраняются цепочки отказоустойчивости Hub) и позволяет обрабатывать профили без email. Вариант 1 можно добавить позже как механизм автозаполнения для Варианта 3, чтобы упростить ручную настройку.