Files
kandev/apps/backend/internal/agentctl/api/server.go
T
jcfs 20f5f43fb3 feat: complete acp-go-sdk integration with task state updates
- Use acp-go-sdk for all ACP communication in agentctl
- Mark agent as completed when Prompt() returns successfully
- Update task state to COMPLETED when agent completes
- Update task state to FAILED when agent fails and retries exhausted
- Enhanced E2E test to verify agent actually creates files
- E2E test now validates full flow: task creation -> agent work -> file creation -> task completion
2026-01-10 16:46:35 +00:00

175 lines
4.1 KiB
Go

// Package api provides the HTTP REST API for agentctl
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/kandev/kandev/internal/agentctl/config"
"github.com/kandev/kandev/internal/agentctl/process"
"github.com/kandev/kandev/internal/common/logger"
"go.uber.org/zap"
)
// Server is the HTTP API server
type Server struct {
cfg *config.Config
procMgr *process.Manager
logger *logger.Logger
router *gin.Engine
upgrader websocket.Upgrader
}
// NewServer creates a new API server
func NewServer(cfg *config.Config, procMgr *process.Manager, log *logger.Logger) *Server {
gin.SetMode(gin.ReleaseMode)
s := &Server{
cfg: cfg,
procMgr: procMgr,
logger: log.WithFields(zap.String("component", "api-server")),
router: gin.New(),
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for container-local communication
},
},
}
s.setupRoutes()
return s
}
// Router returns the HTTP router
func (s *Server) Router() http.Handler {
return s.router
}
func (s *Server) setupRoutes() {
// Health check
s.router.GET("/health", s.handleHealth)
// Agent control
api := s.router.Group("/api/v1")
{
// Status and info
api.GET("/status", s.handleStatus)
api.GET("/info", s.handleInfo)
// Process control
api.POST("/start", s.handleStart)
api.POST("/stop", s.handleStop)
// ACP high-level methods (using acp-go-sdk)
api.POST("/acp/initialize", s.handleACPInitialize)
api.POST("/acp/session/new", s.handleACPNewSession)
api.POST("/acp/prompt", s.handleACPPrompt)
api.GET("/acp/stream", s.handleACPStreamWS)
// Output streaming
api.GET("/output", s.handleGetOutput)
api.GET("/output/stream", s.handleOutputStreamWS)
}
}
// Health check response
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
func (s *Server) handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, HealthResponse{
Status: "ok",
Timestamp: time.Now().UTC().Format(time.RFC3339),
})
}
// Status response
type StatusResponse struct {
AgentStatus string `json:"agent_status"`
ProcessInfo map[string]interface{} `json:"process_info"`
Uptime string `json:"uptime,omitempty"`
}
func (s *Server) handleStatus(c *gin.Context) {
c.JSON(http.StatusOK, StatusResponse{
AgentStatus: string(s.procMgr.Status()),
ProcessInfo: s.procMgr.GetProcessInfo(),
})
}
// Info response
type InfoResponse struct {
Version string `json:"version"`
AgentCommand string `json:"agent_command"`
WorkDir string `json:"work_dir"`
Port int `json:"port"`
}
func (s *Server) handleInfo(c *gin.Context) {
c.JSON(http.StatusOK, InfoResponse{
Version: "0.1.0",
AgentCommand: s.cfg.AgentCommand,
WorkDir: s.cfg.WorkDir,
Port: s.cfg.Port,
})
}
// Start request/response
type StartRequest struct {
// Future: could add options like custom command, env overrides
}
type StartResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
func (s *Server) handleStart(c *gin.Context) {
if err := s.procMgr.Start(c.Request.Context()); err != nil {
s.logger.Error("failed to start agent", zap.Error(err))
c.JSON(http.StatusInternalServerError, StartResponse{
Success: false,
Error: err.Error(),
})
return
}
c.JSON(http.StatusOK, StartResponse{
Success: true,
Message: "agent started",
})
}
// Stop request/response
type StopRequest struct {
Force bool `json:"force"`
}
type StopResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
func (s *Server) handleStop(c *gin.Context) {
if err := s.procMgr.Stop(c.Request.Context()); err != nil {
s.logger.Error("failed to stop agent", zap.Error(err))
c.JSON(http.StatusInternalServerError, StopResponse{
Success: false,
Error: err.Error(),
})
return
}
c.JSON(http.StatusOK, StopResponse{
Success: true,
Message: "agent stopped",
})
}