16 KiB
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=Trueprevents replacing dataclass attributes, but nested dicts, lists and contained models remain mutable. The snapshot is therefore shallowly immutable, not deeply immutable.generationis the UI comparison key. A publicseqfield does not exist in version 1.0.- Scheduler request
seqis internal toHubStateStore; stale requests are rejected whenseq < _latest_applied_seq. The acceptedseqis 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:
- email;
- display name/username;
- provider account ID;
- 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:
- No provider currently supplies live numeric quota values through
AccountQuotaService; every configured-provider collector returns baseline buckets. - Antigravity bucket separation exists structurally, but the four values and reset times are not measured from provider responses.
HubSnapshothas no public requestseq; it exposes only generation.- Snapshot immutability is shallow.
is_stalehas no age/source policy beyond the empty bootstrap snapshot.- Plan provenance is not carried into
ProfileViewModel; inferred plans cannot be distinguished safely by the UI. AgentViewModellacks active session and quota data.PipelineNodelacks account identity, quota state and failover reason.- Most targeted event constants are declared but have no canonical publisher.
- Single/all scheduler triggers start nested asynchronous quota workers and may rebuild state before those quota workers finish.
- Stale-response protection records
seqbefore the slow work is complete, so the current implementation does not fully prove that a late result can never overwrite a newer result. - 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.