2e108b040f
* feat: session resumption with ACP session ID support - Add useSessionResumption hook for automatic session recovery on page reload - Fix ACPSessionID not being set on AgentInstance in Launch() and createInstance() - Add task.session.status and task.session.resume WebSocket actions - Fix shell terminal retry logic for resumed sessions - Fix taskSessionState reading from Zustand store instead of local state - Remove dead code: unused stream handling functions in lifecycle/manager.go - Fix test mock interfaces (CleanupStaleInstanceByTaskID, NewManager signature) - Add enhanced logging for session resumption debugging - Simplify page-client.tsx state merging logic * refactor: remove unused AgentController (dead code) - Delete agent_controller.go - entirely unused duplicate of Manager methods - Remove agentController field from Manager struct - Remove AgentController initialization in NewManager The AgentController duplicated StartAgentProcess, initializeACPSession, and PromptAgent from Manager but was never called. All agent control operations go through Manager's own methods. * refactor: remove unused MemoryRepository (dead code) - Delete memory.go - entire MemoryRepository implementation unused (replaced by SQLite repository) - Delete memory_test.go - tests for removed MemoryRepository Found via 'deadcode' tool analysis. * refactor: consolidate agentctl packages into server/client structure Reorganize the confusing dual agentctl packages: - internal/agentctl/* → internal/agentctl/server/ - internal/agent/agentctl/* → internal/agentctl/client/ Structure is now: internal/agentctl/ ├── client/ - HTTP client for talking to agentctl servers │ ├── client.go │ ├── standalone.go │ ├── acp.go │ └── launcher/ ├── server/ - agentctl server implementation (runs in containers) │ ├── api/ │ ├── adapter/ │ ├── acp/ │ ├── config/ │ ├── instance/ │ ├── process/ │ └── shell/ └── types/ - Shared types used by both client and server Also consolidated shared types (GitStatusUpdate, FileInfo, etc.) from process/types.go into the shared types/ package. * refactor: rename AgentInstance to AgentExecution for clarity Rename AgentInstance -> AgentExecution across the entire codebase to better reflect what it represents: an agent actively working on a task, not just a generic 'instance'. Changes: - AgentInstance struct -> AgentExecution - InstanceStore -> ExecutionStore (new file: execution_store.go) - RecoveredInstance -> RecoveredExecution - All method names: GetInstance* -> GetExecution*, ListInstances -> ListExecutions, etc. - All parameters: instanceID -> executionID - All local variables: instance -> execution - DTOs and JSON tags: agent_instance_id -> agent_execution_id - Database columns: agent_instance_id -> agent_execution_id Also consolidates duplicate prompt logic: - Merge sendInitialPrompt and SendPrompt into single SendPrompt method - Add validateStatus parameter to differentiate initial vs follow-up prompts Also extracts ACP session management to session.go: - Move initializeACPSession logic to SessionManager.InitializeAndPrompt - Move PromptAgent logic to SessionManager.SendPrompt - Manager now delegates to SessionManager for all prompt operations Also removes duplicate event publishing: - Remove 9 duplicate m.publish*() methods from manager.go - Use existing EventPublisher methods via m.eventPublisher.Publish*() Also extracts types to dedicated file: - Move AgentExecution, LaunchRequest, PromptResult, etc. to types.go Net reduction: ~600 lines removed from manager.go through deduplication and better separation of concerns. * fix(backend): Docker runtime now creates instance via ControlServer The Docker runtime was incorrectly creating an agentctl.Client pointing directly to port 9999 (the ControlServer port), then trying to call instance-specific APIs like /api/v1/agent/configure which only exist on per-instance servers. Fixed by making Docker runtime follow the same flow as standalone mode: - LaunchContainer now uses StandaloneCtl to create an instance via the control API, then creates Client pointing to the instance port - RecoverInstances queries the ControlServer to get the instance port - Added waitForHealth helper to wait for agentctl to be ready Also improved HTTP error handling in agentctl client: - All client methods now check HTTP status codes before JSON decoding - Better error messages include status code and response body * fix: restore session resumption imports and variables * fix: add sessionId prop back to TaskLayout
138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
// Package api provides the HTTP REST API for agentctl control server
|
|
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/kandev/kandev/internal/agentctl/server/config"
|
|
"github.com/kandev/kandev/internal/agentctl/server/instance"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ControlServer provides instance management endpoints on the control port.
|
|
// It exposes the same API regardless of deployment context (Docker or host).
|
|
// Each instance runs its own HTTP server with agent-specific endpoints.
|
|
type ControlServer struct {
|
|
cfg *config.Config
|
|
instMgr *instance.Manager
|
|
logger *logger.Logger
|
|
router *gin.Engine
|
|
}
|
|
|
|
// NewControlServer creates a new ControlServer for instance management.
|
|
func NewControlServer(cfg *config.Config, instMgr *instance.Manager, log *logger.Logger) *ControlServer {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
cs := &ControlServer{
|
|
cfg: cfg,
|
|
instMgr: instMgr,
|
|
logger: log.WithFields(zap.String("component", "control-server")),
|
|
router: gin.New(),
|
|
}
|
|
|
|
cs.setupRoutes()
|
|
return cs
|
|
}
|
|
|
|
// Router returns the HTTP handler for the control server.
|
|
func (m *ControlServer) Router() http.Handler {
|
|
return m.router
|
|
}
|
|
|
|
func (m *ControlServer) setupRoutes() {
|
|
// Health check
|
|
m.router.GET("/health", m.handleHealth)
|
|
|
|
// Instance management API - same endpoints regardless of mode
|
|
api := m.router.Group("/api/v1")
|
|
api.POST("/instances", m.handleCreateInstance)
|
|
api.GET("/instances", m.handleListInstances)
|
|
api.GET("/instances/:id", m.handleGetInstance)
|
|
api.DELETE("/instances/:id", m.handleDeleteInstance)
|
|
}
|
|
|
|
func (m *ControlServer) handleHealth(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ok",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
"max_instances": m.cfg.MaxInstances,
|
|
})
|
|
}
|
|
|
|
func (m *ControlServer) handleCreateInstance(c *gin.Context) {
|
|
var req instance.CreateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
m.logger.Warn("invalid create instance request", zap.Error(err))
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "invalid request body: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate required fields
|
|
if req.WorkspacePath == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "workspace_path is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
resp, err := m.instMgr.CreateInstance(c.Request.Context(), &req)
|
|
if err != nil {
|
|
m.logger.Error("failed to create instance", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "failed to create instance: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, resp)
|
|
}
|
|
|
|
func (m *ControlServer) handleListInstances(c *gin.Context) {
|
|
instances := m.instMgr.ListInstances()
|
|
c.JSON(http.StatusOK, instances)
|
|
}
|
|
|
|
func (m *ControlServer) handleGetInstance(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
inst, found := m.instMgr.GetInstance(id)
|
|
if !found {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "instance not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, inst.Info())
|
|
}
|
|
|
|
func (m *ControlServer) handleDeleteInstance(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
// First check if instance exists
|
|
if _, found := m.instMgr.GetInstance(id); !found {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "instance not found",
|
|
})
|
|
return
|
|
}
|
|
|
|
err := m.instMgr.StopInstance(c.Request.Context(), id)
|
|
if err != nil {
|
|
m.logger.Error("failed to stop instance", zap.String("id", id), zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "failed to stop instance: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "instance stopped successfully",
|
|
})
|
|
}
|