Files
kandev/backend/internal/task/service/service.go
T
jcfs ba919305c2 Initial commit: Kandev with SQLite persistence and agent integration
Features:
- Unified Go binary running Task Service, Agent Manager, and Orchestrator
- SQLite persistence for boards, columns, and tasks
- Docker-based agent execution with ACP message streaming
- Augment Agent integration with session resumption support
- RESTful API for all operations (port 8080)
- In-memory event bus for service communication

Architecture:
- cmd/kandev: Single entry point for all services
- internal/task: Task, board, and column management
- internal/agent: Docker container lifecycle and ACP streaming
- internal/orchestrator: Agent coordination and monitoring
- internal/events: In-memory pub/sub event bus

Documentation:
- README.md: Project overview and quick start
- docs/ARCHITECTURE.md: System architecture (current + planned)
- docs/GETTING_STARTED.md: Development setup guide
- docs/IMPLEMENTATION_PLAN.md: Original microservices plan
- backend/NEXT_STEPS.md: Development roadmap
2026-01-10 01:30:24 +00:00

415 lines
12 KiB
Go

package service
import (
"context"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/task/repository"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
// Service provides task business logic
type Service struct {
repo repository.Repository
eventBus bus.EventBus
logger *logger.Logger
}
// NewService creates a new task service
func NewService(repo repository.Repository, eventBus bus.EventBus, log *logger.Logger) *Service {
return &Service{
repo: repo,
eventBus: eventBus,
logger: log,
}
}
// Request types
// CreateTaskRequest contains the data for creating a new task
type CreateTaskRequest struct {
BoardID string `json:"board_id"`
ColumnID string `json:"column_id"`
Title string `json:"title"`
Description string `json:"description"`
Priority int `json:"priority"`
AgentType string `json:"agent_type,omitempty"`
AssignedTo string `json:"assigned_to,omitempty"`
Position int `json:"position"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// UpdateTaskRequest contains the data for updating a task
type UpdateTaskRequest struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Priority *int `json:"priority,omitempty"`
AgentType *string `json:"agent_type,omitempty"`
AssignedTo *string `json:"assigned_to,omitempty"`
Position *int `json:"position,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// CreateBoardRequest contains the data for creating a new board
type CreateBoardRequest struct {
Name string `json:"name"`
Description string `json:"description"`
OwnerID string `json:"owner_id"`
}
// UpdateBoardRequest contains the data for updating a board
type UpdateBoardRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
// CreateColumnRequest contains the data for creating a new column
type CreateColumnRequest struct {
BoardID string `json:"board_id"`
Name string `json:"name"`
Position int `json:"position"`
State v1.TaskState `json:"state"`
}
// Task operations
// CreateTask creates a new task and publishes a task.created event
func (s *Service) CreateTask(ctx context.Context, req *CreateTaskRequest) (*models.Task, error) {
task := &models.Task{
ID: uuid.New().String(),
BoardID: req.BoardID,
ColumnID: req.ColumnID,
Title: req.Title,
Description: req.Description,
State: v1.TaskStateTODO,
Priority: req.Priority,
AgentType: req.AgentType,
AssignedTo: req.AssignedTo,
Position: req.Position,
Metadata: req.Metadata,
}
if err := s.repo.CreateTask(ctx, task); err != nil {
s.logger.Error("failed to create task", zap.Error(err))
return nil, err
}
s.publishTaskEvent(ctx, events.TaskCreated, task, nil)
s.logger.Info("task created", zap.String("task_id", task.ID), zap.String("title", task.Title))
return task, nil
}
// GetTask retrieves a task by ID
func (s *Service) GetTask(ctx context.Context, id string) (*models.Task, error) {
return s.repo.GetTask(ctx, id)
}
// UpdateTask updates an existing task and publishes a task.updated event
func (s *Service) UpdateTask(ctx context.Context, id string, req *UpdateTaskRequest) (*models.Task, error) {
task, err := s.repo.GetTask(ctx, id)
if err != nil {
return nil, err
}
if req.Title != nil {
task.Title = *req.Title
}
if req.Description != nil {
task.Description = *req.Description
}
if req.Priority != nil {
task.Priority = *req.Priority
}
if req.AgentType != nil {
task.AgentType = *req.AgentType
}
if req.AssignedTo != nil {
task.AssignedTo = *req.AssignedTo
}
if req.Position != nil {
task.Position = *req.Position
}
if req.Metadata != nil {
task.Metadata = req.Metadata
}
task.UpdatedAt = time.Now().UTC()
if err := s.repo.UpdateTask(ctx, task); err != nil {
s.logger.Error("failed to update task", zap.String("task_id", id), zap.Error(err))
return nil, err
}
s.publishTaskEvent(ctx, events.TaskUpdated, task, nil)
s.logger.Info("task updated", zap.String("task_id", task.ID))
return task, nil
}
// DeleteTask deletes a task and publishes a task.deleted event
func (s *Service) DeleteTask(ctx context.Context, id string) error {
task, err := s.repo.GetTask(ctx, id)
if err != nil {
return err
}
if err := s.repo.DeleteTask(ctx, id); err != nil {
s.logger.Error("failed to delete task", zap.String("task_id", id), zap.Error(err))
return err
}
s.publishTaskEvent(ctx, events.TaskDeleted, task, nil)
s.logger.Info("task deleted", zap.String("task_id", id))
return nil
}
// ListTasks returns all tasks for a board
func (s *Service) ListTasks(ctx context.Context, boardID string) ([]*models.Task, error) {
return s.repo.ListTasks(ctx, boardID)
}
// UpdateTaskState updates the state of a task and publishes a task.state_changed event
func (s *Service) UpdateTaskState(ctx context.Context, id string, state v1.TaskState) (*models.Task, error) {
task, err := s.repo.GetTask(ctx, id)
if err != nil {
return nil, err
}
oldState := task.State
if err := s.repo.UpdateTaskState(ctx, id, state); err != nil {
s.logger.Error("failed to update task state", zap.String("task_id", id), zap.Error(err))
return nil, err
}
// Reload task to get updated state
task, err = s.repo.GetTask(ctx, id)
if err != nil {
return nil, err
}
s.publishTaskEvent(ctx, events.TaskStateChanged, task, &oldState)
s.logger.Info("task state changed",
zap.String("task_id", id),
zap.String("old_state", string(oldState)),
zap.String("new_state", string(state)))
return task, nil
}
// UpdateTaskMetadata updates only the metadata of a task (merges with existing)
func (s *Service) UpdateTaskMetadata(ctx context.Context, id string, metadata map[string]interface{}) (*models.Task, error) {
task, err := s.repo.GetTask(ctx, id)
if err != nil {
return nil, err
}
// Merge metadata (existing keys are preserved, new keys are added/updated)
if task.Metadata == nil {
task.Metadata = make(map[string]interface{})
}
for k, v := range metadata {
task.Metadata[k] = v
}
task.UpdatedAt = time.Now().UTC()
if err := s.repo.UpdateTask(ctx, task); err != nil {
s.logger.Error("failed to update task metadata", zap.String("task_id", id), zap.Error(err))
return nil, err
}
s.logger.Debug("task metadata updated", zap.String("task_id", id), zap.Any("metadata", metadata))
return task, nil
}
// MoveTask moves a task to a different column and position
func (s *Service) MoveTask(ctx context.Context, id string, columnID string, position int) (*models.Task, error) {
task, err := s.repo.GetTask(ctx, id)
if err != nil {
return nil, err
}
// Get the column to determine the new state
column, err := s.repo.GetColumn(ctx, columnID)
if err != nil {
return nil, err
}
oldState := task.State
task.ColumnID = columnID
task.Position = position
task.State = column.State
task.UpdatedAt = time.Now().UTC()
if err := s.repo.UpdateTask(ctx, task); err != nil {
s.logger.Error("failed to move task", zap.String("task_id", id), zap.Error(err))
return nil, err
}
// Publish state_changed event if state changed, otherwise just updated
if oldState != task.State {
s.publishTaskEvent(ctx, events.TaskStateChanged, task, &oldState)
} else {
s.publishTaskEvent(ctx, events.TaskUpdated, task, nil)
}
s.logger.Info("task moved",
zap.String("task_id", id),
zap.String("column_id", columnID),
zap.Int("position", position))
return task, nil
}
// Board operations
// CreateBoard creates a new board
func (s *Service) CreateBoard(ctx context.Context, req *CreateBoardRequest) (*models.Board, error) {
board := &models.Board{
ID: uuid.New().String(),
Name: req.Name,
Description: req.Description,
OwnerID: req.OwnerID,
}
if err := s.repo.CreateBoard(ctx, board); err != nil {
s.logger.Error("failed to create board", zap.Error(err))
return nil, err
}
s.logger.Info("board created", zap.String("board_id", board.ID), zap.String("name", board.Name))
return board, nil
}
// GetBoard retrieves a board by ID
func (s *Service) GetBoard(ctx context.Context, id string) (*models.Board, error) {
return s.repo.GetBoard(ctx, id)
}
// UpdateBoard updates an existing board
func (s *Service) UpdateBoard(ctx context.Context, id string, req *UpdateBoardRequest) (*models.Board, error) {
board, err := s.repo.GetBoard(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
board.Name = *req.Name
}
if req.Description != nil {
board.Description = *req.Description
}
board.UpdatedAt = time.Now().UTC()
if err := s.repo.UpdateBoard(ctx, board); err != nil {
s.logger.Error("failed to update board", zap.String("board_id", id), zap.Error(err))
return nil, err
}
s.logger.Info("board updated", zap.String("board_id", board.ID))
return board, nil
}
// DeleteBoard deletes a board
func (s *Service) DeleteBoard(ctx context.Context, id string) error {
if err := s.repo.DeleteBoard(ctx, id); err != nil {
s.logger.Error("failed to delete board", zap.String("board_id", id), zap.Error(err))
return err
}
s.logger.Info("board deleted", zap.String("board_id", id))
return nil
}
// ListBoards returns all boards
func (s *Service) ListBoards(ctx context.Context) ([]*models.Board, error) {
return s.repo.ListBoards(ctx)
}
// Column operations
// CreateColumn creates a new column
func (s *Service) CreateColumn(ctx context.Context, req *CreateColumnRequest) (*models.Column, error) {
column := &models.Column{
ID: uuid.New().String(),
BoardID: req.BoardID,
Name: req.Name,
Position: req.Position,
State: req.State,
}
if err := s.repo.CreateColumn(ctx, column); err != nil {
s.logger.Error("failed to create column", zap.Error(err))
return nil, err
}
s.logger.Info("column created",
zap.String("column_id", column.ID),
zap.String("board_id", column.BoardID),
zap.String("name", column.Name))
return column, nil
}
// GetColumn retrieves a column by ID
func (s *Service) GetColumn(ctx context.Context, id string) (*models.Column, error) {
return s.repo.GetColumn(ctx, id)
}
// ListColumns returns all columns for a board
func (s *Service) ListColumns(ctx context.Context, boardID string) ([]*models.Column, error) {
return s.repo.ListColumns(ctx, boardID)
}
// publishTaskEvent publishes task events to the event bus
func (s *Service) publishTaskEvent(ctx context.Context, eventType string, task *models.Task, oldState *v1.TaskState) {
if s.eventBus == nil {
return
}
data := map[string]interface{}{
"task_id": task.ID,
"board_id": task.BoardID,
"column_id": task.ColumnID,
"title": task.Title,
"description": task.Description,
"state": string(task.State),
"priority": task.Priority,
"position": task.Position,
"created_at": task.CreatedAt.Format(time.RFC3339),
"updated_at": task.UpdatedAt.Format(time.RFC3339),
}
if task.AgentType != "" {
data["agent_type"] = task.AgentType
}
if task.AssignedTo != "" {
data["assigned_to"] = task.AssignedTo
}
if task.Metadata != nil {
data["metadata"] = task.Metadata
}
if oldState != nil {
data["old_state"] = string(*oldState)
data["new_state"] = string(task.State)
}
event := bus.NewEvent(eventType, "task-service", data)
if err := s.eventBus.Publish(ctx, eventType, event); err != nil {
s.logger.Error("failed to publish task event",
zap.String("event_type", eventType),
zap.String("task_id", task.ID),
zap.Error(err))
}
}