hermes-hub/docs/UI_STATE_CONTRACT.md
2026-08-20 23:46:50 +07:00

281 lines
16 KiB
Markdown
Raw Permalink 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.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]` | 0100; reconciled from remaining percent when one side exists. |
| `remaining_percent` | `Optional[float]` | 0100; 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.