Frontend-Backend API Contract
The authoritative contract between the River Agents frontend (Next.js 15, React 19, TypeScript, Zustand) and the backend API -- covering the API client configuration, all TypeScript type definitions, page-by-page call specifications for all 12 pages, 7 Zustand store designs, WebSocket event types, error handling, optimistic update patterns, and cache invalidation rules.
Quick Navigation
API Client Configuration
All frontend HTTP traffic routes through TLO Gateway (:8001). The frontend never calls the backend, data orchestration, or any internal service directly.
Base URL
Base URL = NEXT_PUBLIC_API_BASE_URL (default: http://localhost:8001)
Full URL = {Base URL}/api/v1{endpoint}
Required Headers
| Header | Value | Applied To |
|---|---|---|
Authorization | Bearer {access_token} | All authenticated endpoints |
Content-Type | application/json | All requests (auto-removed for FormData) |
Accept | application/json | All requests |
Token Lifecycle
- On login, signup, or OTP verify, the backend returns
{ access_token, refresh_token, expires_in }. - Tokens are stored via
StorageService(localStorage). - On 401 response, the client automatically attempts refresh via
POST /auth/refresh-token. - If refresh fails (invalid or expired refresh token), all auth storage is cleared and the user is redirected to
/signin. - On successful refresh, the original request is retried once with the new token.
Standard Response Envelope
Every API response uses this structure:
{
"success": true,
"status": 200,
"message": "Agents retrieved successfully",
"data": {},
"error": null,
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-05-10T08:04:22Z"
}
}
Pagination Convention
Paginated endpoints accept these query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-indexed) |
page_size | integer | 20 | Items per page (max 100) |
sort_by | string | created_at | Sort field |
sort_order | asc or desc | desc | Sort direction |
search | string | -- | Full-text search across name, description |
Paginated responses include:
{
"items": [],
"pagination": {
"total": 142,
"page": 1,
"page_size": 20,
"total_pages": 8,
"has_next": true,
"has_previous": false
}
}
TypeScript Type Definitions
Enumerations
// Agent lifecycle status
type AgentStatus =
| 'draft'
| 'configured'
| 'validated'
| 'deployed'
| 'active'
| 'running'
| 'awaiting_approval'
| 'paused'
| 'archived';
// Business function category
type AgentCategory =
| 'customer_support'
| 'sales_lead_qualification'
| 'finance_reconciliation'
| 'risk_compliance'
| 'data_analyst'
| 'operations_monitoring'
| 'executive_decision'
| 'custom_enterprise';
// Autonomy level
type ActionLevel =
| 'read_respond' // Read data and respond only
| 'recommend' // Suggest actions but not execute
| 'act_with_approval' // Execute write actions after human approval
| 'fully_automated'; // Execute all allowed actions autonomously
// How an execution is triggered
type TriggerType =
| 'manual'
| 'scheduled'
| 'event'
| 'workflow'
| 'api'
| 'threshold';
// Execution run state
type ExecutionStatus =
| 'queued'
| 'running'
| 'awaiting_approval'
| 'success'
| 'failed'
| 'cancelled'
| 'timeout'
| 'max_turns_exceeded'
| 'budget_exceeded';
// Approval resolution state
type ApprovalStatus =
| 'pending'
| 'approved'
| 'rejected'
| 'edited_approved'
| 'expired';
// Agentic loop step classification
type StepType =
| 'reasoning'
| 'tool_call'
| 'observation'
| 'interaction'
| 'approval_gate'
| 'error_recovery';
// Tool dispatch category
type ToolCategory = 'reasoning' | 'execution' | 'interaction';
// Agent version deployment state
type VersionStatus = 'draft' | 'active' | 'archived';
// Template availability state
type TemplateStatus = 'draft' | 'published' | 'deprecated';
// Governance enforcement mode
type EnforcementMode = 'block' | 'require_approval' | 'warn' | 'log_only';
// Governance policy scope
type PolicyScope = 'organization' | 'workspace';
// Notification delivery channel
type NotificationChannel = 'email' | 'webhook' | 'slack' | 'in_app';
// Audit event categories
type AuditEventType =
| 'agent_created' | 'agent_updated' | 'agent_deployed'
| 'agent_paused' | 'agent_resumed' | 'agent_archived' | 'agent_deleted'
| 'execution_started' | 'execution_completed' | 'execution_failed' | 'execution_cancelled'
| 'approval_requested' | 'approval_granted' | 'approval_rejected' | 'approval_expired'
| 'trigger_created' | 'trigger_updated' | 'trigger_deleted'
| 'policy_created' | 'policy_updated' | 'policy_violated'
| 'settings_updated' | 'api_key_created' | 'api_key_revoked'
| 'data_source_bound' | 'data_source_unbound'
| 'tool_enabled' | 'tool_disabled';
// Alert severity
type AlertSeverity = 'info' | 'warning' | 'critical';
// System health
type HealthStatus = 'healthy' | 'degraded' | 'critical' | 'unknown';
Core Entity Interfaces
// ---- Agent ----
interface Agent {
id: number;
organization_id: number;
workspace_id: number;
created_by: number;
owner_user_id: number;
name: string;
description: string;
category: AgentCategory;
status: AgentStatus;
action_level: ActionLevel;
governance_level: 'standard' | 'strict' | 'custom';
icon_name: string; // Lucide icon name
icon_color: string; // Tailwind color class
icon_bg: string; // Tailwind background class
tags: string[];
goal: string;
instructions: string;
system_prompt: string | null;
model_config: ModelConfig;
notification_config: NotificationConfig;
current_version_id: number | null;
template_id: number | null;
// Computed / joined fields returned by GET /agents and GET /agents/{id}
owner_name: string;
owner_avatar: string | null;
health_status: HealthStatus;
health_score: number; // 0-100
total_runs: number;
successful_runs: number;
failed_runs: number;
avg_duration_seconds: number;
last_run_at: string | null; // ISO 8601
last_run_status: ExecutionStatus | null;
deployed_at: string | null;
created_at: string; // ISO 8601
updated_at: string;
deleted_at: string | null;
}
// ---- AgentVersion ----
interface ModelConfig {
default_tier?: 'fast' | 'balanced' | 'reasoning' | 'coding';
reasoning_tier?: 'fast' | 'balanced' | 'reasoning' | 'coding';
provider_preference?: 'anthropic' | 'openai' | 'gemini' | 'deepseek' | 'any';
temperature?: number;
max_tokens?: number;
max_turns?: number;
token_budget?: number;
timeout_seconds?: number;
}
interface ToolConfigOverride {
timeout_seconds?: number;
retry_count?: number;
disabled?: boolean;
}
interface ApprovalRules {
require_approval_for: string[]; // Tool names requiring approval
auto_approve_conditions: { tool: string; condition: string }[];
approver_roles: string[];
escalation_timeout_minutes: number;
}
interface AgentVersion {
id: number;
agent_id: number;
version_number: number;
status: VersionStatus;
instruction_set: string;
model_config: ModelConfig;
tool_id_overrides: Record<string, ToolConfigOverride>;
approval_rules: ApprovalRules | null;
is_active: boolean;
change_summary: string | null;
created_by: number;
created_by_name: string;
created_at: string;
tools?: ToolBinding[];
data_sources?: DataSourceBinding[];
}
// ---- AgentTrigger ----
interface ScheduledTriggerConfig {
cron: string;
timezone: string;
input_template?: Record<string, unknown>;
}
interface EventTriggerConfig {
event_source: 'webhook' | 'database' | 'pubsub';
event_type: string;
filter_expression?: string;
input_mapping?: Record<string, string>;
}
interface ThresholdTriggerConfig {
metric: string;
operator: 'gt' | 'lt' | 'gte' | 'lte' | 'eq';
value: number;
window_seconds: number;
cooldown_seconds: number;
}
interface AgentTrigger {
id: number;
agent_id: number;
trigger_type: TriggerType;
config: ScheduledTriggerConfig | EventTriggerConfig | ThresholdTriggerConfig | Record<string, unknown>;
is_active: boolean;
last_triggered_at: string | null;
next_trigger_at: string | null;
created_at: string;
updated_at: string;
}
// ---- Execution ----
interface Execution {
id: number;
organization_id: number;
workspace_id: number;
agent_id: number;
agent_name: string; // Joined
agent_version_id: number | null;
version_number: number | null; // Joined
trigger_type: TriggerType;
trigger_id: number | null;
triggered_by_user_id: number | null;
triggered_by_name: string | null; // Joined
input_prompt: string | null;
status: ExecutionStatus;
priority: 'low' | 'medium' | 'high' | 'critical';
started_at: string | null;
completed_at: string | null;
duration_seconds: number | null;
turn_count: number;
tool_calls_count: number;
total_tokens: number;
cost_estimate: number | null;
error_message: string | null;
error_code: string | null;
temporal_workflow_id: string | null;
created_at: string;
updated_at: string;
}
// ---- ExecutionStep ----
interface ExecutionStep {
id: number;
execution_id: number;
step_number: number;
step_type: StepType;
tool_name: string | null;
tool_category: ToolCategory | null;
input_data: Record<string, unknown>;
output_data: Record<string, unknown>;
model_used: string | null;
model_provider: string | null;
tokens_input: number | null;
tokens_output: number | null;
duration_ms: number | null;
status: 'success' | 'error' | 'timeout' | 'skipped' | 'blocked';
governance_decision: 'PROCEED' | 'APPROVAL_REQUIRED' | 'BLOCKED' | 'SUGGEST_ONLY' | null;
error_message: string | null;
created_at: string;
}
// ---- ApprovalRequest ----
interface ApprovalRequest {
id: number;
organization_id: number;
workspace_id: number;
execution_id: number;
agent_id: number;
agent_name: string; // Joined
execution_step_id: number | null;
action_description: string;
action_tool: string | null;
action_payload: Record<string, unknown>;
reasoning_summary: string | null;
risk_context: string | null;
confidence_score: number | null;
status: ApprovalStatus;
requested_at: string;
expires_at: string | null;
responded_at: string | null;
responded_by_user_id: number | null;
responded_by_name: string | null; // Joined
response_note: string | null;
created_at: string;
updated_at: string;
}
// ---- Template ----
interface Template {
id: number;
organization_id: number | null; // null for system templates
name: string;
description: string;
category: AgentCategory;
icon_name: string;
icon_color: string;
icon_bg: string;
config: {
name_suggestion: string;
description: string;
category: AgentCategory;
action_level: ActionLevel;
instructions: string;
model_config?: ModelConfig;
tools?: string[];
data_source_ids?: number[];
default_triggers?: AgentTrigger[];
governance_policy_ids?: number[];
};
integrations: string[];
governance: Record<string, unknown>;
is_system: boolean;
is_active: boolean;
usage_count: number;
created_by: number | null;
created_at: string;
updated_at: string;
}
// ---- GovernancePolicy ----
interface PolicyCondition {
when: string; // CEL expression
description?: string;
}
interface PolicyEnforcement {
action: 'block' | 'gate' | 'log' | 'alert';
message?: string;
approvers?: string[];
alert_channels?: NotificationChannel[];
expires_hours?: number;
}
interface GovernancePolicy {
id: number;
agent_id: number;
policy_type: string;
name: string;
description: string | null;
condition: PolicyCondition;
enforcement: PolicyEnforcement;
is_active: boolean;
created_at: string;
updated_at: string;
}
// ---- AgentMetrics ----
interface AgentMetricsPeriod {
period_start: string;
period_end: string;
total_runs: number;
successful_runs: number;
failed_runs: number;
avg_duration_seconds: number;
p95_duration_seconds: number;
total_tokens_used: number;
total_cost_usd: number;
approval_requests: number;
approval_rate: number;
health_score: number;
error_rate: number;
throughput_per_hour: number;
daily: AgentMetricsDailyPoint[];
}
interface AgentMetricsDailyPoint {
date: string;
total_runs: number;
successful_runs: number;
failed_runs: number;
avg_duration_seconds: number;
total_tokens_used: number;
total_cost_usd: number;
}
// ---- AuditLogEntry ----
interface AuditLogEntry {
id: number;
organization_id: number;
workspace_id: number;
event_type: AuditEventType;
actor_user_id: number | null;
actor_type: 'user' | 'agent' | 'system' | 'scheduler';
resource_type: string;
resource_id: number | null;
agent_id: number | null;
agent_name: string | null; // Joined
execution_id: number | null;
details: Record<string, unknown>;
previous_state: Record<string, unknown> | null;
new_state: Record<string, unknown> | null;
ip_address: string | null;
user_agent: string | null;
created_at: string;
}
// ---- WorkspaceSettings ----
interface NotificationConfig {
channels: { type: NotificationChannel; target: string; enabled: boolean }[];
on_success: boolean;
on_failure: boolean;
on_approval_needed: boolean;
on_policy_violation: boolean;
}
interface WorkspaceSettings {
general: {
default_action_level: ActionLevel;
default_model_tier: 'fast' | 'balanced' | 'reasoning' | 'coding';
max_agents_per_workspace: number;
auto_archive_after_days: number | null;
default_notification_channels: NotificationChannel[];
};
compute: {
max_concurrent_executions: number;
max_turns_per_execution: number;
execution_timeout_seconds: number;
max_tokens_per_execution: number;
max_retries: number;
retry_delay_seconds: number;
compute_priority: 'low' | 'medium' | 'high' | 'critical';
};
governance: {
require_approval_above_level: ActionLevel;
strict_governance_mode: boolean;
budget_limit_monthly_usd: number | null;
budget_alert_threshold_percent: number;
require_certification_for_deploy: boolean;
policy_violation_action: 'block' | 'warn' | 'log';
};
access: {
default_agent_visibility: 'workspace' | 'organization';
allowed_categories: AgentCategory[];
role_permissions: Record<string, string[]>;
};
notifications: {
notifications_enabled: boolean;
notify_on_execution_failure: boolean;
notify_on_approval_required: boolean;
notify_on_policy_violation: boolean;
slack_webhook_url: string | null;
teams_webhook_url: string | null;
};
api: {
api_access_enabled: boolean;
api_rate_limit_per_minute: number;
api_keys: ApiKey[];
webhooks: Webhook[];
};
}
// ---- ApiKey ----
interface ApiKey {
id: number;
name: string;
key_prefix: string; // e.g., "ra_k_abc"
permissions: Record<string, unknown>;
is_active: boolean;
expires_at: string | null;
last_used_at: string | null;
usage_count: number;
created_at: string;
}
// ---- Webhook ----
interface Webhook {
id: number;
name: string;
url: string;
events: string[];
headers: Record<string, string>;
is_active: boolean;
last_triggered_at: string | null;
last_status_code: number | null;
failure_count: number;
created_at: string;
updated_at: string;
}
// ---- Tool / DataSource Bindings ----
interface ToolBinding {
id: number;
agent_id: number;
tool_name: string;
tool_category: ToolCategory;
is_enabled: boolean;
config: Record<string, unknown>;
}
interface DataSourceBinding {
id: number;
agent_id: number;
data_source_id: number;
data_source_name: string; // Joined
data_source_type: string; // Joined
access_level: 'read' | 'write' | 'read_write';
connection_status: 'connected' | 'disconnected' | 'error';
last_sync_at: string | null;
}
// ---- Aggregate / Stats Types ----
interface AgentStats {
total_agents: number;
active_agents: number;
running_agents: number;
paused_agents: number;
failed_agents: number;
pending_approvals: number;
agents_by_category: Record<AgentCategory, number>;
agents_by_status: Record<AgentStatus, number>;
}
interface RunStats {
total_runs: number;
running: number;
success: number;
failed: number;
awaiting_approval: number;
avg_duration_seconds: number;
success_rate: number; // 0.0-1.0
}
interface MonitoringStats {
total_active_agents: number;
total_runs_24h: number;
overall_success_rate: number;
avg_latency_ms: number;
total_tokens_24h: number;
active_alerts: number;
queue_depth: number;
system_health: HealthStatus;
}
interface ThroughputDataPoint {
timestamp: string;
requests: number;
errors: number;
latency_ms: number;
}
interface Alert {
id: number;
severity: AlertSeverity;
agent_id: number | null;
agent_name: string | null;
title: string;
message: string;
metric_name: string | null;
metric_value: number | null;
threshold_value: number | null;
acknowledged: boolean;
acknowledged_by: string | null;
created_at: string;
resolved_at: string | null;
}
interface AgentHealth {
agent_id: number;
agent_name: string;
status: AgentStatus;
health_status: HealthStatus;
health_score: number;
success_rate: number;
avg_latency_ms: number;
last_run_at: string | null;
}
interface AuditStats {
total_events: number;
events_by_type: Record<AuditEventType, number>;
unique_agents: number;
unique_users: number;
time_range_start: string;
time_range_end: string;
}
interface GeneratedAgentResult {
agent_config: {
name: string;
description: string;
category: AgentCategory;
action_level: ActionLevel;
instruction_set: string;
tools: string[];
data_source_types: string[];
trigger_config: Record<string, unknown>;
notification_config: NotificationConfig;
governance_level: string;
tags: string[];
};
reasoning: string;
confidence: number; // 0.0-1.0
warnings: string[];
}
// ---- Generic Response Wrappers ----
interface PaginatedResponse<T> {
success: boolean;
status: number;
message: string;
data: {
items: T[];
pagination: {
total: number;
page: number;
page_size: number;
total_pages: number;
has_next: boolean;
has_previous: boolean;
};
};
error: string | null;
meta: { request_id: string; timestamp: string };
}
interface SingleResponse<T> {
success: boolean;
status: number;
message: string;
data: T;
error: string | null;
meta: { request_id: string; timestamp: string };
}
interface ErrorResponse {
success: false;
status: number;
message: string;
data: null;
error: string;
meta: { request_id: string; timestamp: string };
}
interface PaginationMeta {
total: number;
page: number;
page_size: number;
total_pages: number;
has_next: boolean;
has_previous: boolean;
}
Page-by-Page API Specifications
Agent Library (/agents)
On mount -- both calls fire in parallel:
| # | Method | Endpoint | Query Params | Response | Purpose |
|---|---|---|---|---|---|
| 1 | GET | /api/v1/agents | page=1&page_size=20 | PaginatedResponse<Agent> | Initial agent list |
| 2 | GET | /api/v1/agents/stats | -- | SingleResponse<AgentStats> | Header metric cards |
User actions:
| Action | Trigger | Method | Endpoint | Params / Body | Response |
|---|---|---|---|---|---|
| Search | Debounced 300ms keystroke | GET | /api/v1/agents | ?search={query}&page=1 | PaginatedResponse<Agent> |
| Filter by status | Dropdown selection | GET | /api/v1/agents | ?status={status}&page=1 | PaginatedResponse<Agent> |
| Filter by category | Dropdown selection | GET | /api/v1/agents | ?category={cat}&page=1 | PaginatedResponse<Agent> |
| Sort | Column header click | GET | /api/v1/agents | ?sort_by={field}&sort_order={asc/desc} | PaginatedResponse<Agent> |
| Paginate | Page button click | GET | /api/v1/agents | ?page={n}&page_size=20 | PaginatedResponse<Agent> |
| Edit agent | Dropdown "Edit" | PUT | /api/v1/agents/{id} | { name, description, action_level } | SingleResponse<Agent> |
| Delete agent | Dropdown "Delete" + confirm | DELETE | /api/v1/agents/{id} | ?force=true (optional) | SingleResponse<{ message: string }> |
Create Entry (/agents/create)
No API calls on mount. Static page presenting two creation paths. Navigation only.
| Action | Result |
|---|---|
| Click "Create with AI" | Navigate to /agents/create/ai |
| Click "Guided Setup" | Navigate to /agents/create/guided |
AI Create (/agents/create/ai)
On mount: No API calls. Renders the input form.
User actions:
| Action | Method | Endpoint | Body | Response |
|---|---|---|---|---|
| Submit prompt | POST | /api/v1/agents/generate | { prompt: string, quick_starter_id?: string } | SingleResponse<GeneratedAgentResult> |
| Accept generated config | POST | /api/v1/agents | Full AgentCreateRequest from generated config | SingleResponse<Agent> |
| Load generation history | GET | /api/v1/agents/generation-history | -- | PaginatedResponse<GenerationHistoryItem> |
Generation animation: While awaiting the POST /api/v1/agents/generate response, the frontend drives a 4-stage animated UI:
| Stage | Label |
|---|---|
| 1 | Analyzing requirements |
| 2 | Designing configuration |
| 3 | Selecting tools and data sources |
| 4 | Finalizing |
On success, store the generated config in agentStore.draftConfig and allow user to review before posting to POST /api/v1/agents.
Guided Wizard (/agents/create/guided)
Accepts ?template={id} query param. Pre-populates from template config when present.
On mount:
| Condition | Method | Endpoint | Response | Purpose |
|---|---|---|---|---|
?template={id} present | GET | /api/v1/agents/templates/{id} | SingleResponse<Template> | Pre-fill all wizard fields |
| Always | GET | /api/v1/agents/templates | PaginatedResponse<Template> | Optional template selector in Step 1 |
Per-step API calls:
| Step | Name | API Calls |
|---|---|---|
| Step 1 | Identity | None -- local state only |
| Step 2 | Define Goal | None -- local state only |
| Step 3 | Data and Systems | GET /api/v1/data-sources -- PaginatedResponse<DataSource> |
| Step 4 | Actions and Tools | None -- local state only |
| Step 5 | Review and Create | POST /api/v1/agents on submit |
POST /api/v1/agents request body:
interface AgentCreateRequest {
name: string;
description: string;
category: AgentCategory;
domain: string | null;
action_level: ActionLevel;
governance_level: 'standard' | 'strict' | 'custom';
goal: string;
icon_name: string;
icon_color: string;
icon_bg: string;
tags: string[];
template_id: number | null;
version: {
instruction_set: string;
model_config: ModelConfig;
tools: string[];
data_sources: { data_source_id: number; access_level: string }[];
trigger_config: Record<string, unknown> | null;
approval_rules: ApprovalRules | null;
};
notification_config: NotificationConfig;
}
Response: SingleResponse<Agent>
Agent Detail (/agents/{id})
The most complex page. 7 tabs: Overview, Activity, Data Access, Governance, Monitoring, Audit and Logs, Settings.
On mount -- all three calls fire in parallel:
| # | Method | Endpoint | Response | Purpose |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/{id} | SingleResponse<Agent> | Full agent detail |
| 2 | GET | /api/v1/agents/{id}/executions?page=1&page_size=10 | PaginatedResponse<Execution> | Recent runs for Overview tab |
| 3 | GET | /api/v1/agents/{id}/metrics?period=7d | SingleResponse<AgentMetricsPeriod> | Chart data for KPI strip |
Hero actions (available on all tabs):
| Action | Method | Endpoint | Body | Notes |
|---|---|---|---|---|
| Deploy | POST | /api/v1/agents/{id}/deploy | -- | Optimistic: status -> 'deployed' |
| Pause | POST | /api/v1/agents/{id}/pause | -- | Optimistic: status -> 'paused' |
| Resume | POST | /api/v1/agents/{id}/resume | -- | Optimistic: status -> 'active' |
| Run Now | POST | /api/v1/agents/{id}/run | { trigger_payload? } | Returns { execution_id }; opens WebSocket |
| Archive | POST | /api/v1/agents/{id}/archive | -- | Optimistic: status -> 'archived' |
| Delete | DELETE | /api/v1/agents/{id} | -- | Soft delete |
Tab: Overview -- no additional API calls. Uses data from mount.
Tab: Activity
| # | Method | Endpoint | Query Params | Response |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/executions | page=1&page_size=20 | PaginatedResponse<Execution> |
User actions: paginate, click row to navigate to /agents/runs/{execution_id}.
Tab: Data Access
| # | Method | Endpoint | Response |
|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/data-sources | SingleResponse<DataSourceBinding[]> |
| User Action | Method | Endpoint | Body |
|---|---|---|---|
| Add data source | POST | /api/v1/agents/{id}/data-sources | { data_source_id, access_level, config? } |
| Remove data source | DELETE | /api/v1/agents/{id}/data-sources/{binding_id} | -- |
Tab: Governance
| # | Method | Endpoint | Response |
|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/policies | SingleResponse<GovernancePolicy[]> |
| User Action | Method | Endpoint | Body |
|---|---|---|---|
| Add policy | POST | /api/v1/agents/{id}/policies | { policy_id } or inline policy object |
| Update policy | PUT | /api/v1/agents/{id}/policies/{policy_id} | Updated fields |
| Remove policy | DELETE | /api/v1/agents/{id}/policies/{policy_id} | -- |
Tab: Monitoring
| # | Method | Endpoint | Query Params | Response |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/metrics | period=30d | SingleResponse<AgentMetricsPeriod> |
Period switch (7d, 30d, 90d) re-fetches with updated period param.
Tab: Audit and Logs
| # | Method | Endpoint | Query Params | Response |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/audit | page=1&page_size=20 | PaginatedResponse<AuditLogEntry> |
Export: GET /api/v1/agents/{id}/audit?format=csv
Tab: Settings -- all three fire in parallel on tab activation:
| # | Method | Endpoint | Response |
|---|---|---|---|
| 1 | GET | /api/v1/agents/{id}/triggers | SingleResponse<AgentTrigger[]> |
| 2 | GET | /api/v1/agents/{id}/tools | SingleResponse<ToolBinding[]> |
| 3 | GET | /api/v1/agents/{id}/versions | SingleResponse<AgentVersion[]> |
| User Action | Method | Endpoint | Body |
|---|---|---|---|
| Add trigger | POST | /api/v1/agents/{id}/triggers | Trigger object sans id |
| Update trigger | PUT | /api/v1/agents/{id}/triggers/{tid} | Updated fields |
| Delete trigger | DELETE | /api/v1/agents/{id}/triggers/{tid} | -- |
| Rollback version | POST | /api/v1/agents/{id}/versions/{vid}/rollback | -- |
Template Library (/agents/templates)
On mount:
| # | Method | Endpoint | Query Params | Response |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/templates | page=1&page_size=20 | PaginatedResponse<Template> |
User actions:
| Action | Method | Endpoint | Params |
|---|---|---|---|
| Search | GET | /api/v1/agents/templates | ?search={query}&page=1 |
| Filter by category | GET | /api/v1/agents/templates | ?category={cat}&page=1 |
| Use template | -- | -- | Navigate to /agents/create/guided?template={id} |
Template Detail (/agents/templates/{id})
On mount:
| # | Method | Endpoint | Response |
|---|---|---|---|
| 1 | GET | /api/v1/agents/templates/{id} | SingleResponse<Template> |
"Use This Template" navigates to /agents/create/guided?template={id}. No additional API call.
System Monitoring (/agents/monitoring)
On mount -- all four calls fire in parallel:
| # | Method | Endpoint | Query Params | Response | Purpose |
|---|---|---|---|---|---|
| 1 | GET | /api/v1/agents/monitoring/stats | -- | SingleResponse<MonitoringStats> | System health KPIs |
| 2 | GET | /api/v1/agents/monitoring/throughput | period=24h&granularity=1h | SingleResponse<ThroughputDataPoint[]> | Request/error/latency chart |
| 3 | GET | /api/v1/agents/monitoring/alerts | -- | SingleResponse<Alert[]> | Active alerts list |
| 4 | GET | /api/v1/agents/monitoring/health | -- | SingleResponse<AgentHealth[]> | Per-agent health table |
After initial load: subscribe to monitoring.stats_update WebSocket events at WS /api/v1/agents/ws/monitoring. When connected, stop HTTP polling. On WebSocket disconnect, fall back to polling all four endpoints every 30 seconds.
User actions:
| Action | Method | Endpoint | Body |
|---|---|---|---|
| Acknowledge alert | PATCH | /api/v1/agents/monitoring/alerts/{id} | { acknowledged: true } |
| Change period | GET | /api/v1/agents/monitoring/throughput | ?period={1h/6h/24h/7d} |
Runs List (/agents/runs)
On mount -- both calls fire in parallel:
| # | Method | Endpoint | Query Params | Response | Purpose |
|---|---|---|---|---|---|
| 1 | GET | /api/v1/agents/runs | page=1&page_size=20 | PaginatedResponse<Execution> | All runs across agents |
| 2 | GET | /api/v1/agents/runs/stats | -- | SingleResponse<RunStats> | Summary cards |
User actions:
| Action | Trigger | Method | Endpoint | Params / Body | Response |
|---|---|---|---|---|---|
| Search | Debounced 300ms | GET | /api/v1/agents/runs | ?search={query}&page=1 | PaginatedResponse<Execution> |
| Filter by status | Dropdown | GET | /api/v1/agents/runs | ?status={status}&page=1 | PaginatedResponse<Execution> |
| Filter by agent | Dropdown | GET | /api/v1/agents/runs | ?agent_id={id}&page=1 | PaginatedResponse<Execution> |
| Cancel run | "Stop" button | POST | /api/v1/agents/runs/{id}/stop | -- | SingleResponse<Execution> |
| Retry run | "Retry" button | POST | /api/v1/agents/runs/{id}/retry | -- | SingleResponse<{ execution_id: number }> |
| Open approval | "Review" button | GET | /api/v1/approvals/{approval_id} | -- | SingleResponse<ApprovalRequest> |
| Approve | Modal confirm | PATCH | /api/v1/approvals/{id} | { status: 'approved', resolution_comment? } | SingleResponse<ApprovalRequest> |
| Reject | Modal confirm | PATCH | /api/v1/approvals/{id} | { status: 'rejected', resolution_comment: string } | SingleResponse<ApprovalRequest> |
| Edit and approve | Modify + confirm | PATCH | /api/v1/approvals/{id} | { status: 'edited_approved', modified_payload: {...}, resolution_comment? } | SingleResponse<ApprovalRequest> |
Run Detail (/agents/runs/{id})
On mount:
| # | Method | Endpoint | Response | Purpose |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/runs/{id} | SingleResponse<Execution> | Execution metadata |
| 2 | GET | /api/v1/agents/runs/{id}/logs | SingleResponse<ExecutionStep[]> | Full step trace |
If status is running or awaiting_approval, open the per-execution WebSocket: WS /api/v1/agents/ws/runs/{id}. Append incoming steps to executionStore.steps in real time.
User actions:
| Action | Method | Endpoint | Body |
|---|---|---|---|
| Stop execution | POST | /api/v1/agents/runs/{id}/stop | -- |
| Retry execution | POST | /api/v1/agents/runs/{id}/retry | -- |
| Download logs | GET | /api/v1/agents/runs/{id}/logs?format=json | -- (file download) |
Workspace Settings (/agents/settings)
On mount:
| # | Method | Endpoint | Response |
|---|---|---|---|
| 1 | GET | /api/v1/agents/settings | SingleResponse<WorkspaceSettings> |
Per-tab save actions (each tab has its own "Save Changes" button):
| Tab | Method | Endpoint | Body |
|---|---|---|---|
| General | PUT | /api/v1/agents/settings/general | WorkspaceSettings['general'] |
| Compute and Limits | PUT | /api/v1/agents/settings/compute | WorkspaceSettings['compute'] |
| Governance | PUT | /api/v1/agents/settings/governance | WorkspaceSettings['governance'] |
| Access Control | PUT | /api/v1/agents/settings/access | WorkspaceSettings['access'] |
| Notifications | PUT | /api/v1/agents/settings/notifications | WorkspaceSettings['notifications'] |
| API and Webhooks | PUT | /api/v1/agents/settings/api | WorkspaceSettings['api'] |
API and Webhooks tab -- additional actions:
| Action | Method | Endpoint | Body | Notes |
|---|---|---|---|---|
| Create API key | POST | /api/v1/agents/settings/api/keys | { name, permissions, expires_at? } | Returns ApiKey & { secret: string } -- shown once |
| Delete API key | DELETE | /api/v1/agents/settings/api/keys/{id} | -- | -- |
| Create webhook | POST | /api/v1/agents/settings/api/webhooks | { url, events, name, secret? } | -- |
| Update webhook | PUT | /api/v1/agents/settings/api/webhooks/{id} | Updated fields | -- |
| Delete webhook | DELETE | /api/v1/agents/settings/api/webhooks/{id} | -- | -- |
| Test webhook | POST | /api/v1/agents/settings/api/webhooks/{id}/test | -- | Returns { status_code, response_time_ms } |
Audit and Logs (/agents/audit)
On mount -- both calls fire in parallel:
| # | Method | Endpoint | Query Params | Response |
|---|---|---|---|---|
| 1 | GET | /api/v1/agents/audit | page=1&page_size=20 | PaginatedResponse<AuditLogEntry> |
| 2 | GET | /api/v1/agents/audit/stats | -- | SingleResponse<AuditStats> |
User actions:
| Action | Trigger | Method | Endpoint | Params |
|---|---|---|---|---|
| Search | Debounced 300ms | GET | /api/v1/agents/audit | ?search={query}&page=1 |
| Filter by event type | Dropdown | GET | /api/v1/agents/audit | ?event_type={type}&page=1 |
| Filter by agent | Dropdown | GET | /api/v1/agents/audit | ?agent_id={id}&page=1 |
| Filter by user | Dropdown | GET | /api/v1/agents/audit | ?user_id={id}&page=1 |
| Filter by date range | Date picker | GET | /api/v1/agents/audit | ?date_from={iso}&date_to={iso}&page=1 |
| Export | "Export" button | GET | /api/v1/agents/audit/export | ?format=csv&{current_filters} |
No edit or delete controls are rendered. The UI enforces the write-once audit log constraint by not exposing mutation actions.
Zustand Store Designs
agentStore
interface AgentListFilters {
search?: string;
category?: AgentCategory;
status?: AgentStatus;
sort_by?: string;
sort_order?: 'asc' | 'desc';
page: number;
page_size: number;
}
interface LoadingState {
list: boolean;
detail: boolean;
create: boolean;
update: boolean;
delete: boolean;
[actionKey: string]: boolean; // e.g., loading['deploy'] = true
}
interface AgentStore {
agents: Agent[];
currentAgent: Agent | null;
stats: AgentStats | null;
versions: AgentVersion[];
draftConfig: Partial<GeneratedAgentResult['agent_config']> | null;
filters: AgentListFilters;
pagination: PaginationMeta | null;
loading: LoadingState;
error: string | null;
fetchAgents: () => Promise<void>;
fetchAgentStats: () => Promise<void>;
fetchAgentDetail: (id: number) => Promise<void>;
fetchVersions: (id: number) => Promise<void>;
createAgent: (data: AgentCreateRequest) => Promise<Agent>;
updateAgent: (id: number, data: Partial<AgentCreateRequest>) => Promise<Agent>;
deleteAgent: (id: number, force?: boolean) => Promise<void>;
deployAgent: (id: number) => Promise<Agent>;
pauseAgent: (id: number) => Promise<Agent>;
resumeAgent: (id: number) => Promise<Agent>;
archiveAgent: (id: number) => Promise<Agent>;
runAgent: (id: number, payload?: Record<string, unknown>) => Promise<number>; // -> execution_id
generateAgent: (prompt: string) => Promise<GeneratedAgentResult>;
setFilters: (filters: Partial<AgentListFilters>) => void;
setDraftConfig: (config: Partial<GeneratedAgentResult['agent_config']> | null) => void;
clearCurrentAgent: () => void;
reset: () => void;
}
fetchAgentscallsGET /api/v1/agentswith serializedfilters; updatesagentsandpagination.setFiltersmerges the partial update intofiltersand callsfetchAgents.deployAgent,pauseAgent,resumeAgent,archiveAgentall apply optimistic status updates before the API call and roll back on failure.setDraftConfigis local-only -- does not call the API. Used by the AI creation flow to pass generated config to the wizard.
executionStore
interface RunFilters {
search?: string;
status?: ExecutionStatus | 'all';
agent_id?: number | 'all';
page: number;
page_size: number;
}
interface ExecutionStore {
executions: Execution[];
currentExecution: Execution | null;
currentSteps: ExecutionStep[];
stats: RunStats | null;
pagination: PaginationMeta | null;
filters: RunFilters;
wsConnected: boolean;
loading: LoadingState;
error: string | null;
fetchRuns: () => Promise<void>;
fetchRunStats: () => Promise<void>;
fetchRunDetail: (id: number) => Promise<void>;
fetchRunSteps: (id: number) => Promise<void>;
fetchAgentExecutions: (agentId: number, page?: number) => Promise<void>;
stopRun: (id: number) => Promise<void>;
retryRun: (id: number) => Promise<number>; // -> new execution_id
connectRunWebSocket: (executionId: number) => void;
disconnectRunWebSocket: () => void;
appendStep: (step: ExecutionStep) => void; // Called by WebSocket handler
updateExecutionStatus: (id: number, status: ExecutionStatus) => void;
setFilters: (filters: Partial<RunFilters>) => void;
reset: () => void;
}
connectRunWebSocketis called by Run Detail on mount whenstatusis non-terminal. SetswsConnected = trueand wires the WebSocket message handler to callappendStepandupdateExecutionStatus.disconnectRunWebSocketis called on component unmount or when a terminalexecution.status_changeWebSocket event arrives.appendStepappends tocurrentSteps-- steps are displayed in chronological order in the log viewer.cancelRuncallsPOST /api/v1/agents/runs/{id}/stopand callsupdateExecutionStatus(id, 'cancelled')on success.
approvalStore
interface ApprovalStore {
currentApproval: ApprovalRequest | null;
isLoading: boolean;
error: string | null;
fetchApproval: (id: number) => Promise<void>;
approveRequest: (id: number, comment?: string) => Promise<void>;
rejectRequest: (id: number, comment: string) => Promise<void>;
editAndApprove: (
id: number,
modifiedPayload: Record<string, unknown>,
comment?: string
) => Promise<void>;
clearApproval: () => void;
}
approveRequestapplies an optimistic update (setsstatus = 'approved') before the API call and rolls back on failure.rejectRequestandeditAndApprovefollow the same optimistic pattern.
templateStore
interface TemplateFilters {
search?: string;
category?: AgentCategory | 'all';
page: number;
page_size: number;
}
interface TemplateStore {
templates: Template[];
currentTemplate: Template | null;
pagination: PaginationMeta | null;
filters: TemplateFilters;
loading: LoadingState;
error: string | null;
fetchTemplates: () => Promise<void>;
fetchTemplateDetail: (id: number) => Promise<void>;
setFilters: (filters: Partial<TemplateFilters>) => void;
reset: () => void;
}
fetchTemplateDetailis called by both Template Detail and the Guided Wizard (when?template={id}is present). IfcurrentTemplatealready matches the requested ID, the store skips the network call.
monitoringStore
interface MonitoringStore {
stats: MonitoringStats | null;
throughput: ThroughputDataPoint[];
alerts: Alert[];
agentHealth: AgentHealth[];
period: '1h' | '6h' | '24h' | '7d';
wsConnected: boolean;
loading: LoadingState;
error: string | null;
lastRefreshedAt: string | null;
fetchAll: () => Promise<void>; // Parallel fetch all 4 endpoints
fetchStats: () => Promise<void>;
fetchThroughput: () => Promise<void>;
fetchAlerts: () => Promise<void>;
fetchAgentHealth: () => Promise<void>;
acknowledgeAlert: (id: number) => Promise<void>;
connectWebSocket: () => void;
disconnectWebSocket: () => void;
setPeriod: (period: MonitoringStore['period']) => void;
updateFromWebSocket: (data: MonitoringStats) => void;
reset: () => void;
}
connectWebSocketconnects toWS /api/v1/agents/ws/monitoringand callsupdateFromWebSocketon each incoming event.- When
wsConnected = false, the component pollsfetchAllevery 30 seconds via auseEffecttimer.
agentSettingsStore
interface AgentSettingsStore {
settings: WorkspaceSettings | null;
originalSettings: WorkspaceSettings | null; // for cancel / reset
isDirty: boolean;
activeTab: string;
isLoading: boolean;
isSaving: boolean;
error: string | null;
fetchSettings: () => Promise<void>;
saveGeneral: (data: WorkspaceSettings['general']) => Promise<void>;
saveCompute: (data: WorkspaceSettings['compute']) => Promise<void>;
saveGovernance: (data: WorkspaceSettings['governance']) => Promise<void>;
saveAccess: (data: WorkspaceSettings['access']) => Promise<void>;
saveNotifications: (data: WorkspaceSettings['notifications']) => Promise<void>;
saveApi: (data: WorkspaceSettings['api']) => Promise<void>;
createApiKey: (data: { name: string; permissions: Record<string, unknown> }) => Promise<ApiKey & { secret: string }>;
deleteApiKey: (id: number) => Promise<void>;
createWebhook: (data: Partial<Webhook>) => Promise<Webhook>;
updateWebhook: (id: number, data: Partial<Webhook>) => Promise<Webhook>;
deleteWebhook: (id: number) => Promise<void>;
testWebhook: (id: number) => Promise<{ status_code: number; response_time_ms: number }>;
setLocalField: (path: string, value: unknown) => void;
resetChanges: () => void;
setActiveTab: (tab: string) => void;
}
setLocalFieldmakes a local change tosettingsand setsisDirty = true. It does not call the API.resetChangesrestoressettingsfromoriginalSettingsand setsisDirty = false.isSavingis a dedicated flag separate fromisLoadingso the Save button spinner does not affect the page-level skeleton.
auditStore
interface AuditFilters {
search?: string;
event_type?: AuditEventType | 'all';
agent_id?: number | 'all';
user_id?: number | 'all';
date_from?: string | null;
date_to?: string | null;
page: number;
page_size: number;
}
interface AuditStore {
logs: AuditLogEntry[];
stats: AuditStats | null;
pagination: PaginationMeta | null;
filters: AuditFilters;
expandedRowId: number | null;
isLoading: boolean;
exporting: boolean;
error: string | null;
fetchLogs: () => Promise<void>;
fetchAuditStats: () => Promise<void>;
exportLogs: () => Promise<void>;
setFilters: (filters: Partial<AuditFilters>) => void;
toggleExpandedRow: (id: number) => void;
reset: () => void;
}
fetchLogsnever triggers an optimistic update -- audit data is read-only.exportLogssetsexporting = true, callsGET /api/v1/agents/audit/export?format=csv, then setsexporting = false. For direct-download responses it creates and clicks an<a download>element. For email-delivery responses it shows a toast.- No
update,delete, orclearLogsactions exist in this store. The absence of mutation actions is intentional and documents the write-once enforcement.
WebSocket Event Contract
Connection Endpoints
WS /api/v1/agents/ws/runs/{execution_id} -- per-execution streaming (Run Detail)
WS /api/v1/agents/ws/monitoring -- workspace-wide monitoring (Monitoring)
WS /api/v1/agents/ws/workspace -- workspace-wide event bus (Agent Library, Runs List)
Authentication: Sec-WebSocket-Protocol: Bearer {access_token}
Server-to-Client Events
execution.step_update
Fired when a reasoning turn or tool call step completes.
interface ExecutionStepUpdateEvent {
type: 'execution.step_update';
execution_id: number;
step: {
step_number: number;
step_type: StepType;
tool_name: string | null;
tool_category: ToolCategory | null;
status: 'processing' | 'completed' | 'failed' | 'blocked';
message: string | null;
governance_decision: string | null;
latency_ms: number | null;
};
timestamp: string;
}
Subscribed by: Run Detail, Agent Detail "Run Now" flow
execution.status_change
Fired when the overall execution status changes.
interface ExecutionStatusChangeEvent {
type: 'execution.status_change';
execution_id: number;
agent_id: number;
previous_status: ExecutionStatus;
new_status: ExecutionStatus;
error_message: string | null;
timestamp: string;
}
Subscribed by: Run Detail, Runs List, Agent Detail
execution.progress
Fired periodically during execution to indicate overall progress.
interface ExecutionProgressEvent {
type: 'execution.progress';
execution_id: number;
turn: number;
max_turns: number;
progress_percentage: number; // 0-100
current_phase: string; // e.g., "Analyzing data sources"
timestamp: string;
}
Subscribed by: Run Detail (progress bar), Agent Detail "Run Now" modal
approval.requested
Fired when an execution hits an approval gate.
interface ApprovalRequestedEvent {
type: 'approval.requested';
approval_id: number;
execution_id: number;
agent_id: number;
agent_name: string;
action_tool: string;
action_description: string;
reasoning_summary: string;
risk_context: string | null;
expires_at: string;
timestamp: string;
}
Subscribed by: Runs List (badge update), Agent Detail, global notification system
approval.resolved
Fired when a pending approval is resolved.
interface ApprovalResolvedEvent {
type: 'approval.resolved';
approval_id: number;
execution_id: number;
agent_id: number;
status: ApprovalStatus;
resolved_by_name: string | null;
resolution_comment: string | null;
timestamp: string;
}
Subscribed by: Runs List, Run Detail, Agent Detail
agent.status_change
Fired when an agent's lifecycle status changes.
interface AgentStatusChangeEvent {
type: 'agent.status_change';
agent_id: number;
agent_name: string;
previous_status: AgentStatus;
new_status: AgentStatus;
changed_by: string; // User display name
timestamp: string;
}
Subscribed by: Agent Library, Agent Detail, Monitoring
monitoring.stats_update
Fired every 10 seconds with live monitoring data.
interface MonitoringStatsUpdateEvent {
type: 'monitoring.stats_update';
stats: MonitoringStats;
throughput_latest: ThroughputDataPoint;
timestamp: string;
}
Subscribed by: Monitoring page
notification
General notification for any significant event.
interface NotificationEvent {
type: 'notification';
id: string;
severity: AlertSeverity;
title: string;
message: string;
agent_id: number | null;
execution_id: number | null;
action_url: string | null; // Deep link within the app
timestamp: string;
}
Subscribed by: Global notification bell in the app header
Client-to-Server Events
user_response
Sent when a user responds to a clarification or confirmation request during an execution.
interface UserResponseMessage {
type: 'user_response';
execution_id: number;
request_id: string;
response_type: 'selection' | 'confirmation' | 'input' | 'cancelled';
value: unknown;
free_text?: string;
timestamp: string;
}
Page Subscription Matrix
| Page | Events Subscribed | Connection Lifecycle |
|---|---|---|
| Agent Library | agent.status_change, notification | Global WS, connected while on page |
| Agent Detail | agent.status_change, execution.status_change, approval.requested | Global WS, per-agent filter |
| Run Detail | execution.step_update, execution.progress, execution.status_change, approval.requested, approval.resolved | Per-execution WS, opened on mount if run is active; closed on terminal event |
| Runs List | execution.status_change, approval.requested, approval.resolved | Global WS, connected while on page |
| Monitoring | monitoring.stats_update, agent.status_change | Global WS or 30s HTTP polling (fallback) |
| Settings | None | No WS |
| Audit and Logs | None | No WS (historical data) |
| Templates | None | No WS |
Error Handling Contract
Standard Error Response Shape
{
"success": false,
"status": 422,
"message": "Validation failed",
"data": null,
"error": "validation_error",
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-05-10T08:04:22Z"
}
}
Error Code Reference
| HTTP Status | Error Code | UI Treatment | User Message |
|---|---|---|---|
| 400 | validation_error | Inline field errors from details.fields array | Field-level messages |
| 400 | agent_invalid_state_transition | Inline error banner; revert optimistic update | "Agent cannot transition to that state" |
| 400 | agent_budget_exhausted | Terminal status badge in Run Detail | (shown via execution status) |
| 401 | unauthorized | Auto-refresh token; if fails, redirect to /signin | -- |
| 401 | invalid_refresh_token | Clear all auth; redirect to /signin | -- |
| 403 | forbidden | Toast (error) | "You don't have permission to perform this action" |
| 403 | governance_policy_blocked | Modal showing policy name and block reason | Policy name from details.policy_name |
| 403 | tool_acl_denied | Append error step to Run Detail log | (shown in step trace) |
| 404 | agent_not_found | Redirect to /agents with toast | "Agent not found" |
| 404 | execution_not_found | Redirect to /agents/runs with toast | "Execution not found" |
| 404 | template_not_found | Redirect to /agents/templates with toast | "Template not found" |
| 409 | conflict | Toast (warning) | "Agent is already in the requested state" |
| 409 | agent_already_running | Toast (warning) | "Agent is already executing. Wait for the current run to complete." |
| 409 | approval_already_resolved | Toast (warning); refresh approval list | "This approval was already resolved" |
| 422 | unprocessable_entity | Modal with detailed validation errors | Field-level detail from details.fields |
| 422 | trigger_config_invalid | Wizard Step 2 inline field errors | From details.fields |
| 422 | data_source_unreachable | Wizard Step 3 inline error below selector | -- |
| 422 | validation_failed | Async validation errors in wizard; navigate back to failing step | From validation polling response |
| 429 | rate_limited | Toast (warning) | "Too many requests. Please wait {retry_after} seconds." |
| 402 | workspace_quota_exceeded | Modal with quota details | "Workspace limit reached" |
| 500 | internal_error | Toast (error) | "Something went wrong. Please try again." + show request_id |
| 502, 503 | service_unavailable | Full-page error state with retry button | "The service is temporarily unavailable. Retrying..." |
Error Display Rules
- All errors surface the human-readable
messagefield. - The
meta.request_idis shown in a collapsed "Technical Details" section for support escalation. - Network errors (no response received) display: "Connection error -- check your network and try again."
- 401 responses trigger the token refresh flow. If refresh fails, the current URL is preserved as
?redirectbefore redirecting to/signin. - Error responses to optimistic mutations must always trigger a rollback to the prior state before displaying the error message.
Loading and Optimistic Update Patterns
LoadingState Design
Each store uses per-action loading flags rather than a single isLoading boolean. This allows the UI to show targeted spinners -- for example, a spinner on the Deploy button without freezing the entire Agent Detail page.
interface LoadingState {
list: boolean; // paginated list fetches
detail: boolean; // single-entity fetches
create: boolean; // POST operations
update: boolean; // PUT/PATCH operations
delete: boolean; // DELETE operations
[actionKey: string]: boolean; // named lifecycle actions: loading['deploy'] = true
}
All flags start as false. The calling action sets the relevant flag to true before the await and to false in the finally block.
Optimistic Update Pattern
Used for lifecycle transitions and approval resolutions where the API response is fast and failures are rare.
// Example: agentStore -- deploy action
async deployAgent(id: number) {
const prior = get().currentAgent;
// 1. Apply optimistic update immediately
set(state => ({
currentAgent: state.currentAgent
? { ...state.currentAgent, status: 'deployed' as AgentStatus }
: null,
loading: { ...state.loading, deploy: true }
}));
try {
const updated = await api.post<Agent>(`/api/v1/agents/${id}/deploy`);
// 2. Confirm with server response
set({ currentAgent: updated.data });
} catch (err) {
// 3. Roll back to prior state on failure
set({ currentAgent: prior });
throw err;
} finally {
set(state => ({ loading: { ...state.loading, deploy: false } }));
}
}
Optimistic Rollback Rule
Any action with an optimistic update must snapshot the prior state before modifying it. On catch, the snapshot is restored and the error is re-thrown so the calling component can display the error message. Optimistic failures are never swallowed silently.
Non-Optimistic Operations
These operations are not optimistic and display a blocking spinner:
createAgent-- the wizard is already a multi-step flow; the user expects to waitexportAuditLog--exportingflag drives a disabled button state, not an inline spinner- All
fetchXoperations -- show skeleton loaders, not optimistic state
Cache Invalidation Rules
| User Action | Invalidated Store Slices |
|---|---|
| Agent lifecycle transition (deploy, pause, resume, archive) | agentStore.currentAgent (optimistic update in place); update matching entry in agentStore.agents |
| Agent configuration save | agentStore.currentAgent, agentStore.versions |
| Agent delete | Remove from agentStore.agents; clear agentStore.currentAgent if matching |
| Manual run triggered | Prepend to executionStore.executions; invalidate agentStore.currentAgent metrics summary |
Run completed (via WebSocket execution.status_change) | executionStore.currentExecution, executionStore.executions, agent metrics summary |
| Run cancelled | executionStore.currentExecution, executionStore.executions |
| Approval resolved | approvalStore.currentApproval; update matching entry in executionStore.executions |
| Data source bound | Re-fetch GET /api/v1/agents/{id}/data-sources |
| Data source unbound | Remove from local DataSourceBinding[]; no full re-fetch needed |
| Template applied to new agent | No template cache invalidation -- templates are read-only during wizard |
| Settings tab saved | agentSettingsStore.settings, agentSettingsStore.originalSettings |
| API key created | Append to agentSettingsStore.settings.api.api_keys |
| API key deleted | Remove from agentSettingsStore.settings.api.api_keys |
| Webhook created / updated | Refresh agentSettingsStore.settings.api.webhooks |
| Audit log viewed | No invalidation -- audit data is never mutated |
| Metrics period change | executionStore metrics only; does not invalidate agentStore.agents |