rdev/internal/port/work_queue.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

216 lines
6.2 KiB
Go

// Package port defines interfaces (ports) for external dependencies.
package port
import (
"context"
"time"
)
// WorkQueue defines operations for the worker pool task queue.
// Unlike CommandQueue (project-specific claudebox commands), WorkQueue
// supports generic tasks that any worker in the pool can claim and execute.
type WorkQueue interface {
// Enqueue adds a task to the queue.
// Returns the task ID.
Enqueue(ctx context.Context, task *WorkTask) (string, error)
// Dequeue atomically claims the next available task for a worker.
// Uses FOR UPDATE SKIP LOCKED for concurrent worker safety.
// Returns nil if no tasks are available.
Dequeue(ctx context.Context, workerID string) (*WorkTask, error)
// Complete marks a task as successfully completed with results.
Complete(ctx context.Context, taskID string, result *WorkResult) error
// Fail marks a task as failed with an error message.
// If retry_count < max_retries, the task will be re-queued as pending.
Fail(ctx context.Context, taskID string, errMsg string) error
// Cancel marks a pending task as cancelled.
// Returns an error if the task is not in pending status.
Cancel(ctx context.Context, taskID string) error
// GetTask retrieves a task by ID.
GetTask(ctx context.Context, taskID string) (*WorkTask, error)
// ListByProject returns tasks for a project with optional status filter and pagination.
ListByProject(ctx context.Context, projectID string, status *WorkTaskStatus, opts WorkListOptions) (*WorkListResult, error)
// GetStats returns queue statistics.
GetStats(ctx context.Context) (*WorkQueueStats, error)
// CleanupOld removes completed/failed/cancelled tasks older than the specified duration.
CleanupOld(ctx context.Context, olderThan time.Duration) (int64, error)
// RequeueStale re-queues tasks that have been running longer than the timeout.
// This handles workers that crashed without reporting completion.
RequeueStale(ctx context.Context, timeout time.Duration) (int64, error)
}
// WorkTaskStatus represents the status of a work task.
type WorkTaskStatus string
const (
WorkTaskStatusPending WorkTaskStatus = "pending"
WorkTaskStatusRunning WorkTaskStatus = "running"
WorkTaskStatusCompleted WorkTaskStatus = "completed"
WorkTaskStatusFailed WorkTaskStatus = "failed"
WorkTaskStatusCancelled WorkTaskStatus = "cancelled"
)
// IsValid returns true if the status is a known valid status.
func (s WorkTaskStatus) IsValid() bool {
switch s {
case WorkTaskStatusPending, WorkTaskStatusRunning, WorkTaskStatusCompleted,
WorkTaskStatusFailed, WorkTaskStatusCancelled:
return true
}
return false
}
// WorkTaskType represents the type of work task.
type WorkTaskType string
const (
WorkTaskTypeBuild WorkTaskType = "build"
WorkTaskTypeTest WorkTaskType = "test"
WorkTaskTypeDeploy WorkTaskType = "deploy"
WorkTaskTypeCustom WorkTaskType = "custom"
)
// IsValid returns true if the task type is a known valid type.
func (t WorkTaskType) IsValid() bool {
switch t {
case WorkTaskTypeBuild, WorkTaskTypeTest, WorkTaskTypeDeploy, WorkTaskTypeCustom:
return true
}
return false
}
// WorkTask represents a task in the work queue.
type WorkTask struct {
// ID is the unique task identifier.
ID string
// ProjectID is the project this task belongs to.
ProjectID string
// Type is the task type (build, test, deploy, custom).
Type WorkTaskType
// Spec contains task-specific parameters.
// For build tasks: template, prompt, variables, auto_deploy, git_url
// For test tasks: test_command, git_url
// For deploy tasks: image, replicas, env
Spec map[string]any
// Status is the current task status.
Status WorkTaskStatus
// Priority determines execution order (higher = more urgent).
Priority int
// WorkerID is the ID of the worker that claimed this task.
WorkerID string
// CallbackURL is the webhook URL for completion notification.
CallbackURL string
// CreatedAt is when the task was created.
CreatedAt time.Time
// StartedAt is when a worker started executing the task.
StartedAt *time.Time
// CompletedAt is when the task finished (success or failure).
CompletedAt *time.Time
// Result contains the task output (if completed).
Result *WorkResult
// Error contains the error message (if failed).
Error string
// RetryCount is the number of retry attempts.
RetryCount int
// MaxRetries is the maximum allowed retry attempts.
MaxRetries int
}
// WorkResult contains the result of a completed task.
type WorkResult struct {
// Output is the main output from task execution.
Output string `json:"output,omitempty"`
// Artifacts contains named artifacts from the task.
// For build tasks: commit_sha, deploy_url, etc.
Artifacts map[string]string `json:"artifacts,omitempty"`
}
// WorkQueueStats contains queue statistics.
type WorkQueueStats struct {
// Pending is the count of pending tasks.
Pending int64 `json:"pending"`
// Running is the count of running tasks.
Running int64 `json:"running"`
// Completed is the count of completed tasks (last 24h).
Completed int64 `json:"completed"`
// Failed is the count of failed tasks (last 24h).
Failed int64 `json:"failed"`
// Cancelled is the count of cancelled tasks (last 24h).
Cancelled int64 `json:"cancelled"`
// OldestPending is the age of the oldest pending task.
OldestPending *time.Duration `json:"oldest_pending,omitempty"`
}
// WorkListOptions contains pagination options for listing tasks.
type WorkListOptions struct {
// Limit is the maximum number of tasks to return (default: 50, max: 100).
Limit int
// Offset is the number of tasks to skip (for pagination).
Offset int
}
// DefaultWorkListOptions returns options with default values.
func DefaultWorkListOptions() WorkListOptions {
return WorkListOptions{
Limit: 50,
Offset: 0,
}
}
// Normalize applies defaults and limits to the options.
func (o *WorkListOptions) Normalize() {
if o.Limit <= 0 {
o.Limit = 50
}
if o.Limit > 100 {
o.Limit = 100
}
if o.Offset < 0 {
o.Offset = 0
}
}
// WorkListResult contains paginated task results.
type WorkListResult struct {
// Tasks is the list of tasks.
Tasks []*WorkTask
// Total is the total count of matching tasks (for pagination metadata).
Total int64
// Limit is the limit that was applied.
Limit int
// Offset is the offset that was applied.
Offset int
}