rdev/internal/adapter/codeagent/registry.go
jordan 39df51defd feat: Add multi-provider code agent interface with Claude Code and OpenCode adapters
Implements weeks 1-4 of the multi-provider architecture:

Week 1 - Foundation:
- Add domain models (AgentProvider, AgentRequest, AgentEvent, AgentResult)
- Define CodeAgent port interface with Execute, Cancel, Capabilities
- Create thread-safe provider registry with first-registered default

Week 2 - Claude Code Adapter:
- Extract kubectl exec logic into CodeAgent implementation
- Parse stream-json output format (init, message, tool_use, result)
- Support session continuation via --resume flag

Week 3 - OpenCode Adapter:
- HTTP/SSE client for opencode serve API
- Session management (create, send message, abort)
- Event streaming with documented buffer rationale

Week 4 - Quality & Polish:
- Fix race condition in OpenCode Cancel method
- Add AgentRequest.Validate() with ErrPromptRequired, ErrInvalidTimeout
- Document DefaultAvailabilityTimeout constants
- Add HTTP error context for debugging

Also includes:
- Work queue system with PostgreSQL adapter
- Credential store for infrastructure secrets
- Project templates with Woodpecker CI integration
- Comprehensive test coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 09:25:51 -07:00

123 lines
2.8 KiB
Go

// Package codeagent provides the code agent registry and common utilities.
package codeagent
import (
"context"
"fmt"
"sync"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
// Registry implements port.CodeAgentRegistry with thread-safe agent management.
type Registry struct {
mu sync.RWMutex
agents map[domain.AgentProvider]port.CodeAgent
defProv domain.AgentProvider
hasAgent bool
}
// NewRegistry creates a new empty agent registry.
func NewRegistry() *Registry {
return &Registry{
agents: make(map[domain.AgentProvider]port.CodeAgent),
}
}
// Ensure Registry implements port.CodeAgentRegistry at compile time.
var _ port.CodeAgentRegistry = (*Registry)(nil)
// Register adds an agent implementation for a provider.
func (r *Registry) Register(agent port.CodeAgent) {
r.mu.Lock()
defer r.mu.Unlock()
provider := agent.Provider()
r.agents[provider] = agent
// If this is the first agent, make it the default
if !r.hasAgent {
r.defProv = provider
r.hasAgent = true
}
}
// Get returns the agent for a specific provider.
func (r *Registry) Get(provider domain.AgentProvider) port.CodeAgent {
r.mu.RLock()
defer r.mu.RUnlock()
return r.agents[provider]
}
// Default returns the default agent implementation.
func (r *Registry) Default() port.CodeAgent {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.hasAgent {
return nil
}
return r.agents[r.defProv]
}
// SetDefault sets which provider should be used as the default.
func (r *Registry) SetDefault(provider domain.AgentProvider) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.agents[provider]; !exists {
return fmt.Errorf("agent provider %q is not registered", provider)
}
r.defProv = provider
return nil
}
// Available returns all registered providers.
func (r *Registry) Available() []domain.AgentProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]domain.AgentProvider, 0, len(r.agents))
for p := range r.agents {
providers = append(providers, p)
}
return providers
}
// AvailableAgents returns all registered agents that are currently available.
func (r *Registry) AvailableAgents(ctx context.Context) []port.CodeAgent {
r.mu.RLock()
defer r.mu.RUnlock()
available := make([]port.CodeAgent, 0, len(r.agents))
for _, agent := range r.agents {
if agent.Available(ctx) {
available = append(available, agent)
}
}
return available
}
// DefaultProvider returns the current default provider.
// Returns empty string if no agents are registered.
func (r *Registry) DefaultProvider() domain.AgentProvider {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.hasAgent {
return ""
}
return r.defProv
}
// Count returns the number of registered agents.
func (r *Registry) Count() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.agents)
}