diff --git a/.github/workflows/gateway-check.yml b/.github/workflows/gateway-check.yml
new file mode 100644
index 000000000..7662b1d1c
--- /dev/null
+++ b/.github/workflows/gateway-check.yml
@@ -0,0 +1,71 @@
+name: Gateway Checks
+
+# Guards the gateway's Go -> TypeScript codegen contract:
+# - tygo-check regenerates ui/src/api/types.ts from the Go structs
+# in gateway/internal/adminapi and fails on any diff, so a Go
+# struct change can't land without its committed TS counterpart
+# (drift would silently break the dashboard's typed fetch layer).
+# - ui-build type-checks (tsc -b) and bundles the SPA, catching
+# imports that the regenerated types.ts no longer satisfies.
+
+on:
+ pull_request:
+ paths:
+ - "gateway/**"
+ - ".github/workflows/gateway-check.yml"
+ push:
+ branches: [main]
+ paths:
+ - "gateway/**"
+ - ".github/workflows/gateway-check.yml"
+
+jobs:
+ tygo-check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ defaults:
+ run:
+ working-directory: gateway
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: gateway/go.mod
+ cache-dependency-path: gateway/go.sum
+
+ # Pinned so the generated output is byte-stable — a tygo release
+ # changing its formatting would otherwise fail every PR. Bump in
+ # lockstep with the version noted in gateway/Makefile.
+ - name: Install tygo
+ run: go install github.com/gzuidhof/tygo@v0.2.21
+
+ - name: Check generated TS types are up to date
+ run: make tygo-check
+
+ ui-build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ defaults:
+ run:
+ working-directory: gateway
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: npm
+ # package-lock.json is git-ignored for this SPA, so key the
+ # npm cache on package.json instead.
+ cache-dependency-path: gateway/internal/adminapi/ui/package.json
+
+ - name: Install SPA dependencies
+ run: make ui-install
+
+ - name: Type-check and build SPA
+ run: make ui-build
diff --git a/gateway/Makefile b/gateway/Makefile
index 563923ff5..e8fa0439d 100644
--- a/gateway/Makefile
+++ b/gateway/Makefile
@@ -90,8 +90,9 @@ ui: ui-install ui-build
# ─── tygo (Go -> TS struct codegen) ───────────────────────────────────
#
-# Install once per dev machine:
-# go install github.com/gzuidhof/tygo@latest
+# Install once per dev machine (CI pins the same version in
+# .github/workflows/gateway-check.yml — bump both together):
+# go install github.com/gzuidhof/tygo@v0.2.21
tygo:
tygo generate
diff --git a/gateway/go.sum b/gateway/go.sum
index 722d97016..0f6e99fce 100644
--- a/gateway/go.sum
+++ b/gateway/go.sum
@@ -54,6 +54,8 @@ github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/maximhq/bifrost/core v1.5.18 h1:f1lX3sPesKTScUJHv+K9tWpd1wLLjd7CAzJY0BpbAaE=
github.com/maximhq/bifrost/core v1.5.18/go.mod h1:7vry9xB5kmjT3smAVVQ2+mtfTmmeSN+7N1b8r1buaTI=
+github.com/maximhq/bifrost/core v1.6.2 h1:dESW02/iyDznt/VnnjzYnevddUmplXw2YeaK/0dl3HA=
+github.com/maximhq/bifrost/core v1.6.2/go.mod h1:GuRwPmx0Kh7lhZ+SnbLjVAclDI360DyKrJtioc3t5jg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
diff --git a/gateway/internal/adminapi/evals.go b/gateway/internal/adminapi/evals.go
index cf5df79ab..19f5aef5a 100644
--- a/gateway/internal/adminapi/evals.go
+++ b/gateway/internal/adminapi/evals.go
@@ -104,8 +104,9 @@ type evalRunRequest struct {
Agent string `json:"agent,omitempty"`
}
-// evalRefResponse is the create/link acknowledgement.
-type evalRefResponse struct {
+// EvalRefResponse is the create/link acknowledgement. Exported so
+// tygo emits the TS binding the SPA's eval mutations decode.
+type EvalRefResponse struct {
RefID string `json:"ref_id"`
Linked bool `json:"linked,omitempty"`
}
@@ -234,7 +235,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
writeError(w, http.StatusBadGateway, "catalog_write_failed", "neo4j write failed")
return
}
- writeJSON(w, http.StatusOK, evalRefResponse{RefID: req.SetID, Linked: true})
+ writeJSON(w, http.StatusOK, EvalRefResponse{RefID: req.SetID, Linked: true})
return
}
@@ -243,7 +244,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
writeError(w, http.StatusBadRequest, "missing_field", "name (or set_id) is required")
return
}
- var created evalRefResponse
+ var created EvalRefResponse
if err := h.hive.call(ctx, http.MethodPost, "/api/gateway/evals",
map[string]any{"name": req.Name, "description": req.Description}, &created); err != nil {
relayHiveError(w, err)
@@ -262,7 +263,7 @@ func (h *evalHandlers) createOrLinkForAgent(w http.ResponseWriter, r *http.Reque
"set created but linking to agent failed")
return
}
- writeJSON(w, http.StatusOK, evalRefResponse{RefID: created.RefID})
+ writeJSON(w, http.StatusOK, EvalRefResponse{RefID: created.RefID})
}
// linkEdge MERGEs HiveAgent-[:HAS_EVAL_SET]->EvalSet and clears any
@@ -457,7 +458,7 @@ func (h *evalHandlers) createSet(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "missing_field", "name is required")
return
}
- var created evalRefResponse
+ var created EvalRefResponse
if err := h.hive.call(r.Context(), http.MethodPost, "/api/gateway/evals",
map[string]any{"name": req.Name, "description": req.Description}, &created); err != nil {
relayHiveError(w, err)
@@ -513,7 +514,7 @@ func (h *evalHandlers) createRequirement(w http.ResponseWriter, r *http.Request,
writeError(w, http.StatusBadRequest, "missing_field", "name is required")
return
}
- var created evalRefResponse
+ var created EvalRefResponse
if err := h.hive.call(r.Context(), http.MethodPost,
"/api/gateway/evals/"+urlSeg(setID)+"/requirements", req, &created); err != nil {
relayHiveError(w, err)
diff --git a/gateway/internal/adminapi/ui/dist/index.html b/gateway/internal/adminapi/ui/dist/index.html
index c37010c7d..48d0c60e5 100644
--- a/gateway/internal/adminapi/ui/dist/index.html
+++ b/gateway/internal/adminapi/ui/dist/index.html
@@ -5,8 +5,8 @@
Agent Mothership
-
-
+
+
diff --git a/gateway/internal/adminapi/ui/src/api/client.ts b/gateway/internal/adminapi/ui/src/api/client.ts
index a2a392b58..93d19a5d4 100644
--- a/gateway/internal/adminapi/ui/src/api/client.ts
+++ b/gateway/internal/adminapi/ui/src/api/client.ts
@@ -12,7 +12,7 @@
// or auth-refresh logic here lights the trap of having two HTTP
// stacks; Tanstack Query gives us all of that one layer up.
-import type { ApiError } from "./types";
+import type { ApiError } from "./manual";
const PLUGIN_PREFIX = "/_plugin";
diff --git a/gateway/internal/adminapi/ui/src/api/manual.ts b/gateway/internal/adminapi/ui/src/api/manual.ts
new file mode 100644
index 000000000..44949e273
--- /dev/null
+++ b/gateway/internal/adminapi/ui/src/api/manual.ts
@@ -0,0 +1,169 @@
+// Hand-maintained types that tygo cannot generate. Everything the
+// SPA decodes from an exported Go struct in internal/adminapi lives
+// in the sibling types.ts, which is generated by `make tygo` and
+// checked in CI by `make tygo-check` — never hand-edit that file.
+//
+// What belongs here instead, and why:
+// - String-literal unions (Window / Bucket / Dimension): the Go
+// side validates these as plain strings, so there is no struct
+// for tygo to translate.
+// - ApiError: the phase-7 error envelope is built inline by
+// adminapi's writeError helper (a map, not a struct).
+// - TrustOrg / TrustStatus: mirror gateway/internal/trust, a
+// package outside tygo.yaml's adminapi scope, re-named for the
+// dashboard (trust.Org -> TrustOrg, trust.StatusResponse ->
+// TrustStatus).
+// - Chat / usage shapes (ChatMessage, TokenUsage, CacheDebug, …):
+// Bifrost pass-through fields that Go carries as
+// json.RawMessage (tygo emits `any`); typed here to the depth
+// the RunDetail drawer actually renders.
+//
+// Keep each block in lockstep with the Go source it names.
+
+// ─── enums validated Go-side as strings ─────────────────────────────
+
+// Window options the SPA exposes to the operator. Kept in lockstep
+// with the Go-side validation in observability.go > parseWindow.
+export type Window = "1h" | "6h" | "24h" | "7d" | "30d";
+
+// Bucket options for the histogram endpoints — same source-of-truth
+// note as Window.
+export type Bucket = "1m" | "5m" | "10m" | "1h" | "6h" | "1d";
+
+// Dimension values the histogram endpoint accepts. Phase 11 removed
+// `realm-id` — every row in a swarm's logs.db is implicitly for
+// that swarm's realm, and the realm is surfaced on the trust-status
+// card instead of as a per-row column.
+export type Dimension =
+ | "agent-name"
+ | "run-id"
+ | "session-id"
+ | "user-id";
+
+// ─── error envelope ─────────────────────────────────────────────────
+
+// Phase-7 error envelope (returned on 4xx/5xx). Mirrors the map
+// built by internal/adminapi's writeError helper.
+export interface ApiError {
+ error: {
+ code: string;
+ message: string;
+ };
+}
+
+// ─── trust registry (gateway/internal/trust) ────────────────────────
+
+// Trust-registry Org entry — mirrors gateway/internal/trust.Org.
+// Surfaced on the dashboard's Provenance card so an operator can
+// see which org's signature authorized a run, plus the pubkey /
+// issuer URL the plugin would verify against.
+export interface TrustOrg {
+ org_id: string;
+ pubkey: string;
+ issuer_url: string;
+ revocation_poll_seconds: number;
+ grace_pubkeys?: string[];
+ grace_until?: string;
+}
+
+// Trust-registry status — mirrors gateway/internal/trust.StatusResponse.
+// The Provenance card on RunDetail uses `realm_id` to show the
+// swarm's self-identity ("this run was processed by swarm w1"),
+// since phase 11 dropped the per-row realm-id metadata column.
+export interface TrustStatus {
+ claimed: boolean;
+ org_count: number;
+ orgs: string[];
+ seed_source: "" | "env" | "api";
+ last_modified: string;
+ /** Set on multi-swarm deployments; absent / empty on single-swarm. */
+ realm_id?: string;
+}
+
+// ─── Bifrost pass-through shapes (json.RawMessage on the Go side) ───
+
+// One message in a chat-style input_history / output_message.
+// Mirrors Bifrost's `schemas.ChatMessage` to the depth the drawer
+// renders: role, content (string OR an array of content blocks),
+// optional tool-call list (assistant) and tool_call_id (tool reply).
+// Everything is optional because providers vary in which fields
+// they populate, and the drawer falls back to JSON for anything it
+// doesn't recognize.
+export interface ChatMessage {
+ role?: string;
+ name?: string;
+ /** OpenAI/Anthropic-style: either a plain string or an array of
+ * typed content blocks. */
+ content?: string | ChatContentBlock[] | null;
+ /** Tool messages: which prior tool_call this is the result for. */
+ tool_call_id?: string;
+ /** Assistant tool calls. */
+ tool_calls?: ChatToolCall[];
+ /** Anthropic / OpenAI reasoning summaries. */
+ reasoning?: string;
+ refusal?: string;
+}
+
+export interface ChatContentBlock {
+ type: string;
+ text?: string;
+ refusal?: string;
+ /** Anthropic-style cache marker. When present on a block the
+ * provider charged this block as a cache write (or read on a
+ * subsequent call). */
+ cache_control?: { type?: string } | null;
+ cachePoint?: { type?: string } | null;
+ image_url?: unknown;
+ input_audio?: unknown;
+ file?: unknown;
+}
+
+export interface ChatToolCall {
+ id?: string;
+ type?: string;
+ function?: { name?: string; arguments?: string };
+}
+
+/** Provider-reported usage breakdown. Source: Bifrost
+ * `schemas.BifrostLLMUsage`. The `prompt_tokens_details` sub-
+ * object is where the cache split lives — Anthropic populates
+ * `cached_write_tokens` (prompt cache writes) and
+ * `cached_read_tokens`; OpenAI uses `cached_read_tokens` only. */
+export interface TokenUsage {
+ prompt_tokens?: number;
+ completion_tokens?: number;
+ total_tokens?: number;
+ prompt_tokens_details?: {
+ text_tokens?: number;
+ audio_tokens?: number;
+ image_tokens?: number;
+ cached_read_tokens?: number;
+ cached_write_tokens?: number;
+ cached_write_token_details?: {
+ cached_write_tokens_5m?: number;
+ cached_write_tokens_1h?: number;
+ };
+ };
+ completion_tokens_details?: {
+ reasoning_tokens?: number;
+ accepted_prediction_tokens?: number;
+ };
+ cost?: unknown;
+}
+
+/** Semantic cache verdict. Source: `schemas.BifrostCacheDebug`.
+ * Distinct from prompt-cache token splits (those live in
+ * TokenUsage above). Present only when a semantic cache plugin
+ * is configured. */
+export interface CacheDebug {
+ cache_hit: boolean;
+ cache_id?: string;
+ hit_type?: string;
+ requested_provider?: string;
+ requested_model?: string;
+ provider_used?: string;
+ model_used?: string;
+ input_tokens?: number;
+ threshold?: number;
+ similarity?: number;
+}
diff --git a/gateway/internal/adminapi/ui/src/api/queries.ts b/gateway/internal/adminapi/ui/src/api/queries.ts
index cd87946f0..42e77361f 100644
--- a/gateway/internal/adminapi/ui/src/api/queries.ts
+++ b/gateway/internal/adminapi/ui/src/api/queries.ts
@@ -25,13 +25,15 @@ import type {
SpendByAgentResponse,
SpendByAgentUserResponse,
SpendByUserResponse,
- TrustOrg,
- TrustStatus,
UserDetailResponse,
- Window,
+} from "./types";
+import type {
Bucket,
Dimension,
-} from "./types";
+ TrustOrg,
+ TrustStatus,
+ Window,
+} from "./manual";
// ─── /me ─────────────────────────────────────────────────────────────
// Fires once at boot, plus on tab refocus (Tanstack default). Cheap
diff --git a/gateway/internal/adminapi/ui/src/api/types.ts b/gateway/internal/adminapi/ui/src/api/types.ts
index f7bd8e94a..5997805ec 100644
--- a/gateway/internal/adminapi/ui/src/api/types.ts
+++ b/gateway/internal/adminapi/ui/src/api/types.ts
@@ -1,216 +1,394 @@
// Code generated by tygo. DO NOT EDIT.
//
-// To regenerate from the Go source of truth:
-// make tygo (or: tygo generate from gateway/)
-//
// Source: github.com/stakwork/stakgraph/gateway/internal/adminapi
-//
-// Phase 8 hand-maintains this file as a stop-gap until the tygo
-// codegen runs in CI. The shapes below MUST stay byte-equivalent to
-// the Go structs they mirror (LoginResponse, MeResponse,
-// SpendByAgentResponse, SpendByUserResponse, HistogramCostResponse,
-// RunDetailResponse and friends). Any drift fails the matching
-// adminapi Go tests on next run.
-
+// Regenerate with `make tygo`; CI enforces via `make tygo-check`.
+// Hand-maintained companions (unions, trust mirrors) live in
+// manual.ts.
/* eslint-disable */
+//////////
+// source: budgets.go
+
+/**
+ * AgentBudgetResponse is the wire shape for /_plugin/agents/:name/budget.
+ * Phase-8.5 read-only view — phase 9 grows the matching PUT/DELETE
+ * mutations on `/_plugin/config/agent_budgets/:name`.
+ * `CapUSD` and `Window` come from the plugin config (YAML / Redis
+ * override merged); `SpentUSD` comes from the live phase-6 Redis
+ * accumulator `bifrost:cost:agent::`. Either source
+ * being absent is fine — the response surfaces nil so the UI can
+ * render "no budget" or "no spend yet" rather than misleading zeros.
+ */
+export interface AgentBudgetResponse {
+ agent_name: string;
+ /**
+ * Cap is the configured maximum spend for the current window.
+ * Null when the agent has no entry in `agent_budgets`.
+ */
+ cap_usd?: number /* float64 */;
+ /**
+ * Window is the Bifrost duration string the cap applies to
+ * (e.g. "1d", "1h"). Empty when no cap is set.
+ */
+ window: string;
+ /**
+ * PeriodStart / PeriodEnd bracket the active bucket. Both are
+ * RFC3339 strings, UTC. Phase 8 only renders the start; future
+ * "resets in: 23h" badges can read end.
+ */
+ period_start?: string;
+ period_end?: string;
+ /**
+ * SpentUSD is the running total against the cap for the
+ * current bucket. 0 (not null) when the bucket key simply
+ * doesn't exist yet — that's a real-world "fresh window, no
+ * calls yet" state, not a missing-data state.
+ */
+ spent_usd: number /* float64 */;
+ /**
+ * RemainingUSD = max(cap - spent, 0). Null when cap is null.
+ */
+ remaining_usd?: number /* float64 */;
+ /**
+ * Ratio = spent / cap, clamped to [0, 1+] (we don't clamp the
+ * upper bound so a runaway "150%" reads honestly). Null when
+ * cap is null.
+ */
+ ratio?: number /* float64 */;
+}
+
+//////////
+// source: catalog.go
+
+/**
+ * AgentCatalogResponse is the merged view across all sources for one
+ * agent — what the agent is _made of_ (prompts/tools/skills), as
+ * opposed to the budget view's what it's _allowed to spend_.
+ */
+export interface AgentCatalogResponse {
+ name: string;
+ display_name?: string;
+ description?: string;
+ default_model?: string;
+ sources: string[];
+ prompts: CatalogPrompt[];
+ tools: CatalogTool[];
+ skills: CatalogSkill[];
+}
+/**
+ * CatalogAgentSummary is one row of the catalog list — enough to merge
+ * the registry into the spend-derived /agents table without pulling
+ * every agent's full prompt/tool/skill bodies. Counts let the list
+ * render "📄 2 · 🔧 5 · ✦ 1" badges cheaply.
+ */
+export interface CatalogAgentSummary {
+ name: string;
+ display_name?: string;
+ description?: string;
+ default_model?: string;
+ sources: string[];
+ prompts: number /* int */;
+ tools: number /* int */;
+ skills: number /* int */;
+ updated_at: string;
+}
+/**
+ * CatalogListResponse is the whole registry — every HiveAgent node,
+ * traffic or not. The UI unions this with spend-by-agent so seeded
+ * agents that have never been invoked still appear.
+ */
+export interface CatalogListResponse {
+ agents: CatalogAgentSummary[];
+}
+/**
+ * CatalogPrompt is one prompt linked to an agent. name/body come from
+ * the shared `:Prompt` node the agent links to; source/updated_at come
+ * from the HAS_PROMPT relationship (which system wired it, and when).
+ */
+export interface CatalogPrompt {
+ name: string;
+ body: string;
+ /**
+ * Role is the prompt's slot for this agent — "SYSTEM" or "USER"
+ * (the main/task prompt). Stored on the HAS_PROMPT relationship;
+ * empty when the wiring source didn't classify it.
+ */
+ role?: string;
+ source: string;
+ updated_at: string;
+}
+export interface CatalogTool {
+ name: string;
+ description: string;
+ schema?: any /* json.RawMessage */;
+ source: string;
+ version?: string;
+ /**
+ * Enabled is the per-swarm operator toggle, mirroring skills:
+ * seeded enabled, flipped in the dashboard, preserved across
+ * Hive re-seeds. Legacy nodes with no value read back as true.
+ */
+ enabled: boolean;
+ updated_at: string;
+}
+export interface CatalogSkill {
+ name: string;
+ description: string;
+ source: string;
+ version?: string;
+ /**
+ * Enabled is the per-swarm operator toggle. Skills seed enabled by
+ * default; an operator flips this in the dashboard and the gateway
+ * preserves it across Hive re-seeds (a push only refreshes the
+ * palette + metadata, never the toggle). Legacy nodes with no
+ * stored value read back as true (coalesce on the read query).
+ */
+ enabled: boolean;
+ updated_at: string;
+}
+
+//////////
+// source: evals.go
+
+/**
+ * EvalSetSummary is one row of an agent's eval-set list.
+ */
+export interface EvalSetSummary {
+ ref_id: string;
+ name?: string;
+ description?: string;
+ requirements: number /* int */;
+}
+/**
+ * AgentEvalsResponse is the agent-detail Evals tab payload — the sets
+ * linked to this agent via HAS_EVAL_SET.
+ */
+export interface AgentEvalsResponse {
+ agent: string;
+ sets: EvalSetSummary[];
+}
+/**
+ * EvalTriggerSummary is one captured trigger under a requirement, plus
+ * the outcome of its most-recent run (nil-ish fields when never run).
+ */
+export interface EvalTriggerSummary {
+ ref_id: string;
+ agent?: string;
+ source?: string;
+ environment?: string;
+ change_type?: string;
+ last_result?: string; // "pass" / "fail" / ""
+ last_score?: number /* float64 */;
+ last_notes?: string;
+ last_attempt?: number /* int */;
+}
+/**
+ * EvalRequirementDetail is a requirement with its triggers.
+ */
+export interface EvalRequirementDetail {
+ ref_id: string;
+ name?: string;
+ description?: string;
+ prompt_snippet?: string;
+ order: number /* int */;
+ triggers: EvalTriggerSummary[];
+}
+/**
+ * EvalSetDetailResponse is the expanded view of one set.
+ */
+export interface EvalSetDetailResponse {
+ ref_id: string;
+ name?: string;
+ description?: string;
+ requirements: EvalRequirementDetail[];
+}
+/**
+ * EvalRefResponse is the create/link acknowledgement. Exported so
+ * tygo emits the TS binding the SPA's eval mutations decode.
+ */
+export interface EvalRefResponse {
+ ref_id: string;
+ linked?: boolean;
+}
+
+//////////
+// source: hivecallback.go
+
+
+//////////
+// source: login.go
+
+/**
+ * LoginResponse is the JSON body returned by `POST /_plugin/login`
+ * on success. The SPA stashes the username so the topbar can render
+ * it without an extra `/me` round-trip.
+ * Exported because tygo emits TS bindings from this declaration
+ * (see gateway/tygo.yaml); rename in lockstep with the frontend.
+ */
export interface LoginResponse {
user: string;
}
-
+/**
+ * MeResponse is `GET /_plugin/me` — the SPA's boot probe to decide
+ * "am I authenticated?". Unix timestamps (seconds) keep the wire
+ * format compact and language-agnostic; the frontend converts to
+ * Date once.
+ */
export interface MeResponse {
user: string;
- iat: number;
- last_seen: number;
+ iat: number /* int64 */;
+ last_seen: number /* int64 */;
}
+//////////
+// source: logstore_client.go
+
+
+//////////
+// source: observability.go
+
+/**
+ * AgentSpend is one row of /_plugin/spend/by-agent.
+ */
export interface AgentSpend {
agent_name: string;
- total_cost: number;
- total_tokens: number;
- request_count: number;
+ total_cost: number /* float64 */;
+ total_tokens: number /* int64 */;
+ request_count: number /* int64 */;
}
-
+/**
+ * SpendByAgentResponse is the envelope for /_plugin/spend/by-agent.
+ */
export interface SpendByAgentResponse {
window: string;
results: AgentSpend[];
}
-
+/**
+ * UserSpend is one row of /_plugin/spend/by-user.
+ */
export interface UserSpend {
user_id: string;
user_name: string;
- total_cost: number;
- total_tokens: number;
- request_count: number;
+ total_cost: number /* float64 */;
+ total_tokens: number /* int64 */;
+ request_count: number /* int64 */;
}
-
+/**
+ * SpendByUserResponse is the envelope for /_plugin/spend/by-user.
+ */
export interface SpendByUserResponse {
window: string;
results: UserSpend[];
}
-
-// Per-provider slice inside an AgentUserSpend row. Lets the canvas
-// drive gateway→provider edge widths from real spend; same Bifrost
-// call that computes the (agent × user) rollup also fills these,
-// so no extra round-trips.
+/**
+ * ProviderSpend is the per-provider slice carried inside an
+ * AgentUserSpend row. Lets the canvas drive gateway→provider edge
+ * widths (and the provider drawer) from real spend without a second
+ * round-trip — the data is already on every Bifrost log row, so
+ * surfacing it costs only a second bucket in the same pass.
+ */
export interface ProviderSpend {
provider: string;
- total_cost: number;
- request_count: number;
-}
-
-// One row of /_plugin/spend/by-agent-user — the (agent × user) fan-out
-// the Canvas page uses to render one box per pairing without N round
-// trips. Rows missing either dim are excluded server-side.
-//
-// `providers` sums to `total_cost` / `request_count` and is sorted by
-// cost desc, with provider name as a stable tiebreaker.
+ total_cost: number /* float64 */;
+ request_count: number /* int64 */;
+}
+/**
+ * AgentUserSpend is one row of /_plugin/spend/by-agent-user — the
+ * fan-out crossing of (agent-name × user-id) so the flowchart UI can
+ * render one box per pairing in a single round-trip. Rows missing
+ * either dim are excluded (same policy as by-agent / by-user).
+ * `Providers` is the breakdown across providers for this pairing.
+ * Sums of `Providers[*].TotalCost` and `Providers[*].RequestCount`
+ * equal `TotalCost` and `RequestCount` — the breakdown is additive
+ * to the top-line totals, not a replacement.
+ */
export interface AgentUserSpend {
agent_name: string;
user_id: string;
user_name: string;
- total_cost: number;
- total_tokens: number;
- request_count: number;
+ total_cost: number /* float64 */;
+ total_tokens: number /* int64 */;
+ request_count: number /* int64 */;
providers: ProviderSpend[];
}
-
+/**
+ * SpendByAgentUserResponse is the envelope for
+ * /_plugin/spend/by-agent-user.
+ */
export interface SpendByAgentUserResponse {
window: string;
results: AgentUserSpend[];
}
-
+/**
+ * HistogramPoint is one (timestamp, cost) datum in a per-dimension
+ * series. Timestamp is the bucket's start time in RFC3339.
+ */
export interface HistogramPoint {
ts: string;
- cost: number;
+ cost: number /* float64 */;
}
-
+/**
+ * HistogramSeries is one line on a stacked-area chart.
+ */
export interface HistogramSeries {
dimension_value: string;
points: HistogramPoint[];
}
-
+/**
+ * HistogramCostResponse is the envelope for
+ * /_plugin/histogram/cost.
+ */
export interface HistogramCostResponse {
- bucket_size_seconds: number;
+ bucket_size_seconds: number /* int64 */;
dimension: string;
series: HistogramSeries[];
}
-
+/**
+ * RunLogEntry is one row inside a RunDetailResponse's `logs` field.
+ * A trimmed view of Bifrost's Log — phase 8 only renders the columns
+ * the call-log table actually shows.
+ */
export interface RunLogEntry {
id: string;
timestamp: string;
provider: string;
model: string;
status: string;
- cost: number;
- latency: number;
- metadata: Record;
+ cost: number /* float64 */;
+ latency: number /* float64 */;
+ metadata: { [key: string]: string};
}
-
+/**
+ * RunStats is the aggregate-card summary at the top of /runs/:id.
+ */
export interface RunStats {
- total_requests: number;
- total_cost: number;
- total_tokens: number;
+ total_requests: number /* int64 */;
+ total_cost: number /* float64 */;
+ total_tokens: number /* int64 */;
}
-
+/**
+ * RunDetailResponse is the envelope for /_plugin/runs/:run_id.
+ */
export interface RunDetailResponse {
run_id: string;
logs: RunLogEntry[];
stats: RunStats;
}
-
-// One message in a chat-style input_history / output_message.
-// Mirrors Bifrost's `schemas.ChatMessage` to the depth the drawer
-// renders: role, content (string OR an array of content blocks),
-// optional tool-call list (assistant) and tool_call_id (tool reply).
-// Everything is optional because providers vary in which fields
-// they populate, and the drawer falls back to JSON for anything it
-// doesn't recognize.
-export interface ChatMessage {
- role?: string;
- name?: string;
- /** OpenAI/Anthropic-style: either a plain string or an array of
- * typed content blocks. */
- content?: string | ChatContentBlock[] | null;
- /** Tool messages: which prior tool_call this is the result for. */
- tool_call_id?: string;
- /** Assistant tool calls. */
- tool_calls?: ChatToolCall[];
- /** Anthropic / OpenAI reasoning summaries. */
- reasoning?: string;
- refusal?: string;
-}
-
-export interface ChatContentBlock {
- type: string;
- text?: string;
- refusal?: string;
- /** Anthropic-style cache marker. When present on a block the
- * provider charged this block as a cache write (or read on a
- * subsequent call). */
- cache_control?: { type?: string } | null;
- cachePoint?: { type?: string } | null;
- image_url?: unknown;
- input_audio?: unknown;
- file?: unknown;
-}
-
-export interface ChatToolCall {
- id?: string;
- type?: string;
- function?: { name?: string; arguments?: string };
-}
-
-/** Provider-reported usage breakdown. Source: Bifrost
- * `schemas.BifrostLLMUsage`. The `prompt_tokens_details` sub-
- * object is where the cache split lives — Anthropic populates
- * `cached_write_tokens` (prompt cache writes) and
- * `cached_read_tokens`; OpenAI uses `cached_read_tokens` only. */
-export interface TokenUsage {
- prompt_tokens?: number;
- completion_tokens?: number;
- total_tokens?: number;
- prompt_tokens_details?: {
- text_tokens?: number;
- audio_tokens?: number;
- image_tokens?: number;
- cached_read_tokens?: number;
- cached_write_tokens?: number;
- cached_write_token_details?: {
- cached_write_tokens_5m?: number;
- cached_write_tokens_1h?: number;
- };
- };
- completion_tokens_details?: {
- reasoning_tokens?: number;
- accepted_prediction_tokens?: number;
- };
- cost?: unknown;
-}
-
-/** Semantic cache verdict. Source: `schemas.BifrostCacheDebug`.
- * Distinct from prompt-cache token splits (those live in
- * TokenUsage above). Present only when a semantic cache plugin
- * is configured. */
-export interface CacheDebug {
- cache_hit: boolean;
- cache_id?: string;
- hit_type?: string;
- requested_provider?: string;
- requested_model?: string;
- provider_used?: string;
- model_used?: string;
- input_tokens?: number;
- threshold?: number;
- similarity?: number;
-}
-
-// CallDetailResponse — /_plugin/runs/:run_id/calls/:call_id.
-//
-// The full body of one LLM call: same fields as a list row, plus
-// the heavy JSON columns Bifrost strips from /api/logs. Chat-shaped
-// fields (input_history / output_message) are typed so the drawer
-// can render bubble UIs; everything else stays opaque.
+/**
+ * CallDetailResponse is the envelope for
+ * /_plugin/runs/:run_id/calls/:call_id — the full request/response
+ * content for a single LLM call, fetched on-demand when the
+ * operator clicks a row in the RunDetail call log.
+ * Run-scoping rationale: we verify that the fetched log's
+ * metadata.run-id matches the URL's run_id before returning, so a
+ * caller can't enumerate other workspaces' logs by guessing IDs.
+ * Bifrost's /api/logs/{id} doesn't do this check itself (it just
+ * looks up by primary key), so the plugin enforces it.
+ * Body fields are pass-through json.RawMessage from Bifrost — the
+ * SPA pretty-prints them; the plugin doesn't introspect them. This
+ * keeps the schema coupling minimal: new fields upstream surface in
+ * the UI without code changes here.
+ */
export interface CallDetailResponse {
id: string;
run_id: string;
@@ -218,251 +396,154 @@ export interface CallDetailResponse {
provider: string;
model: string;
status: string;
- cost: number;
- latency: number;
+ cost: number /* float64 */;
+ latency: number /* float64 */;
customer_id: string;
- metadata: Record;
-
+ metadata: { [key: string]: string};
+ /**
+ * Per-request descriptors stamped by Bifrost. `stop_reason`
+ * tells the operator why the model stopped (stop, length,
+ * content_filter, tool_calls, refusal). `stream` flags
+ * streaming responses (which lack a single output_message and
+ * surface their content via Bifrost's stream chunk replay).
+ * Retries / fallback_index are zero on the happy path; non-zero
+ * means Bifrost had to retry the call or fall back to a
+ * different provider, which is useful provenance.
+ */
stop_reason?: string;
stream: boolean;
- number_of_retries: number;
- fallback_index: number;
-
- token_usage?: TokenUsage;
- cache_debug?: CacheDebug;
-
- input_history?: ChatMessage[];
- output_message?: ChatMessage;
- params?: unknown;
- tools?: unknown;
- error_details?: unknown;
+ number_of_retries: number /* int */;
+ fallback_index: number /* int */;
+ /**
+ * TokenUsage is the provider-reported usage breakdown
+ * (BifrostLLMUsage). Includes prompt/completion/total totals
+ * plus cached read/write splits (Anthropic prompt-cache,
+ * OpenAI cached_tokens), audio/image token counts, and a
+ * per-call cost record. Pass-through JSON — the SPA introspects
+ * it. Bifrost's row-level prompt_tokens / completion_tokens /
+ * total_tokens columns are denormalized helpers tagged
+ * `json:"-"`, so this is the only place token data is on the
+ * wire.
+ */
+ token_usage?: any /* json.RawMessage */;
+ /**
+ * CacheDebug carries Bifrost's *semantic* cache verdict for
+ * this call (hit/miss + similarity score). Distinct from
+ * prompt-cache tokens, which live in TokenUsage above. Absent
+ * when no semantic cache is configured for this swarm.
+ */
+ cache_debug?: any /* json.RawMessage */;
+ /**
+ * All optional; missing on failures, realtime turns, or rows
+ * recorded before a given column existed.
+ */
+ input_history?: any /* json.RawMessage */;
+ output_message?: any /* json.RawMessage */;
+ params?: any /* json.RawMessage */;
+ tools?: any /* json.RawMessage */;
+ error_details?: any /* json.RawMessage */;
raw_request?: string;
raw_response?: string;
content_summary?: string;
}
-// One row of UserDetailResponse.AgentsUsed — which agents the user
-// invoked in the window, and how much each cost.
-export interface UserAgentUsage {
- agent_name: string;
- total_cost: number;
- request_count: number;
- last_seen?: string;
-}
+//////////
+// source: ratelimit.go
-// One row of UserDetailResponse.RecentRuns — links into RunDetail.
-export interface UserRunSummary {
- run_id: string;
- agent_name: string;
- total_cost: number;
- request_count: number;
- first_seen?: string;
- last_seen?: string;
-}
-// /_plugin/users/:id response.
-export interface UserDetailResponse {
- user_id: string;
- window: string;
- total_cost: number;
- request_count: number;
- agents_used: UserAgentUsage[];
- recent_runs: UserRunSummary[];
- first_seen?: string;
- last_seen?: string;
-}
+//////////
+// source: server.go
+/*
+Package adminapi is the gateway plugin's in-process HTTP server,
+hosting the `/_plugin/*` route namespace.
-// Trust-registry Org entry — mirrors gateway/internal/trust.Org.
-// Surfaced on the dashboard's Provenance card so an operator can
-// see which org's signature authorized a run, plus the pubkey /
-// issuer URL the plugin would verify against.
-export interface TrustOrg {
- org_id: string;
- pubkey: string;
- issuer_url: string;
- revocation_poll_seconds: number;
- grace_pubkeys?: string[];
- grace_until?: string;
-}
+The wrapper binary (gateway/wrapper) reverse-proxies traffic on
+`/_plugin/*` to this server (loopback only). Why a separate server
+instead of routes registered through Bifrost's router: Bifrost
+plugins (.so) cannot register arbitrary HTTP routes through
+Bifrost's own router, and we also want routes that aren't behind
+Bifrost's AuthMiddleware so Hive can bootstrap on a fresh swarm.
-// Trust-registry status — mirrors gateway/internal/trust.StatusResponse.
-// The Provenance card on RunDetail uses `realm_id` to show the
-// swarm's self-identity ("this run was processed by swarm w1"),
-// since phase 11 dropped the per-row realm-id metadata column.
-export interface TrustStatus {
- claimed: boolean;
- org_count: number;
- orgs: string[];
- seed_source: "" | "env" | "api";
- last_modified: string;
- /** Set on multi-swarm deployments; absent / empty on single-swarm. */
- realm_id?: string;
-}
+Lifecycle
+---------
+ - Start() is called from the plugin's Init().
+ - Stop() is called from the plugin's Cleanup() with a small grace
+ period.
-// Per-agent budget (phase-8.5). Cap and the derived fields can be
-// null when no budget is configured for the agent — the UI renders
-// "no budget" rather than "$0".
-export interface AgentBudgetResponse {
- agent_name: string;
- cap_usd: number | null;
- window: string;
- period_start?: string;
- period_end?: string;
- spent_usd: number;
- remaining_usd: number | null;
- ratio: number | null;
-}
+One server per process. Re-calling Start() is a no-op.
+*/
-// ─── agent catalog ──────────────────────────────────────────────────
-// Mirrors gateway/internal/adminapi/catalog.go. The catalog answers
-// "what is this agent _made of_?" (prompts/tools/skills), as opposed to
-// the budget view's "what is it _allowed to spend_?". Sourced from the
-// neo4j Hive* catalog subgraph via GET /_plugin/agents/:name/catalog.
-// A prompt linked to an agent. name/body come from the shared `:Prompt`
-// node the agent links to (authored by the Stakwork prompt workflow);
-// source/updated_at come from the HAS_PROMPT relationship.
-export interface CatalogPrompt {
- name: string;
- body: string;
- /** Prompt slot for this agent: "SYSTEM" or "USER" (the main/task
- * prompt). Absent when the wiring source didn't classify it. */
- role?: string;
- source: string;
- updated_at: string;
-}
-
-export interface CatalogTool {
- name: string;
- description: string;
- /** JSON parameter schema, passed through opaque — the UI renders it
- * as pretty-printed JSON. Absent when the source didn't supply one. */
- schema?: unknown;
- source: string;
- version?: string;
- /** Per-swarm operator toggle. Seeded enabled; preserved across
- * Hive re-seeds. Flip via PATCH /_plugin/agents/:name/tools. */
- enabled: boolean;
- updated_at: string;
-}
-
-export interface CatalogSkill {
- name: string;
- description: string;
- source: string;
- version?: string;
- /** Per-swarm operator toggle. Seeded enabled; preserved across
- * Hive re-seeds. Flip via PATCH /_plugin/agents/:name/skills. */
- enabled: boolean;
- updated_at: string;
-}
-
-// One row of the catalog list (GET /_plugin/agents/catalog) — identity
-// + child counts, enough to merge the registry into the spend-derived
-// /agents table without pulling every prompt/tool/skill body.
-export interface CatalogAgentSummary {
- name: string;
- display_name?: string;
- description?: string;
- default_model?: string;
- sources: string[];
- prompts: number;
- tools: number;
- skills: number;
- updated_at: string;
-}
-
-// The whole registry — every catalog agent, traffic or not.
-export interface CatalogListResponse {
- agents: CatalogAgentSummary[];
-}
+//////////
+// source: session.go
-// Merged catalog view across all contributing sources for one agent.
-export interface AgentCatalogResponse {
- name: string;
- display_name?: string;
- description?: string;
- /** Default LLM used for this agent (model shortcut or full id). */
- default_model?: string;
- sources: string[];
- prompts: CatalogPrompt[];
- tools: CatalogTool[];
- skills: CatalogSkill[];
-}
-// ─── Evals (agent-detail tab) ───────────────────────────────────────
-// Mirror of the Go structs in internal/adminapi/evals.go. Eval sets are
-// Jarvis-authored nodes surfaced under an agent via HAS_EVAL_SET; the
-// gateway reads them from neo4j and delegates writes/runs to Hive.
+//////////
+// source: tickets.go
-export interface EvalSetSummary {
- ref_id: string;
- name?: string;
- description?: string;
- requirements: number;
+/**
+ * TicketResponse is the JSON body returned by POST /_plugin/auth/ticket.
+ * Hive embeds the ticket in the iframe src as `?ticket=`.
+ */
+export interface TicketResponse {
+ ticket: string;
+ expires_in: number /* int */; // seconds; mirrors ticketTTL
}
-export interface AgentEvalsResponse {
- agent: string;
- sets: EvalSetSummary[];
-}
+//////////
+// source: trust.go
-export interface EvalTriggerSummary {
- ref_id: string;
- agent?: string;
- source?: string;
- environment?: string;
- change_type?: string;
- last_result?: string; // "pass" | "fail" | ""
- last_score?: number;
- last_notes?: string;
- last_attempt?: number;
-}
-export interface EvalRequirementDetail {
- ref_id: string;
- name?: string;
- description?: string;
- prompt_snippet?: string;
- order: number;
- triggers: EvalTriggerSummary[];
-}
+//////////
+// source: users.go
-export interface EvalSetDetailResponse {
- ref_id: string;
- name?: string;
- description?: string;
- requirements: EvalRequirementDetail[];
+/**
+ * UserAgentUsage is one row in `UserDetailResponse.AgentsUsed` —
+ * a per-agent rollup scoped to this user's traffic in the window.
+ */
+export interface UserAgentUsage {
+ agent_name: string;
+ total_cost: number /* float64 */;
+ request_count: number /* int64 */;
+ last_seen?: string;
}
-
-// Acknowledgement for create/link (ref_id of the set/requirement).
-export interface EvalRefResponse {
- ref_id: string;
- linked?: boolean;
+/**
+ * UserRunSummary is one row in `UserDetailResponse.RecentRuns`.
+ * Phase 8 keeps this lightweight — the dashboard renders cost +
+ * agent + first/last-seen and links into RunDetail for the full
+ * call log.
+ */
+export interface UserRunSummary {
+ run_id: string;
+ agent_name: string;
+ total_cost: number /* float64 */;
+ request_count: number /* int64 */;
+ first_seen?: string;
+ last_seen?: string;
}
-
-// Phase-7 error envelope (returned on 4xx/5xx).
-export interface ApiError {
- error: {
- code: string;
- message: string;
- };
+/**
+ * UserDetailResponse is the wire shape for /_plugin/users/:id.
+ * Composition
+ * -----------
+ * Everything is derived from a single paged scan of `logs.db`
+ * filtered by `metadata.user-id = ` (with a fallback to the
+ * indexed `customer_id` column on Bifrost's logs table, which
+ * equals the user-id per the v2 invariant). One round-trip to
+ * Bifrost, one in-memory aggregation pass; the dashboard renders
+ * the result without further fan-out.
+ * Once phase 6's PostLLMHook fills Redis cost accumulators, the
+ * `total_cost` field could be sourced from the Redis hash instead
+ * of summing logs — same number, less work. Phase 8 doesn't take
+ * that shortcut yet because the Redis bucket is per-(agent, day)
+ * not per-user.
+ */
+export interface UserDetailResponse {
+ user_id: string;
+ window: string;
+ total_cost: number /* float64 */;
+ request_count: number /* int64 */;
+ agents_used: UserAgentUsage[];
+ recent_runs: UserRunSummary[];
+ first_seen?: string;
+ last_seen?: string;
}
-
-// Window options the SPA exposes to the operator. Kept in lockstep
-// with the Go-side validation in observability.go > parseWindow.
-export type Window = "1h" | "6h" | "24h" | "7d" | "30d";
-
-// Bucket options for the histogram endpoints — same source-of-truth
-// note as Window.
-export type Bucket = "1m" | "5m" | "10m" | "1h" | "6h" | "1d";
-
-// Dimension values the histogram endpoint accepts. Phase 11 removed
-// `realm-id` — every row in a swarm's logs.db is implicitly for
-// that swarm's realm, and the realm is surfaced on the trust-status
-// card instead of as a per-row column.
-export type Dimension =
- | "agent-name"
- | "run-id"
- | "session-id"
- | "user-id";
diff --git a/gateway/internal/adminapi/ui/src/api/window.ts b/gateway/internal/adminapi/ui/src/api/window.ts
index 57e45d414..cff5b3979 100644
--- a/gateway/internal/adminapi/ui/src/api/window.ts
+++ b/gateway/internal/adminapi/ui/src/api/window.ts
@@ -2,7 +2,7 @@
// because the backend's parseWindow is the authority on this set; if
// it grows a new option, this mapping needs to grow with it.
-import type { Window } from "./types";
+import type { Window } from "./manual";
export function windowToSeconds(w: Window): number {
switch (w) {
diff --git a/gateway/internal/adminapi/ui/src/components/controls/WindowPicker.tsx b/gateway/internal/adminapi/ui/src/components/controls/WindowPicker.tsx
index 7f0d26435..1d5aca09f 100644
--- a/gateway/internal/adminapi/ui/src/components/controls/WindowPicker.tsx
+++ b/gateway/internal/adminapi/ui/src/components/controls/WindowPicker.tsx
@@ -3,7 +3,7 @@
// set stays consistent (and stays in lockstep with the Go-side
// parseWindow whitelist).
-import type { Window } from "../../api/types";
+import type { Window } from "../../api/manual";
const OPTIONS: Window[] = ["1h", "24h", "7d", "30d"];
diff --git a/gateway/internal/adminapi/ui/src/pages/AgentDetail.tsx b/gateway/internal/adminapi/ui/src/pages/AgentDetail.tsx
index 20120ed98..1d6817927 100644
--- a/gateway/internal/adminapi/ui/src/pages/AgentDetail.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/AgentDetail.tsx
@@ -31,7 +31,8 @@ import {
useToggleSkill,
useToggleTool,
} from "../api/queries";
-import type { HistogramCostResponse, Window } from "../api/types";
+import type { HistogramCostResponse } from "../api/types";
+import type { Window } from "../api/manual";
import { windowToSeconds } from "../api/window";
import { EvalsView } from "./EvalsView";
diff --git a/gateway/internal/adminapi/ui/src/pages/Agents.tsx b/gateway/internal/adminapi/ui/src/pages/Agents.tsx
index cb0c4e044..834365041 100644
--- a/gateway/internal/adminapi/ui/src/pages/Agents.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/Agents.tsx
@@ -15,7 +15,7 @@ import {
useAgentCatalogList,
useSpendByAgent,
} from "../api/queries";
-import type { Window } from "../api/types";
+import type { Window } from "../api/manual";
// A merged agent row: spend metrics (zeroed when registry-only) plus
// catalog identity/counts (absent when traffic-only).
diff --git a/gateway/internal/adminapi/ui/src/pages/Canvas.tsx b/gateway/internal/adminapi/ui/src/pages/Canvas.tsx
index e40b4d839..99adb7c48 100644
--- a/gateway/internal/adminapi/ui/src/pages/Canvas.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/Canvas.tsx
@@ -35,7 +35,8 @@ import {
useTrustOrg,
useTrustStatus,
} from "../api/queries";
-import type { AgentUserSpend, TrustOrg, Window } from "../api/types";
+import type { AgentUserSpend } from "../api/types";
+import type { TrustOrg, Window } from "../api/manual";
import { WindowPicker } from "../components/controls/WindowPicker";
import { canvasTheme, PROVIDER_DISPLAY, providerIcon } from "./canvasTheme";
diff --git a/gateway/internal/adminapi/ui/src/pages/Dashboard.tsx b/gateway/internal/adminapi/ui/src/pages/Dashboard.tsx
index 94be8c10c..6e970a5bb 100644
--- a/gateway/internal/adminapi/ui/src/pages/Dashboard.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/Dashboard.tsx
@@ -17,7 +17,8 @@ import {
useSpendByAgent,
useSpendByUser,
} from "../api/queries";
-import type { AgentSpend, UserSpend, Window } from "../api/types";
+import type { AgentSpend, UserSpend } from "../api/types";
+import type { Window } from "../api/manual";
import { windowToSeconds } from "../api/window";
// LLM call costs can be fractions of a cent; clamping to 2 decimals
diff --git a/gateway/internal/adminapi/ui/src/pages/People.tsx b/gateway/internal/adminapi/ui/src/pages/People.tsx
index 091cdb976..7173254c0 100644
--- a/gateway/internal/adminapi/ui/src/pages/People.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/People.tsx
@@ -14,7 +14,8 @@ import { WindowPicker } from "../components/controls/WindowPicker";
import { UserIcon } from "../components/icons";
import { getErrorMessage } from "../api/client";
import { useSpendByUser } from "../api/queries";
-import type { UserSpend, Window } from "../api/types";
+import type { UserSpend } from "../api/types";
+import type { Window } from "../api/manual";
const fmtUSD = (v: number) => {
if (v === 0) return "$0.00";
diff --git a/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx b/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx
index 4a4a92853..4f9e7820a 100644
--- a/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/RunDetail.tsx
@@ -12,16 +12,15 @@ import { DataTable } from "../components/tables/DataTable";
import { BotIcon, UserIcon } from "../components/icons";
import { getErrorMessage } from "../api/client";
import { useRunCall, useRunDetail, useTrustOrg, useTrustStatus } from "../api/queries";
+import type { CallDetailResponse, RunLogEntry } from "../api/types";
import type {
CacheDebug,
- CallDetailResponse,
ChatContentBlock,
ChatMessage,
ChatToolCall,
- RunLogEntry,
TokenUsage,
TrustOrg,
-} from "../api/types";
+} from "../api/manual";
interface Props {
runID: string;
diff --git a/gateway/internal/adminapi/ui/src/pages/UserDetail.tsx b/gateway/internal/adminapi/ui/src/pages/UserDetail.tsx
index d271ebf68..f026177a8 100644
--- a/gateway/internal/adminapi/ui/src/pages/UserDetail.tsx
+++ b/gateway/internal/adminapi/ui/src/pages/UserDetail.tsx
@@ -22,7 +22,8 @@ import { WindowPicker } from "../components/controls/WindowPicker";
import { BotIcon, UserIcon } from "../components/icons";
import { getErrorMessage } from "../api/client";
import { useHistogramCost, useUserDetail } from "../api/queries";
-import type { UserAgentUsage, UserRunSummary, Window } from "../api/types";
+import type { UserAgentUsage, UserRunSummary } from "../api/types";
+import type { Window } from "../api/manual";
import { windowToSeconds } from "../api/window";
interface Props {
diff --git a/gateway/internal/adminapi/ui/tsconfig.tsbuildinfo b/gateway/internal/adminapi/ui/tsconfig.tsbuildinfo
index be7c070c3..d42130d52 100644
--- a/gateway/internal/adminapi/ui/tsconfig.tsbuildinfo
+++ b/gateway/internal/adminapi/ui/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/queries.ts","./src/api/types.ts","./src/api/window.ts","./src/components/emptystate.tsx","./src/components/errorboundary.tsx","./src/components/icons.tsx","./src/components/charts/costhistogram.tsx","./src/components/charts/uplotchart.tsx","./src/components/controls/windowpicker.tsx","./src/components/layout/shell.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/topbar.tsx","./src/components/tables/datatable.tsx","./src/pages/agentdetail.tsx","./src/pages/agents.tsx","./src/pages/canvas.tsx","./src/pages/dashboard.tsx","./src/pages/login.tsx","./src/pages/notfound.tsx","./src/pages/people.tsx","./src/pages/rundetail.tsx","./src/pages/userdetail.tsx","./src/pages/canvastheme.ts"],"version":"5.6.3"}
\ No newline at end of file
+{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/manual.ts","./src/api/queries.ts","./src/api/types.ts","./src/api/window.ts","./src/components/emptystate.tsx","./src/components/errorboundary.tsx","./src/components/icons.tsx","./src/components/charts/costhistogram.tsx","./src/components/charts/uplotchart.tsx","./src/components/controls/windowpicker.tsx","./src/components/layout/shell.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/topbar.tsx","./src/components/tables/datatable.tsx","./src/pages/agentdetail.tsx","./src/pages/agents.tsx","./src/pages/canvas.tsx","./src/pages/dashboard.tsx","./src/pages/evalsview.tsx","./src/pages/login.tsx","./src/pages/notfound.tsx","./src/pages/people.tsx","./src/pages/rundetail.tsx","./src/pages/userdetail.tsx","./src/pages/canvastheme.ts"],"version":"5.6.3"}
\ No newline at end of file
diff --git a/gateway/tygo.yaml b/gateway/tygo.yaml
index 6e104565c..7ac359e51 100644
--- a/gateway/tygo.yaml
+++ b/gateway/tygo.yaml
@@ -11,7 +11,9 @@
# Source-of-truth: every type the SPA decodes is declared in
# gateway/internal/adminapi/ as an exported Go struct with `json:`
# tags. tygo translates those into TS interfaces; no hand-editing
-# of types.ts.
+# of types.ts. Types with no adminapi Go struct (string-literal
+# unions, the trust-package mirrors, Bifrost pass-through shapes)
+# are hand-maintained in ui/src/api/manual.ts instead.
#
# Install tygo (once, per dev machine):
# go install github.com/gzuidhof/tygo@latest
@@ -19,13 +21,14 @@
packages:
- path: "github.com/stakwork/stakgraph/gateway/internal/adminapi"
output_path: "internal/adminapi/ui/src/api/types.ts"
- # frontmatter is prepended to the generated file. We mark it
- # generated so editors / reviewers know not to hand-edit.
+ # frontmatter is prepended after tygo's own "Code generated"
+ # banner, so editors / reviewers know not to hand-edit.
frontmatter: |
- // Code generated by tygo. DO NOT EDIT.
//
// Source: github.com/stakwork/stakgraph/gateway/internal/adminapi
- // Regenerate with `make tygo`.
+ // Regenerate with `make tygo`; CI enforces via `make tygo-check`.
+ // Hand-maintained companions (unions, trust mirrors) live in
+ // manual.ts.
/* eslint-disable */
type_mappings:
"time.Time": "string"