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
164 lines
4.6 KiB
Go
164 lines
4.6 KiB
Go
// Package controller provides the business logic coordination layer for the agent module.
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/kandev/kandev/internal/agent/dto"
|
|
"github.com/kandev/kandev/internal/agent/lifecycle"
|
|
"github.com/kandev/kandev/internal/agent/registry"
|
|
)
|
|
|
|
// Controller coordinates agent business logic
|
|
type Controller struct {
|
|
lifecycle *lifecycle.Manager
|
|
registry *registry.Registry
|
|
}
|
|
|
|
// NewController creates a new agent controller
|
|
func NewController(lm *lifecycle.Manager, reg *registry.Registry) *Controller {
|
|
return &Controller{
|
|
lifecycle: lm,
|
|
registry: reg,
|
|
}
|
|
}
|
|
|
|
// ListAgents returns all agent instances
|
|
func (c *Controller) ListAgents(ctx context.Context, req dto.ListAgentsRequest) (dto.ListAgentsResponse, error) {
|
|
if c.lifecycle == nil {
|
|
return dto.ListAgentsResponse{}, ErrLifecycleManagerNotAvailable
|
|
}
|
|
|
|
agents := c.lifecycle.ListExecutions()
|
|
resp := dto.ListAgentsResponse{
|
|
Agents: make([]dto.AgentDTO, 0, len(agents)),
|
|
Total: len(agents),
|
|
}
|
|
|
|
for _, a := range agents {
|
|
agent := dto.FromAgentExecution(&dto.AgentExecutionData{
|
|
ID: a.ID,
|
|
TaskID: a.TaskID,
|
|
AgentProfileID: a.AgentProfileID,
|
|
ContainerID: a.ContainerID,
|
|
Status: string(a.Status),
|
|
Progress: a.Progress,
|
|
StartedAt: a.StartedAt,
|
|
FinishedAt: a.FinishedAt,
|
|
ExitCode: a.ExitCode,
|
|
ErrorMessage: a.ErrorMessage,
|
|
})
|
|
resp.Agents = append(resp.Agents, agent)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// LaunchAgent launches a new agent instance
|
|
func (c *Controller) LaunchAgent(ctx context.Context, req dto.LaunchAgentRequest) (dto.LaunchAgentResponse, error) {
|
|
if c.lifecycle == nil {
|
|
return dto.LaunchAgentResponse{}, ErrLifecycleManagerNotAvailable
|
|
}
|
|
|
|
launchReq := &lifecycle.LaunchRequest{
|
|
TaskID: req.TaskID,
|
|
AgentProfileID: req.AgentProfileID,
|
|
WorkspacePath: req.WorkspacePath,
|
|
Env: req.Env,
|
|
}
|
|
|
|
execution, err := c.lifecycle.Launch(ctx, launchReq)
|
|
if err != nil {
|
|
return dto.LaunchAgentResponse{}, err
|
|
}
|
|
|
|
return dto.LaunchAgentResponse{
|
|
Success: true,
|
|
AgentID: execution.ID,
|
|
TaskID: req.TaskID,
|
|
}, nil
|
|
}
|
|
|
|
// GetAgentStatus returns the status of a specific agent
|
|
func (c *Controller) GetAgentStatus(ctx context.Context, req dto.GetAgentStatusRequest) (dto.AgentDTO, error) {
|
|
if c.lifecycle == nil {
|
|
return dto.AgentDTO{}, ErrLifecycleManagerNotAvailable
|
|
}
|
|
|
|
agent, found := c.lifecycle.GetExecution(req.AgentID)
|
|
if !found {
|
|
return dto.AgentDTO{}, ErrAgentNotFound
|
|
}
|
|
|
|
return dto.FromAgentExecution(&dto.AgentExecutionData{
|
|
ID: agent.ID,
|
|
TaskID: agent.TaskID,
|
|
AgentProfileID: agent.AgentProfileID,
|
|
ContainerID: agent.ContainerID,
|
|
Status: string(agent.Status),
|
|
Progress: agent.Progress,
|
|
StartedAt: agent.StartedAt,
|
|
FinishedAt: agent.FinishedAt,
|
|
ExitCode: agent.ExitCode,
|
|
ErrorMessage: agent.ErrorMessage,
|
|
}), nil
|
|
}
|
|
|
|
// GetAgentLogs returns logs for a specific agent
|
|
func (c *Controller) GetAgentLogs(ctx context.Context, req dto.GetAgentLogsRequest) (dto.GetAgentLogsResponse, error) {
|
|
if c.lifecycle == nil {
|
|
return dto.GetAgentLogsResponse{}, ErrLifecycleManagerNotAvailable
|
|
}
|
|
|
|
_, found := c.lifecycle.GetExecution(req.AgentID)
|
|
if !found {
|
|
return dto.GetAgentLogsResponse{}, ErrAgentNotFound
|
|
}
|
|
|
|
// Note: Log retrieval requires Docker client access which is not available here
|
|
return dto.GetAgentLogsResponse{
|
|
AgentID: req.AgentID,
|
|
Logs: []string{},
|
|
Message: "Use HTTP API for full log retrieval",
|
|
}, nil
|
|
}
|
|
|
|
// StopAgent stops a running agent
|
|
func (c *Controller) StopAgent(ctx context.Context, req dto.StopAgentRequest) (dto.SuccessResponse, error) {
|
|
if c.lifecycle == nil {
|
|
return dto.SuccessResponse{}, ErrLifecycleManagerNotAvailable
|
|
}
|
|
|
|
if err := c.lifecycle.StopAgent(ctx, req.AgentID, false); err != nil {
|
|
return dto.SuccessResponse{}, err
|
|
}
|
|
|
|
return dto.SuccessResponse{Success: true}, nil
|
|
}
|
|
|
|
// ListAgentTypes returns all available agent types
|
|
func (c *Controller) ListAgentTypes(ctx context.Context, req dto.ListAgentTypesRequest) (dto.ListAgentTypesResponse, error) {
|
|
if c.registry == nil {
|
|
return dto.ListAgentTypesResponse{}, ErrRegistryNotAvailable
|
|
}
|
|
|
|
types := c.registry.List()
|
|
resp := dto.ListAgentTypesResponse{
|
|
Types: make([]dto.AgentTypeDTO, 0, len(types)),
|
|
Total: len(types),
|
|
}
|
|
|
|
for _, t := range types {
|
|
resp.Types = append(resp.Types, dto.FromAgentType(&dto.AgentTypeData{
|
|
ID: t.ID,
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
Image: t.Image,
|
|
Capabilities: t.Capabilities,
|
|
Enabled: t.Enabled,
|
|
}))
|
|
}
|
|
|
|
return resp, nil
|
|
}
|