Files
kandev/apps/backend/internal/agent/controller/controller.go
T
João Salavisa 82e8c8905b feat: replace agent_type with agent_profile_id (#14)
* feat: replace agent_type with agent_profile_id

- Replace agent_type field with agent_profile_id across backend and frontend
- Add ProfileResolver interface and StoreProfileResolver implementation
- Update task creation/update to use agent_profile_id
- Change default agent runtime from docker to standalone
- Fix workspace state types to include all required fields
- Fix websocket handlers type mismatches for environments, executors, workspaces
- Update integration tests to use agent_profile_id

* feat: pass model as CLI argument to agents

- Add ModelFlag field to AgentTypeConfig for specifying the CLI flag
- Set ModelFlag to '--model' for all agents (auggie, claude, codex, gemini, opencode, copilot)
- Update launchStandalone to append model flag to agent command
- Update launchDocker/buildContainerConfig to append model flag to container command
- Model is passed when both agent supports ModelFlag and profile has Model configured

* fix: make WebSocket message handling async to prevent blocking

When a long-running handler like orchestrator.prompt was executing,
it blocked the ReadPump loop, preventing other requests (like
workspace.tree.get) from being processed until the handler completed.

Now each message is handled in its own goroutine, allowing concurrent
message handling so file tree requests work while the agent is working.

* feat: show task description as first message in chat

- Add 'Task' label with distinct styling (lighter background, border)
- Pass taskDescription from page to TaskChatPanel component
- Differentiate from regular user messages visually
2026-01-14 12:29:58 +00:00

165 lines
4.5 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.ListInstances()
resp := dto.ListAgentsResponse{
Agents: make([]dto.AgentDTO, 0, len(agents)),
Total: len(agents),
}
for _, a := range agents {
agent := dto.FromAgentInstance(&dto.AgentInstanceData{
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,
}
instance, err := c.lifecycle.Launch(ctx, launchReq)
if err != nil {
return dto.LaunchAgentResponse{}, err
}
return dto.LaunchAgentResponse{
Success: true,
AgentID: instance.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.GetInstance(req.AgentID)
if !found {
return dto.AgentDTO{}, ErrAgentNotFound
}
return dto.FromAgentInstance(&dto.AgentInstanceData{
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.GetInstance(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
}