/** * Shared API contracts. * * These types mirror the FastAPI response schemas. They are intentionally * hand-written and small: the backend is the source of truth, and a drift here * surfaces immediately as a TypeScript error at the call site. */ export type Role = "owner" | "admin" | "manager" | "member" | "viewer"; export type MembershipStatus = "active" | "invited" | "suspended"; /** * Provider kinds the AI Control Plane can talk to. agy (Google Antigravity) * is the only one — the platform no longer has a provider picker. */ export type ProviderType = "agy"; /** agy reasoning efforts, cheapest first. Mirrors the backend's list. */ export type ReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; export const REASONING_EFFORTS: ReasoningEffort[] = [ "minimal", "low", "medium", "high", "xhigh", ]; const EFFORT_LABELS: Record = { minimal: "حداقل", low: "کم", medium: "متوسط", high: "زیاد", xhigh: "بسیار زیاد", }; /** * Reasoning efforts a model may be configured with, or an empty list when the * model cannot think — the backend rejects a thinking config everywhere else. * Same marker rule as the agy adapter: thinking-capable ids contain one of * `thinking` / `gemini-3` / `gemini-2.5-pro` / `gemini-2.5-flash`, image * and claude models are excluded from explicit effort selection. */ export function effortsForModel(modelId: string): ReasoningEffort[] { const lower = modelId.toLowerCase(); if (lower.includes("image")) return []; if (lower.includes("claude")) return []; if (lower.includes("gpt")) return ["medium"]; if (lower.includes("gemini-3.1-pro")) return ["low", "high"]; const canThink = lower.includes("thinking") || lower.includes("gemini-3") || lower.includes("gemini-2.5-pro") || lower.includes("gemini-2.5-flash"); return canThink ? [...REASONING_EFFORTS] : []; } export function effortLabel(effort: string): string { return EFFORT_LABELS[effort as ReasoningEffort] ?? effort; } /** A live model reported by an agy account's catalogue. */ export interface AgyAccountModel { id: string; display_name: string; supports_thinking: boolean; supported_efforts: ReasoningEffort[]; default_effort?: ReasoningEffort | null; supports_images?: boolean; } /** Rotation-pool lifecycle of one agy account. */ export type AgyAccountStatus = "active" | "cooldown" | "error"; /** * One agy account (Google identity). Deliberately credential-free: the console * manages accounts, never tokens. */ export interface AgyAccount { id: string; provider_id: string; name: string; email: string; project_id: string; status: AgyAccountStatus; cooldown_until: string | null; is_active: boolean; last_error: string; last_used_at: string | null; created_at: string; } /** What a model can do. Routing picks models by required capability. */ export type Capability = "text" | "vision" | "tools" | "reasoning" | "long_context" | "code"; /** Routing intents. Customers never see these names — only the platform owner does. */ export type RoutingMode = "fast" | "cheap" | "balanced" | "reasoning" | "vision" | "coding"; export type TaskStatus = | "new" | "triaged" | "waiting_approval" | "running" | "done" | "failed" | "cancelled"; export type RunStatus = | "queued" | "planning" | "waiting_approval" | "running" | "tool_calling" | "sandbox_running" | "completed" | "failed" | "cancelled" | "timed_out"; export type RoleKey = | "manager" | "planner" | "developer" | "reviewer" | "tester" | "researcher" | "content_creator" | "support"; export type EmployeeStatus = "active" | "paused" | "archived"; export type PreviewDomainStatus = "active" | "disabled" | "failed"; export type RateUnit = | "model_input_tokens" | "model_output_tokens" | "sandbox_cpu_second" | "sandbox_memory_gb_second" | "storage_gb_day" | "tool_call" | "premium_employee_run"; export interface ApiErrorBody { error: { code: string; message: string; details?: unknown; /** * Correlates this response with the row the API wrote to its error console * (and with the `X-Request-ID` response header). Surfacing it lets a user * quote one string that points at exactly one server-side record. */ request_id?: string; }; } export interface User { id: string; email: string; name: string; avatar_url?: string | null; is_platform_owner: boolean; } export interface Organization { id: string; name: string; slug: string; owner_user_id: string; } export interface OrganizationWithRole extends Organization { role: Role; } export interface Member { id: string; user_id: string; email: string; name: string; role: Role; status: MembershipStatus; created_at: string; } export interface Invite { id: string; email: string; role: Role; expires_at: string; invite_token: string; invite_url: string; } export interface TokenResponse { access_token: string; token_type: string; user: User; } export interface MeResponse { user: User; organizations: OrganizationWithRole[]; } export interface Project { id: string; organization_id: string; name: string; slug: string; description: string; default_branch: string; created_at: string; } export interface RepoStatus { exists: boolean; private: boolean; default_branch: string; branches: string[]; open_pull_requests: number; } export interface Repository { id: string; provider: string; owner: string; repo_name: string; clone_url: string; web_url: string; default_branch: string; private: boolean; } export interface AdminModel { id: string; provider_id: string; internal_name: string; display_name_for_admin: string; capabilities: string[]; input_price_per_1m: number; output_price_per_1m: number; /** agy reasoning effort ("" = provider default). Owner-only metadata. */ reasoning_effort: string; is_active: boolean; } export interface AdminRoutingPolicy { id: string; name: string; description: string; mode: string; is_default: boolean; is_active: boolean; } export interface AdminRateCard { id: string; name: string; unit: RateUnit; customer_price: number; internal_cost_estimate: number; is_active: boolean; } export interface AdminOrganization { id: string; name: string; slug: string; member_count: number; created_at: string; } /** Result of a routing dry-run: which model a mode would pick, and why. */ export interface ModelChoice { mode: string; model_id: string; internal_name: string; capabilities: string[]; estimated_input_price_per_1m: number; estimated_output_price_per_1m: number; } export interface AdminUsageRow { organization_id: string; quantity: number; total_price: number; } export interface PreviewDomain { id: string; host: string; port: number; status: PreviewDomainStatus; protected: boolean; url: string; } export interface Employee { id: string; organization_id: string; project_id?: string | null; template_id?: string | null; name: string; role_key: RoleKey; status: EmployeeStatus; routing_policy_id?: string | null; created_at: string; } export interface Task { id: string; organization_id: string; project_id?: string | null; thread_id?: string | null; title: string; description: string; status: TaskStatus; assigned_employee_id?: string | null; created_at: string; updated_at: string; } export interface Run { id: string; task_id: string; employee_id?: string | null; status: RunStatus; error?: string | null; started_at?: string | null; finished_at?: string | null; created_at: string; } export interface Thread { id: string; organization_id: string; project_id?: string | null; title: string; created_at: string; } export interface Message { id: string; thread_id: string; role: "user" | "assistant" | "system"; content: string; employee_id?: string | null; created_at: string; } export interface ThreadDetail extends Thread { messages: Message[]; } export interface RunStep { id: string; index: number; kind: string; title: string; payload_json: Record; created_at: string; } export interface ToolCall { id: string; tool_key: string; status: string; requires_approval: boolean; arguments_json: Record; result_json: Record; created_at: string; } export interface ArtifactDetail extends Artifact { content_ref: string; run_id?: string | null; } export interface RunDetail extends Run { steps: RunStep[]; tool_calls: ToolCall[]; artifacts: ArtifactDetail[]; } export interface TaskDetail extends Task { runs: Run[]; } export interface AgentTemplate { id: string; name: string; role_key: RoleKey; description: string; default_tools_json: string[]; } /** A routing policy as a customer may see it — no mode, no model hints. */ export interface RoutingChoice { id: string; name: string; description: string; } export interface PromptVersion { id: string; version: number; content: string; created_at: string; } export interface ToolPermission { tool_key: string; allowed: boolean; } /** One entry in the tool catalog an employee can be granted. */ export interface ToolSpec { key: string; label: string; description: string; requires_approval: boolean; destructive: boolean; category: string; } export interface EmployeeDetail extends Employee { prompt_versions: PromptVersion[]; tool_permissions: ToolPermission[]; open_task_count: number; } export interface PostMessageResponse { user_message: Message; assistant_message: Message; } export interface Artifact { id: string; name: string; kind: string; size_bytes: number; created_at: string; } export interface BillingSummary { balance: number; currency: string; low_balance: boolean; low_balance_threshold: number; total_spend: number; by_unit: { unit: RateUnit; quantity: number; total_price: number }[]; by_employee: Record; by_project: Record; } export interface UsageEvent { id: string; unit: RateUnit; quantity: number; unit_price: number; total_price: number; project_id?: string | null; employee_id?: string | null; run_id?: string | null; created_at: string; } export interface Invoice { id: string; amount: number; currency: string; status: string; period_start?: string | null; period_end?: string | null; created_at: string; } export interface Payment { id: string; amount: number; currency: string; status: string; provider: string; created_at: string; } export interface Wallet { balance: number; currency: string; low_balance: boolean; } export interface AuditEvent { id: string; action: string; actor_user_id?: string | null; target_type: string; target_id: string; created_at: string; } // --- sandbox ---------------------------------------------------------------- export type NetworkPolicy = "deny_all" | "allowlist" | "open"; export interface SandboxTemplate { id: string; name: string; provider: string; image: string; description: string; default_cpu: number; default_memory_mb: number; default_disk_mb: number; } export interface SandboxWorkspace { id: string; project_id: string; template_id: string; provider: string; status: string; created_at: string; } export interface SandboxSession { id: string; workspace_id: string; status: string; started_at: string; last_active_at: string; } export interface SandboxCommandRun { id: string; command: string; exit_code: number | null; stdout: string; stderr: string; duration_ms: number; truncated: boolean; created_at: string; } export interface SandboxFileRead { path: string; content: string; } export interface SandboxSnapshot { id: string; name: string; created_at: string; } export interface SandboxPreviewUrl { port: number; url: string; } /** * Effective limits are resolved server-side (project -> organization -> * platform) and clamped. The UI only ever displays them. */ export interface SandboxResourceLimit { cpu: number; memory_mb: number; disk_mb: number; timeout_seconds: number; max_output_bytes: number; network_policy: NetworkPolicy; } export interface HealthResponse { status: string; app: string; env: string; } export interface ReadinessResponse { status: string; checks: { database: string; model_provider: string; adapters: Record; }; } /* -------------------------------------------------------------------------- */ /* Error console (platform owner) */ /* -------------------------------------------------------------------------- */ export type ErrorSeverity = "warning" | "error" | "critical"; export type ErrorSource = "http" | "worker" | "startup"; export type ErrorStatusFilter = "open" | "resolved"; /** * One persisted failure. Deliberately wide: a record that lacks the request * path or the exception class forces whoever is debugging to reproduce the bug * just to learn what it was. */ export interface ErrorEvent { id: string; kind: string; message: string; stack: string; severity: ErrorSeverity; source: ErrorSource; error_type: string; fingerprint: string; status_code: number | null; method: string; path: string; organization_id: string | null; user_id: string | null; run_id: string | null; request_id: string; ip: string; user_agent: string; metadata_json: Record; resolved_at: string | null; resolved_note: string; created_at: string; } /** Every occurrence of one distinct bug, collapsed into a single row. */ export interface ErrorGroup { fingerprint: string; occurrences: number; open_count: number; first_seen: string; last_seen: string; kind: string; error_type: string; severity: ErrorSeverity; source: ErrorSource; path: string; sample_message: string; } export interface ErrorSummary { open_by_severity: Record; total_open: number; } export interface ErrorListResponse { items: ErrorEvent[]; total: number; limit: number; offset: number; } export interface ResolveErrorResponse { fingerprint: string; resolved: number; } export interface ErrorQuery { status?: ErrorStatusFilter; severity?: ErrorSeverity; source?: ErrorSource; organization_id?: string; search?: string; limit?: number; offset?: number; } // --------------------------------------------------------------------------- // Realtime (WebSocket) // // The live feed is the same product surface as the REST API: one envelope // shape, mirrored key-for-key from ``app/application/ports/realtime.py``. // A page that switches on ``WsEventType`` never needs to know which part of // the backend produced the event. // // Channel naming is what a page subscribes to — ``org:{id}`` for the tenant // feed, ``task:{id}`` for one task, and so on. The server authorizes every // subscribe against the caller's memberships, so a channel the user may not // see simply comes back rejected. // --------------------------------------------------------------------------- export type WsEventType = | "run.step" | "run.status" | "task.status" | "task.created" | "chat.pending" | "message.created" | "approval.pending" | "artifact.created" | "sandbox.command" | "sandbox.session" | "sandbox.reaped" | "usage.recorded" | "billing.wallet" | "error.recorded" | "audit.recorded" | "worker.status"; /** One pushed event, as the browser receives it. */ export interface WsEvent> { id: string; type: WsEventType; data: T; organization_id: string | null; /** ISO-8601 UTC; render with ``toLocaleString`` like every other timestamp. */ at: string; } /** Every frame the server sends. Control frames and events share ``type``. */ export type WsServerFrame = | { type: "hello"; user_id: string | null; server_time: string } | { type: "subscribed"; accepted: string[]; rejected: string[] } | { type: "unsubscribed"; channels: string[] } | { type: "pong" } | { type: "error"; message: string } | { type: "event"; event: WsEvent } /** Reply to a client ``rpc`` frame: the HTTP status and parsed JSON body of the * internal dispatch, correlating on ``id``. */ | { type: "rpc_result"; id: string; status: number; body: unknown }; /** Every frame the client may send. */ export type WsClientFrame = | { action: "subscribe"; channels: string[] } | { action: "unsubscribe"; channels: string[] } | { action: "ping" } /** A request/response call into the server. ``id`` correlates the * ``rpc_result``; ``path`` is the full mounted path including any query * string. Every application call travels over this frame. */ | { action: "rpc"; id: string; method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; path: string; body: unknown; }; /** Live run-step payload — what the task timeline renders as it arrives. */ export interface WsRunStepData { run_id: string; task_id: string; step: { id: string; index: number; kind: string; title: string; payload: Record; }; } /** Live sandbox command payload — the terminal panel's stream. */ export interface WsSandboxCommandData { session_id: string; run_id: string; command: string; status: string; exit_code: number | null; stdout: string; stderr: string; duration_ms: number; truncated: boolean; }