// Package handlers provides HTTP handlers for the rdev API. package handlers import ( "net/http" "time" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/pkg/api" ) // AgentsHandler handles code agent management endpoints. type AgentsHandler struct { registry port.CodeAgentRegistry } // NewAgentsHandler creates a new agents handler. func NewAgentsHandler(registry port.CodeAgentRegistry) *AgentsHandler { return &AgentsHandler{ registry: registry, } } // Mount registers the agent routes. func (h *AgentsHandler) Mount(r api.Router) { r.Route("/agents", func(r chi.Router) { // Read operations r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)).Get("/", h.List) r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)).Get("/health", h.Health) r.With(auth.RequireScope(auth.ScopeProjectsRead, auth.ScopeAdmin)).Get("/{provider}", h.GetCapabilities) // Write operations r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)).Post("/default", h.SetDefault) }) } // AgentDTO is the data transfer object for code agents. type AgentDTO struct { Provider string `json:"provider"` Name string `json:"name"` Available bool `json:"available"` Default bool `json:"default"` Models []string `json:"supported_models,omitempty"` DefaultModel string `json:"default_model,omitempty"` } // AgentCapabilitiesDTO is the DTO for agent capabilities. type AgentCapabilitiesDTO struct { Provider string `json:"provider"` SupportsSessionContinuation bool `json:"supports_session_continuation"` SupportsModelSelection bool `json:"supports_model_selection"` SupportsToolControl bool `json:"supports_tool_control"` SupportsStreaming bool `json:"supports_streaming"` SupportedModels []string `json:"supported_models"` DefaultModel string `json:"default_model"` MaxPromptLength int `json:"max_prompt_length,omitempty"` } // ListAgentsResponse is the response for GET /agents. type ListAgentsResponse struct { Agents []AgentDTO `json:"agents"` DefaultAgent string `json:"default_agent"` TotalAgents int `json:"total_agents"` AvailableCount int `json:"available_count"` } // List returns all registered code agents and their status. // GET /agents func (h *AgentsHandler) List(w http.ResponseWriter, r *http.Request) { if h.registry == nil { api.WriteSuccess(w, r, ListAgentsResponse{ Agents: []AgentDTO{}, DefaultAgent: "", TotalAgents: 0, AvailableCount: 0, }) return } providers := h.registry.Available() defaultProvider := h.registry.DefaultProvider() // Check availability for each agent availableAgents := h.registry.AvailableAgents(r.Context()) availableSet := make(map[domain.AgentProvider]bool) for _, agent := range availableAgents { availableSet[agent.Provider()] = true } agents := make([]AgentDTO, 0, len(providers)) availableCount := 0 for _, provider := range providers { agent := h.registry.Get(provider) if agent == nil { continue } caps := agent.Capabilities() isAvailable := availableSet[provider] isDefault := provider == defaultProvider if isAvailable { availableCount++ } agents = append(agents, AgentDTO{ Provider: string(provider), Name: agent.Name(), Available: isAvailable, Default: isDefault, Models: caps.SupportedModels, DefaultModel: caps.DefaultModel, }) } api.WriteSuccess(w, r, ListAgentsResponse{ Agents: agents, DefaultAgent: string(defaultProvider), TotalAgents: len(agents), AvailableCount: availableCount, }) } // GetCapabilities returns the capabilities of a specific agent. // GET /agents/{provider} func (h *AgentsHandler) GetCapabilities(w http.ResponseWriter, r *http.Request) { providerStr := chi.URLParam(r, "provider") provider := domain.AgentProvider(providerStr) if h.registry == nil { api.WriteNotFound(w, r, "no agents registered") return } agent := h.registry.Get(provider) if agent == nil { api.WriteNotFound(w, r, "agent not found: "+providerStr) return } caps := agent.Capabilities() api.WriteSuccess(w, r, AgentCapabilitiesDTO{ Provider: string(caps.Provider), SupportsSessionContinuation: caps.SupportsSessionContinuation, SupportsModelSelection: caps.SupportsModelSelection, SupportsToolControl: caps.SupportsToolControl, SupportsStreaming: caps.SupportsStreaming, SupportedModels: caps.SupportedModels, DefaultModel: caps.DefaultModel, MaxPromptLength: caps.MaxPromptLength, }) } // AgentHealthDTO represents the health status of a single agent. type AgentHealthDTO struct { Provider string `json:"provider"` Name string `json:"name"` Healthy bool `json:"healthy"` Message string `json:"message"` Latency string `json:"latency"` CheckedAt string `json:"checked_at"` } // AgentHealthResponse is the response for GET /agents/health. type AgentHealthResponse struct { Agents []AgentHealthDTO `json:"agents"` HealthyCount int `json:"healthy_count"` TotalCount int `json:"total_count"` DefaultAgent string `json:"default_agent"` DefaultHealth bool `json:"default_healthy"` } // Health returns the health status of all registered code agents. // GET /agents/health func (h *AgentsHandler) Health(w http.ResponseWriter, r *http.Request) { if h.registry == nil { api.WriteSuccess(w, r, AgentHealthResponse{ Agents: []AgentHealthDTO{}, HealthyCount: 0, TotalCount: 0, }) return } providers := h.registry.Available() defaultProvider := h.registry.DefaultProvider() agents := make([]AgentHealthDTO, 0, len(providers)) healthyCount := 0 defaultHealthy := false for _, provider := range providers { agent := h.registry.Get(provider) if agent == nil { continue } start := time.Now() healthy := agent.Available(r.Context()) latency := time.Since(start) msg := "available" if !healthy { msg = "unavailable" } if healthy { healthyCount++ } if provider == defaultProvider { defaultHealthy = healthy } agents = append(agents, AgentHealthDTO{ Provider: string(provider), Name: agent.Name(), Healthy: healthy, Message: msg, Latency: latency.String(), CheckedAt: time.Now().UTC().Format(time.RFC3339), }) } api.WriteSuccess(w, r, AgentHealthResponse{ Agents: agents, HealthyCount: healthyCount, TotalCount: len(agents), DefaultAgent: string(defaultProvider), DefaultHealth: defaultHealthy, }) } // SetDefaultRequest is the request body for POST /agents/default. type SetDefaultRequest struct { Provider string `json:"provider"` } // SetDefault changes the default code agent. // POST /agents/default func (h *AgentsHandler) SetDefault(w http.ResponseWriter, r *http.Request) { if h.registry == nil { api.WriteBadRequest(w, r, "no agents registered") return } var req SetDefaultRequest if err := api.DecodeJSON(r, &req); err != nil { api.WriteBadRequest(w, r, "invalid request body") return } if req.Provider == "" { api.WriteBadRequest(w, r, "provider is required") return } provider := domain.AgentProvider(req.Provider) if err := h.registry.SetDefault(provider); err != nil { api.WriteBadRequest(w, r, err.Error()) return } api.WriteSuccess(w, r, map[string]any{ "default_agent": req.Provider, "message": "default agent updated", }) }