28 KiB
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. generationandseqare 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, not_tested, 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). |
last_success_at |
str |
yes | Time of last successful network test (%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.0–100.0 or None if unmeasured. |
remaining_percent |
Optional[float] |
0.0–100.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:
connected_countdescending (providers with active authenticated accounts appear first)total_slotsdescendingprovider_nameascending (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: OverallTelemetryAggregatesdictionary across all calls in the window (default 24h).by_provider:{provider_id: TelemetryAggregates}includingcall_share(e.g.0.45,0.35,0.20), median latencylatency_p50_ms, andtotal_calls.by_role:{role_id: TelemetryAggregates}withtotal_calls,latency_p50_ms, andtotal_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 byRouterEngine).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 на каждом вызове
При вызове antigravity_llm_execution (через middleware) Hub фактически получает от Hermes только следующие данные:
- ask_id
- urn_id
- api_request_id
- session_id
- platform
- model
- provider
- ase_url
- api_mode
- api_call_count
equest (payload: список сообщений, temperature и т.д.)
Явно: роли агента среди передаваемых данных нет.
10.2 Чего Hub не видит
Hub не имеет доступа к внутреннему контексту Hermes. В частности, Hub не видит:
- Профиль Hermes, которым выполняется текущий вызов (например, agy-05, worker-fast, deepseek и др.).
- Настройки конфигурации задач (delegate_task, max_concurrent_children и т.д.).
- Состав и иерархию субагентов Hermes.
10.3 Чем Hub управляет
Hub является независимой системой и полностью управляет:
- Собственными профилями (например, ag-w1, ag-orch-fallback, codex-orch, opengo-, claude-, grok-*).
- Цепочками отказоустойчивости (failover), привязанными к его собственным профилям.
- Квотами и авторизацией своих аккаунтов.
10.4 Чем Hub не управляет
Hub не управляет ничем из перечисленного в пункте 10.2. Он не может изменять состав агентов Hermes, перенастраивать профили Hermes или управлять делегированием задач.
11. Варианты связывания профилей Hub и Hermes
Поскольку один и тот же аккаунт пользователя может существовать в двух системах под разными именами (например, agy-05 в Hermes и ag-w2 в Hub), существуют следующие варианты их связывания. Внимание: ни один из вариантов не должен реализовываться без явного решения владельца продукта.
| Вариант | Что становится возможным | Что ломается / Риски | Объем работы |
|---|---|---|---|
| 1. Сопоставление по идентичности аккаунта (email) | Автоматическое связывание большинства профилей без ручной настройки. | Профили без email (например, worker-fast, deepseek, API-ключи OpenCode) не могут быть сопоставлены. Надежность зависит от гарантий Hermes по предоставлению идентичности. | ~3–4 дня. Требует извлечения identity на стороне Hermes и передачи в Hub. |
| 2. Чтение профилей Hermes (Single Source of Truth) | Единый источник истины: Hub перестает вести свой список профилей и полностью отражает конфигурацию Hermes. | Теряются сущности, специфичные для Hub: цепочки отказоустойчивости (failover chains), распределение ролей Hub. Квоты сложнее привязывать к профилям. | ~8–12 дней. Требует глубокого рефакторинга конфигурации и движка роутинга Hub. |
| 3. Явная таблица соответствия (Profile Mapping Table) | Полный контроль и предсказуемость. Владелец может вручную связать любой профиль Hermes (например, agy-05) с профилем Hub (ag-w2). | Требует ручной настройки от пользователя в UI Hub. | ~4–5 дней. Требует добавления конфигурации hermes_profile_map в |
| outer_profiles.yaml и поддержки в UI. |
Рекомендация: Наиболее безопасным и предсказуемым является Вариант 3 (Явная таблица соответствия). Он сохраняет независимость систем (сохраняются цепочки отказоустойчивости Hub) и позволяет обрабатывать профили без email. Вариант 1 можно добавить позже как механизм автозаполнения для Варианта 3, чтобы упростить ручную настройку.