# Hermes Hub — UI State & ViewModel Contract **Document Version:** 1.0.0 **Date:** 2026-08-21 **Status:** Canonical Interface Specification for UI (Codex Assignment B) & State Layer (Antigravity Assignment A) **Scope:** `src/antigravity_provider/router/state_store.py`, `unified_health.py`, `account_identity.py`, `event_bus.py` --- ## 1. Executive Architecture & Invariants 1. **Single Source of Truth (`HubSnapshot`):** The entire UI layer reads state exclusively from the immutable `HubSnapshot` supplied by `HubStateStore.get().get_snapshot()` or emitted via `EventBus`. Views MUST NOT call scanning/probing methods (e.g. `scan_all()`). 2. **Delta Updates via `EventBus`:** Incremental updates (quota shifts, single account mutations, route swaps) dispatch targeted typed events on `EventBus.get()`. 3. **Data Truthfulness Invariant:** Unverified or offline metrics MUST report `is_estimated = True` with source `"baseline"` or `"estimated"`, and percentages as `None` (UI displays «Доступна» / «(оценка)»). Fabricated percentages or false `*_api` source labels are strictly forbidden. --- ## 2. Core Snapshot Model: `HubSnapshot` Immutable snapshot (`@dataclass(frozen=True)`) representing the state of Hermes Hub at generation `generation`. | Field | Type | Description | Real / Simulated | |---|---|---|---| | `generation` | `int` | Monotonically increasing generation number (increments on every snapshot update). | **Real** | | `timestamp` | `float` | UNIX epoch timestamp when snapshot was created (`time.time()`). | **Real** | | `profiles_by_provider` | `Dict[str, List[ProfileViewModel]]` | Profiles grouped by provider identifier (`"antigravity"`, `"openai-codex"`, `"opencode-go"`, `"claude"`, `"grok"`). | **Real** | | `all_profiles` | `Dict[str, ProfileViewModel]` | Flat lookup map of all profiles indexed by `profile_id`. | **Real** | | `readiness` | `SystemReadiness` | Aggregated system readiness, role coverage metrics, and warnings. | **Real** | | `agents` | `List[AgentViewModel]` | List of logical agent roles and their active routing status. | **Real** | | `providers` | `List[ProviderSummary]` | Summary status per provider for quick overview cards. | **Real** | | `routing` | `Dict[str, RolePipeline]` | Role pipelines mapping `role_id` to failover nodes. | **Real** | | `quotas` | `Dict[str, QuotaSnapshot]` | Map of `profile_id` -> `QuotaSnapshot`. | **Real** | | `metrics` | `Dict[str, Any]` | Internal metrics (e.g. `refresh_runs_total`, `refresh_failures_total`). | **Real** | | `is_stale` | `bool` | True if background refresh is overdue (> 300s). | **Real** | ### Helper Methods - `get_profile(profile_id: str) -> Optional[ProfileViewModel]` - `get_provider_profiles(provider: str) -> List[ProfileViewModel]` - `get_role_pipeline(role_id: str) -> Optional[RolePipeline]` --- ## 3. Account View Model: `ProfileViewModel` Model representing an individual account/slot card in UI views. | Field | Type | Nullable / Optional | Description & Values | |---|---|---|---| | `profile_id` | `str` | No | Unique profile identifier (e.g. `"ag-w1"`, `"codex-slot-1"`). | | `display_name` | `str` | No | User-facing display title (e.g. `"Antigravity Slot 1"`). | | `account_identity` | `str` | No | Primary user identity (email, account ID, or profile ID). | | `provider` | `str` | No | Provider ID (`"antigravity"`, `"openai-codex"`, `"opencode-go"`, `"claude"`, `"grok"`). | | `provider_display_name` | `str` | No | Formatted provider name (e.g. `"Google Antigravity"`, `"OpenAI Codex"`). | | `assigned_roles` | `List[str]` | No | List of role names assigned to this profile. | | `primary_role` | `Optional[str]` | Yes | Primary assigned role name (or `None` if unassigned / spare). | | `is_main_account` | `bool` | No | True if designated as Main Account for general execution. | | `is_main_orchestrator` | `bool` | No | True if designated as Orchestrator profile. | | `auth_state` | `str` | No | `"AUTHENTICATED"` \| `"AUTH_REQUIRED"` \| `"AUTH_EXPIRED"` \| `"UNCONFIGURED"`. | | `health_state` | `str` | No | `"healthy"` \| `"warning"` \| `"exhausted"` \| `"rate_limited"` \| `"cooldown"` \| `"auth_required"` \| `"disabled"` \| `"cold_spare"` \| `"unhealthy"` \| `"not_configured"`. | | `health_label_ru` | `str` | No | Localized Russian status text (e.g. `"Готов"`, `"Исчерпан"`, `"Ограничение"`). | | `model_states` | `Dict[str, ModelFamilyHealth]` | No | Per-family health records (e.g. `{"gemini": ModelFamilyHealth(...)}`). | | `cooldown_remaining_sec` | `int` | No | Seconds until cooldown/rate-limit expires (`0` if healthy). | | `last_checked_at` | `Optional[str]` | Yes | Formatted time string (e.g. `"15:42:10"` or `"недавно"`). | | `enabled` | `bool` | No | True if slot is enabled in `router_profiles.yaml`. | | `is_cold_spare` | `bool` | No | True if authenticated but unassigned to any active role chain. | | `is_empty_slot` | `bool` | No | True if unauthenticated placeholder slot. | | `email` | `str` | No | Email parsed from JWT/API (empty string if unavailable). | | `plan` | `str` | No | Localized plan string (e.g. `"Тариф: MAX"`, `"Тариф: PRO"`, `"Тариф: неизвестен"`). | | `plan_code` | `str` | No | Normalized plan code (`"PRO"`, `"PLUS"`, `"MAX"`, `"SUPERGROK"`, `"UNKNOWN"`). | | `quota_snapshot` | `Optional[QuotaSnapshot]` | Yes | Associated quota snapshot object. | | `preferred_models` | `List[str]` | No | List of configured models for this profile. | --- ## 4. Quota Models: `QuotaSnapshot` & `QuotaBucket` ### `QuotaSnapshot` Schema - `account_id: str` — Profile or Account ID - `provider: str` — Provider ID - `buckets: List[QuotaBucket]` — 1 to 4 limit buckets - `fetched_at: datetime` — UTC timestamp of measurement - `stale_after_seconds: int` — Expiry window (default 300s) - `source: str` — `"baseline"` \| `"estimated"` \| `"runtime_event"` \| `"provider_api"` - `is_estimated: bool` — **True** if offline baseline / heuristic, **False** if verified live API - `freshness_label() -> str` — e.g. `"Обновлено: только что"`, `"Обновлено: 5 мин назад"` ### `QuotaBucket` Schema - `id: str` — e.g. `"antigravity.claude.5h"`, `"codex.weekly"`, `"grok.frequent_tasks"` - `display_name: str` — e.g. `"5h"`, `"Weekly"`, `"Задачи"`, `"Запросы"` - `model_family: Optional[str]` — Model family bounded by this bucket (`"gemini"`, `"claude"`, `"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 by provider API) - `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"` - `formatted_remaining() -> str` — e.g. `"Доступна"`, `"Осталось 85%"`, `"150/1000"` - `formatted_reset() -> Optional[str]` — e.g. `"Сброс через 1ч 45м"`, `None` --- ## 5. Actual Provider Quota Breakdown (Live vs Estimated) | Provider | Real Quota APIs Available? | Buckets Populated | `source` | `is_estimated` | Notes | |---|---|---|---|---|---| | **Google Antigravity** | Partial (via CLI runtime 429 reset parsing) | `antigravity.claude.5h`, `antigravity.claude.weekly`, `antigravity.gemini` | `"baseline"` or `"runtime_event"` | `True` (baseline) / `False` (on 429 event) | Baseline shows «Доступна (оценка)». On 429, parses exact reset duration. | | **OpenAI Codex** | Device flow / local token | `codex.primary.weekly` | `"baseline"` or `"runtime_event"` | `True` (baseline) / `False` (on 429 event) | Baseline shows «Доступна (оценка)». | | **xAI Grok** | Device flow / token | `grok.frequent_tasks`, `grok.daily_quota` | `"baseline"` or `"runtime_event"` | `True` (baseline) / `False` (on 429 event) | Baseline shows «Доступна (оценка)». | | **Anthropic Claude** | Token / PKCE | `claude.session.5h`, `claude.weekly` | `"baseline"` or `"runtime_event"` | `True` (baseline) / `False` (on 429 event) | Baseline shows «Доступна (оценка)». | | **OpenCode Go** | Local CLI / API key | `opencode.tasks` | `"baseline"` | `True` | Baseline shows «Доступна (оценка)». | --- ## 6. Routing & System ViewModels ### `SystemReadiness` - `state: str` — `"healthy"` \| `"limited"` \| `"degraded"` \| `"critical"` - `title_ru: str` — Summary title (e.g. `"Система готова к работе"`) - `summary_ru: str` — Explanatory status text - `roles_ready_count: int`, `total_roles: int` - `accounts_connected_count: int`, `total_accounts: int` - `providers_ready_count: int`, `total_providers: int` - `warnings: List[str]` — Critical warning strings for banner display ### `AgentViewModel` - `role_id: str` — Logical role (e.g. `"coder-primary"`, `"reviewer"`) - `role_name_ru: str`, `role_description_ru: str` - `assigned_profile_id: Optional[str]`, `assigned_display_name: Optional[str]` - `provider: str`, `provider_display_name: str`, `model: str` - `routing_position: str` — `"Primary"`, `"Fallback 1"`, `"Fallback 2"` - `status: str` — `"healthy"`, `"exhausted"`, `"auth_required"`, `"unconfigured"` - `is_active: bool`, `is_main_orchestrator: bool`, `cooldown_remaining_sec: int` ### `RolePipeline` & `PipelineNode` - `RolePipeline`: `role_id`, `role_name_ru`, `default_model`, `max_failover`, `session_affinity`, `active_profile_id`, `nodes: List[PipelineNode]` - `PipelineNode`: `profile_id`, `display_name`, `provider`, `model`, `status`, `status_label_ru`, `is_active: bool`, `cooldown_remaining_sec: int` ### `ProviderSummary` - `provider_id: str`, `provider_name: str`, `total_slots: int`, `connected_count: int`, `online_count: int` - `auth_required_count: int`, `quota_exhausted_count: int`, `cold_spare_count: int` - `discovered_models: List[str]`, `last_refresh_at: str` --- ## 7. EventBus Event Catalog & Payloads Subscribers register with `EventBus.get().subscribe(event_name, callback)`: | Event Constant | Name String | Payload Schema | Trigger Condition | |---|---|---|---| | `EVENT_ACCOUNT_UPDATED` | `"ACCOUNT_UPDATED"` | `{"provider": str, "profile_id": str, "profile": ProfileViewModel}` | Single profile auth, role, or state changed | | `EVENT_ACCOUNT_ADDED` | `"ACCOUNT_ADDED"` | `{"provider": str, "profile_id": str}` | New account authorized / connected | | `EVENT_ACCOUNT_REMOVED` | `"ACCOUNT_REMOVED"` | `{"provider": str, "profile_id": str}` | Account removed / disconnected | | `EVENT_ACCOUNT_AUTH_CHANGED` | `"ACCOUNT_AUTH_CHANGED"` | `{"provider": str, "profile_id": str, "auth_state": str}` | Token expired or auth invalidated | | `EVENT_QUOTA_UPDATED` | `"QUOTA_UPDATED"` | `{"provider": str, "profile_id": str, "snapshot": QuotaSnapshot}` | Single account quota changed (429 or refresh) | | `EVENT_QUOTA_STALE` | `"QUOTA_STALE"` | `{"provider": str, "profile_id": str}` | Quota snapshot expired | | `EVENT_PROVIDER_HEALTH_CHANGED` | `"PROVIDER_HEALTH_CHANGED"` | `{"provider": str, "summary": ProviderSummary}` | Aggregate provider health shift | | `EVENT_ROUTING_UPDATED` | `"ROUTING_UPDATED"` | `{"role_id": str, "pipeline": RolePipeline}` | Active role routing modified | | `EVENT_ROUTING_SLOT_UPDATED` | `"ROUTING_SLOT_UPDATED"` | `{"role_id": str, "node": PipelineNode}` | Failover occurred to backup node | | `EVENT_AGENT_UPDATED` | `"AGENT_UPDATED"` | `{"role_id": str, "agent": AgentViewModel}` | Agent role status changed | | `EVENT_SYSTEM_READINESS_CHANGED`| `"SYSTEM_READINESS_CHANGED"`| `{"readiness": SystemReadiness}` | Global readiness level shifted | | `EVENT_REFRESH_STARTED` | `"REFRESH_STARTED"` | `{"scope": str, "provider": Optional[str], "seq": int}` | Background refresh task started | | `EVENT_REFRESH_COMPLETED` | `"REFRESH_COMPLETED"` | `{"scope": str, "provider": Optional[str], "seq": int}` | Background refresh task succeeded | | `EVENT_REFRESH_FAILED` | `"REFRESH_FAILED"` | `{"scope": str, "provider": Optional[str], "error": str, "seq": int}` | Background refresh task failed | --- ## 8. Backend Gaps (Known Limitations & Gaps) To maintain complete transparency and prevent UI fabrication: 1. **Live Quota Metrics:** - OpenAI, xAI, and Claude do not offer public standard REST endpoints for real-time per-second token balances on OAuth device tokens without dedicated organization admin keys. - Consequently, initial state reports `source="baseline"`, `is_estimated=True`, and percentages as `None`. - Quotas transition to `status="exhausted"`, `source="runtime_event"`, `is_estimated=False` with exact reset timers **only upon encountering real provider 429 responses** during runtime execution. 2. **Subscription Expiry Timestamps:** - `SubscriptionPlan.expires_at` and `renews_at` are populated when present in JWT claims (e.g. Google CloudCode / Anthropic JWT claims); otherwise they are `None`. 3. **Model Discovery:** - Static default fallback models are provided when offline or before the first CLI invocation.