hermes-hub/docs/UI_STATE_CONTRACT.md

240 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.2
| Provider | Buckets emitted | Values | Reset | Source / UI treatment |
|---|---|---|---|---|
| **Antigravity** | `antigravity.claude.5h`, `antigravity.gemini.5h` | Baseline: values `None`. On runtime 429: exact 0% remaining. | On 429: extracted from server response. Baseline: `None`. | Baseline: `baseline`, `is_estimated=True`. 429 event: `runtime_event`, `is_estimated=False`. |
| **OpenAI Codex** | `codex.primary.weekly` | Baseline: values `None`. On 429: 0% remaining. | On 429: derived. Baseline: `None`. | Baseline: `baseline`, `is_estimated=True`. |
| **Claude** | `claude.session.5h` | Baseline: values `None`. On 429: 0% remaining. | On 429: derived. Baseline: `None`. | Baseline: `baseline`, `is_estimated=True`. |
| **Grok** | `grok.frequent_tasks` | Baseline: values `None`. On 429: 0% remaining. | On 429: derived. Baseline: `None`. | Baseline: `baseline`, `is_estimated=True`. |
| **OpenCode Go** | `opencode.tasks` | Baseline: values `None`. | `None`. | Baseline: `baseline`, `is_estimated=True`. |
---
## 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`). |
---
## 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 13** | Unmeasurable Provider Internals & Host Hardware | The backend cannot measure external provider server RPS, SLA uptime percentages, or system host hardware (CPU/RAM/Disk/Network) as they are irrelevant to router logic. **UI Constraint:** The UI must display `Н/Д` (Нет данных) or hide these metric cards entirely. The UI must never generate fictional numbers or render mock graphs. |
---
## 8. Telemetry & Empirical Metrics Contract
The backend exposes real empirical metrics via `TelemetryService.get().get_aggregates(...)` and `HubSnapshot.metrics["telemetry"]`:
| Field | Type | Provenance | Description / Absence Behavior |
|---|---|---|---|
| `source` | `str` | `"own_measurement"` | Always 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. |
| `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.). |