# Hermes Hub UI state contract - Contract version: **1.0** - Published against: **`f171a8069d97aef5d3a45f838daed63abf2e69c1`** - Contract owner: `antigravity/state-layer` - 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, not of intended future data. 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. | | `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 assumed globally unique by this map. | | `readiness` | `SystemReadiness` | **Derived** readiness summary. | | `agents` | `list[AgentViewModel]` | **Derived** current role assignments. | | `providers` | `list[ProviderSummary]` | **Derived** provider summaries. | | `routing` | `dict[str, RolePipeline]` | **Derived** routing pipelines keyed by role ID. | | `quotas` | `dict[str, QuotaSnapshot]` | Mixed. Keyed by `profile_id`; see provider truth matrix below. | | `metrics` | `dict[str, Any]` | **Real local diagnostics only:** generation, build duration, profile counts and refresh counters. These are not provider throughput/error metrics. | | `is_stale` | `bool` | `True` only for the empty bootstrap snapshot at this version. No age-based stale policy is implemented yet. | Consistency guarantees: - The store publishes one snapshot reference after building it under an `RLock`; readers never observe the assignment half-complete. - `frozen=True` prevents replacing dataclass attributes, but nested dicts, lists and contained models remain mutable. The snapshot is therefore shallowly immutable, not deeply immutable. - `generation` is the UI comparison key. A public `seq` field does **not** exist in version 1.0. - Scheduler request `seq` is internal to `HubStateStore`; stale requests are rejected when `seq < _latest_applied_seq`. The accepted `seq` is not exposed to the UI. - `get_snapshot()` returns the cached snapshot; on first use it performs a non-forced state build. ## 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 a local fallback. | | `account_identity` | `str` | no | Best available identifier: email → display name → provider account ID → profile ID. Real when auth metadata/JWT contains identity; fallback otherwise. | | `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.** Includes primary/fallback annotations. | | `primary_role` | `str` | yes | **Derived/configured.** May be absent. | | `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; meanings below. | | `health_state` | `str` | no | Normalized health state; meanings below. | | `health_label_ru` | `str` | no | **Derived presentation label.** | | `model_states` | `dict[str, ModelFamilyHealth]` | no | **Derived from local health tracker/runtime observations.** Empty if unobserved. | | `cooldown_remaining_sec` | `int` | no | **Derived local runtime state.** Zero when unknown/not cooling down. | | `last_checked_at` | `str` | yes | **Real local check time string**, not a provider timestamp. | | `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` | logically yes | Real only if available from saved auth/JWT; empty string otherwise. | | `plan` | `str` | logically yes | Display text. At version 1.0 several providers receive inferred defaults; UI must show only if `plan_code != UNKNOWN` **and** source is made trustworthy in a later contract revision. | | `plan_code` | `str` | no | May be inferred (`PRO`, `PLUS`, `MAX`, etc.); not uniformly provider-confirmed. | | `quota_snapshot` | `QuotaSnapshot` | yes | See quota matrix. | | `preferred_models` | `list[str]` | no | **Real config/model-discovery values** when present. | `auth_state` values: | Value | Meaning | |---|---| | `AUTHENTICATED` | Saved authentication material passed the local presence/shape checks. It does not guarantee a fresh remote token check. | | `AUTH_REQUIRED` | No usable saved authentication material is available. | | `AUTH_EXPIRED` | Backend identified expired authentication. Not all providers can distinguish this from `AUTH_REQUIRED`. | `health_state` values: | Value | Meaning | |---|---| | `healthy` | Locally considered available. | | `quota_low` | Local/provider quota evidence indicates a warning threshold. | | `quota_exhausted` | Runtime/provider evidence indicates exhausted quota. | | `cooldown` | Local health tracker has an active cooldown. | | `rate_limited` | Runtime observed a rate limit. | | `not_configured` | Empty slot/no account. | | `auth_required` | Authentication missing. | | `auth_expired` | Authentication expired when distinguishable. | | `disabled` | Disabled in configuration. | | `cold_spare` | Configured reserve not currently active. | | `unhealthy` | Failure not represented by a more specific state. | | `not_tested` | No usable health observation exists. | ### `ModelFamilyHealth` `family`, `display_name`, `status`, `status_label_ru` are required derived fields. `cooldown_remaining_sec` defaults to 0. `reset_at` and `reason` are optional and exist only when the local health tracker recorded them. ## 3. Identity and plan provenance `AccountIdentity` carries provider/profile ID, optional email, display name, provider account ID, organization, `SubscriptionPlan`, auth method, authenticated flag and local verification time. Identity preference is contractual: 1. email; 2. display name/username; 3. provider account ID; 4. profile ID fallback. The backend never exposes access tokens, refresh tokens, authorization codes or raw API keys through these ViewModels. `SubscriptionPlan.source` may be `provider_api`, `provider_auth`, `jwt_claim`, `inferred` or `unknown`. At version 1.0 plan detection in `AccountQuotaService._resolve_identity()` assigns inferred provider defaults when explicit metadata is missing. Therefore the UI must hide a plan badge unless a later contract revision supplies trustworthy plan provenance alongside `ProfileViewModel`. ## 4. 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 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. `baseline`, `estimated`, `unconfigured`, `local_heuristic` imply `is_estimated=True`. | | `unavailable_reason` | `Optional[str]` | Human-readable reason when data cannot be collected. | | `is_estimated` | property | Derived solely from `source`. | ### `QuotaBucket` | Field | Type | Notes | |---|---|---| | `id` | `str` | Stable bucket key. | | `display_name` | `str` | User-facing label. This is the requested logical `label`. | | `model_family` | `Optional[str]` | Family/pool selector when known. | | `used_percent` | `Optional[float]` | 0–100; reconciled from remaining percent when one side exists. | | `remaining_percent` | `Optional[float]` | 0–100; reconciled from used percent when one side exists. | | `used_absolute` | `Optional[int]` | Requested logical `used`. | | `remaining_absolute` | `Optional[int]` | Absolute remaining quantity. | | `limit_absolute` | `Optional[int]` | Requested logical `limit`. | | `reset_at` | `Optional[datetime]` | Reset time if measured or estimated. | | `reset_in_seconds` | `Optional[int]` | Relative reset duration if known. | | `period` | `Optional[str]` | `5h`, `7d`, `30d`, `sliding`, or provider-specific. | | `status` | `str` | `healthy`, `warning`, `exhausted`, `unknown`; derived from remaining values where available. | Requested fields `unit` and `scope` do not exist in version 1.0. The closest available fields are absolute-value semantics implied by the provider and `model_family`/`period`. The UI must not invent units. ### Provider truth matrix at contract version 1.0 | Provider | Buckets emitted | Values | Reset | Source / UI treatment | |---|---|---|---|---| | Antigravity | Claude 5h, Claude Weekly, Gemini 5h, Gemini Weekly | Percent/absolute values are absent | Locally projected +5h/+7d | `baseline`; **estimated**, label explicitly. No live provider quota call. | | OpenAI Codex | Session, Weekly | Values absent | Locally projected +5h/+7d | `baseline`; **estimated**. | | OpenCode Go | Sliding, Weekly, Monthly | Values absent | Weekly/monthly locally projected; sliding reset absent | `baseline`; **estimated**. | | Claude | Current session, Current week | Values absent | Locally projected +5h/+7d | `baseline`; **estimated**. | | Grok | Weekly, GrokChat, GrokBuild, frequent tasks, normal tasks | Usage/remaining absent. Task limits 10/30 are static placeholders. | Mostly absent | `baseline`; **estimated**. | | Unknown provider | One default bucket | Values and reset absent | absent | `baseline`; **estimated**. | | Unconfigured account | No buckets | no data | absent | `unconfigured`; show unavailable reason. | Runtime 429 handling may set one matching bucket to 100% used / 0% remaining with a locally assumed reset duration. This is real evidence of exhaustion but the reset time remains estimated. ## 5. Team and routing models ### `AgentViewModel` Required fields: role ID/name/description, optional assigned profile ID and display name, provider ID/display name, model, account identity, routing position, status/status label, active flag and orchestrator flag. `cooldown_remaining_sec` is derived local runtime state. Reality notes: - role/profile chain comes from router configuration; - the selected model is the first preferred model or the string `default`; - active selection is the first healthy profile in the chain; - there is no active-session field and no per-agent quota field in version 1.0. ### `RolePipeline` Fields: `role_id`, `role_name_ru`, `default_model`, `max_failover`, `session_affinity`, `active_profile_id`, `nodes`. Each `PipelineNode` contains profile ID, display name, provider display name, model, health status/label, active flag and cooldown seconds. Node order is the configured primary → fallback order. The first healthy node is marked active. Version 1.0 does not include account identity, quota snapshot/status or a failover reason in each node. The UI may show the order and health but must not invent why a switch happened. ### `ProviderSummary` Contains provider ID/name; total, connected, online, auth-required, quota-exhausted and cold-spare counts; discovered model names; and a local last refresh time string. Counts are derived from `ProfileViewModel` objects. ### `SystemReadiness` Contains state (`healthy`, `limited`, `degraded`, `critical`), localized title and summary, ready/total counts for roles, connected/total accounts, ready/total providers, and warning strings. All values are derived from the current local profile and routing state; they are not remote SLA metrics. ## 6. Event bus contract Callbacks receive `(event_name: str, payload: Any)`. Delivery is synchronous on the publishing thread unless the caller uses `publish_to_ui(root, ...)`, which schedules through `root.after(0, ...)`. | Event | Payload contract at v1.0 | Emission status | |---|---|---| | `ACCOUNT_UPDATED` | `{profile_id, profile, generation}` | Emitted after targeted account delta rebuild when the profile exists. | | `ACCOUNT_ADDED` | Intended `{provider, profile_id, profile?, generation?}` | Declared only; no canonical publisher yet. | | `ACCOUNT_REMOVED` | Intended `{provider, profile_id, generation?}` | Declared only. | | `ACCOUNT_AUTH_CHANGED` | Intended `{provider, profile_id, auth_state, generation?}` | Declared only. | | `QUOTA_UPDATED` | `{provider, profile_id, quota_snapshot}` | Emitted by `HubStateStore.apply_delta_quota_updated`; generation absent. Collector listeners use a separate callback API. | | `QUOTA_STALE` | Intended `{provider, profile_id}` | Declared only. | | `PROVIDER_HEALTH_CHANGED` | Intended provider summary/delta | Declared only. | | `ROUTING_UPDATED` | Intended `{role_id, pipeline, reason?, generation?}` | Declared and consumed by UI, but no canonical backend publisher. | | `ROUTING_SLOT_UPDATED` | Intended targeted role/slot delta | Declared only. | | `AGENT_UPDATED` | Intended `{role_id, agent, generation?}` | Declared only. | | `SYSTEM_READINESS_CHANGED` | `SystemReadiness` object | Emitted after every accepted full rebuild. | | `REFRESH_STARTED` | `{key, seq}` | Emitted by scheduler before a task. | | `REFRESH_COMPLETED` | `{generation, duration_ms}` | Emitted by state store after rebuild. | | `REFRESH_FAILED` | `{key, error}` | Emitted by scheduler on failure. Error text must already be secret-safe. | Quota/account events are intended to update one stable UI widget keyed by `profile_id`; they must not be treated as instructions to reconstruct every account card. ## 7. Backend gaps The following cannot be honestly implemented by UI code alone: 1. No provider currently supplies live numeric quota values through `AccountQuotaService`; every configured-provider collector returns baseline buckets. 2. Antigravity bucket separation exists structurally, but the four values and reset times are not measured from provider responses. 3. `HubSnapshot` has no public request `seq`; it exposes only generation. 4. Snapshot immutability is shallow. 5. `is_stale` has no age/source policy beyond the empty bootstrap snapshot. 6. Plan provenance is not carried into `ProfileViewModel`; inferred plans cannot be distinguished safely by the UI. 7. `AgentViewModel` lacks active session and quota data. 8. `PipelineNode` lacks account identity, quota state and failover reason. 9. Most targeted event constants are declared but have no canonical publisher. 10. Single/all scheduler triggers start nested asynchronous quota workers and may rebuild state before those quota workers finish. 11. Stale-response protection records `seq` before the slow work is complete, so the current implementation does not fully prove that a late result can never overwrite a newer result. 12. Provider latency, RPS, error percentage, costs and remote SLA are absent. The UI must display `Н/Д` or omit those blocks. Any contract extension must update this document and identify the implementing commit before UI code relies on the new fields.