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
This commit is contained in:
@@ -288,6 +288,7 @@ func main() {
|
||||
// Lifecycle Manager (uses agentctl for agent communication)
|
||||
lifecycleMgr = lifecycle.NewManager(dockerClient, agentRegistry, eventBus, cfg.Agent, log)
|
||||
lifecycleMgr.SetCredentialsManager(credsMgr)
|
||||
lifecycleMgr.SetProfileResolver(lifecycle.NewStoreProfileResolver(agentSettingsRepo))
|
||||
|
||||
if err := lifecycleMgr.Start(ctx); err != nil {
|
||||
log.Fatal("Failed to start lifecycle manager", zap.Error(err))
|
||||
@@ -786,7 +787,7 @@ func (a *lifecycleAdapter) LaunchAgent(ctx context.Context, req *executor.Launch
|
||||
launchReq := &lifecycle.LaunchRequest{
|
||||
TaskID: req.TaskID,
|
||||
TaskTitle: req.TaskTitle,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
WorkspacePath: req.RepositoryURL, // May be empty - lifecycle manager handles this
|
||||
TaskDescription: req.TaskDescription,
|
||||
Metadata: req.Metadata,
|
||||
@@ -839,15 +840,15 @@ func (a *lifecycleAdapter) GetAgentStatus(ctx context.Context, agentInstanceID s
|
||||
containerID := instance.ContainerID
|
||||
now := time.Now()
|
||||
result := &v1.AgentInstance{
|
||||
ID: instance.ID,
|
||||
TaskID: instance.TaskID,
|
||||
AgentType: instance.AgentType,
|
||||
ContainerID: &containerID,
|
||||
Status: instance.Status,
|
||||
StartedAt: &instance.StartedAt,
|
||||
StoppedAt: instance.FinishedAt,
|
||||
CreatedAt: instance.StartedAt,
|
||||
UpdatedAt: now,
|
||||
ID: instance.ID,
|
||||
TaskID: instance.TaskID,
|
||||
AgentProfileID: instance.AgentProfileID,
|
||||
ContainerID: &containerID,
|
||||
Status: instance.Status,
|
||||
StartedAt: &instance.StartedAt,
|
||||
StoppedAt: instance.FinishedAt,
|
||||
CreatedAt: instance.StartedAt,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if instance.ExitCode != nil {
|
||||
@@ -893,10 +894,10 @@ func (a *lifecycleAdapter) GetRecoveredInstances() []executor.RecoveredInstanceI
|
||||
result := make([]executor.RecoveredInstanceInfo, len(recovered))
|
||||
for i, r := range recovered {
|
||||
result[i] = executor.RecoveredInstanceInfo{
|
||||
InstanceID: r.InstanceID,
|
||||
TaskID: r.TaskID,
|
||||
ContainerID: r.ContainerID,
|
||||
AgentType: r.AgentType,
|
||||
InstanceID: r.InstanceID,
|
||||
TaskID: r.TaskID,
|
||||
ContainerID: r.ContainerID,
|
||||
AgentProfileID: r.AgentProfileID,
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -37,16 +37,16 @@ func (c *Controller) ListAgents(ctx context.Context, req dto.ListAgentsRequest)
|
||||
|
||||
for _, a := range agents {
|
||||
agent := dto.FromAgentInstance(&dto.AgentInstanceData{
|
||||
ID: a.ID,
|
||||
TaskID: a.TaskID,
|
||||
AgentType: a.AgentType,
|
||||
ContainerID: a.ContainerID,
|
||||
Status: string(a.Status),
|
||||
Progress: a.Progress,
|
||||
StartedAt: a.StartedAt,
|
||||
FinishedAt: a.FinishedAt,
|
||||
ExitCode: a.ExitCode,
|
||||
ErrorMessage: a.ErrorMessage,
|
||||
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)
|
||||
}
|
||||
@@ -61,10 +61,10 @@ func (c *Controller) LaunchAgent(ctx context.Context, req dto.LaunchAgentRequest
|
||||
}
|
||||
|
||||
launchReq := &lifecycle.LaunchRequest{
|
||||
TaskID: req.TaskID,
|
||||
AgentType: req.AgentType,
|
||||
WorkspacePath: req.WorkspacePath,
|
||||
Env: req.Env,
|
||||
TaskID: req.TaskID,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
WorkspacePath: req.WorkspacePath,
|
||||
Env: req.Env,
|
||||
}
|
||||
|
||||
instance, err := c.lifecycle.Launch(ctx, launchReq)
|
||||
@@ -91,16 +91,16 @@ func (c *Controller) GetAgentStatus(ctx context.Context, req dto.GetAgentStatusR
|
||||
}
|
||||
|
||||
return dto.FromAgentInstance(&dto.AgentInstanceData{
|
||||
ID: agent.ID,
|
||||
TaskID: agent.TaskID,
|
||||
AgentType: agent.AgentType,
|
||||
ContainerID: agent.ContainerID,
|
||||
Status: string(agent.Status),
|
||||
Progress: agent.Progress,
|
||||
StartedAt: agent.StartedAt,
|
||||
FinishedAt: agent.FinishedAt,
|
||||
ExitCode: agent.ExitCode,
|
||||
ErrorMessage: agent.ErrorMessage,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@ type ListAgentsRequest struct{}
|
||||
|
||||
// AgentDTO represents an agent instance
|
||||
type AgentDTO struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type"`
|
||||
ContainerID string `json:"container_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
StartedAt string `json:"started_at"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentProfileID string `json:"agent_profile_id"`
|
||||
ContainerID string `json:"container_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
StartedAt string `json:"started_at"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ListAgentsResponse is the response for agent.list
|
||||
@@ -29,10 +29,10 @@ type ListAgentsResponse struct {
|
||||
|
||||
// LaunchAgentRequest is the payload for agent.launch
|
||||
type LaunchAgentRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type"`
|
||||
WorkspacePath string `json:"workspace_path"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentProfileID string `json:"agent_profile_id"`
|
||||
WorkspacePath string `json:"workspace_path"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// LaunchAgentResponse is the response for agent.launch
|
||||
@@ -91,13 +91,13 @@ type ListAgentTypesResponse struct {
|
||||
// FromAgentInstance converts a lifecycle.AgentInstance to AgentDTO
|
||||
func FromAgentInstance(a *AgentInstanceData) AgentDTO {
|
||||
agent := AgentDTO{
|
||||
ID: a.ID,
|
||||
TaskID: a.TaskID,
|
||||
AgentType: a.AgentType,
|
||||
ContainerID: a.ContainerID,
|
||||
Status: a.Status,
|
||||
Progress: a.Progress,
|
||||
StartedAt: a.StartedAt.Format("2006-01-02T15:04:05Z"),
|
||||
ID: a.ID,
|
||||
TaskID: a.TaskID,
|
||||
AgentProfileID: a.AgentProfileID,
|
||||
ContainerID: a.ContainerID,
|
||||
Status: a.Status,
|
||||
Progress: a.Progress,
|
||||
StartedAt: a.StartedAt.Format("2006-01-02T15:04:05Z"),
|
||||
}
|
||||
if a.FinishedAt != nil && !a.FinishedAt.IsZero() {
|
||||
agent.FinishedAt = a.FinishedAt.Format("2006-01-02T15:04:05Z")
|
||||
@@ -113,16 +113,16 @@ func FromAgentInstance(a *AgentInstanceData) AgentDTO {
|
||||
|
||||
// AgentInstanceData is the input data for FromAgentInstance (avoids circular import)
|
||||
type AgentInstanceData struct {
|
||||
ID string
|
||||
TaskID string
|
||||
AgentType string
|
||||
ContainerID string
|
||||
Status string
|
||||
Progress int
|
||||
StartedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
ExitCode *int
|
||||
ErrorMessage string
|
||||
ID string
|
||||
TaskID string
|
||||
AgentProfileID string
|
||||
ContainerID string
|
||||
Status string
|
||||
Progress int
|
||||
StartedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
ExitCode *int
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// FromAgentType converts a registry.AgentType to AgentTypeDTO
|
||||
|
||||
@@ -47,10 +47,10 @@ func (h *Handlers) wsListAgents(ctx context.Context, msg *ws.Message) (*ws.Messa
|
||||
}
|
||||
|
||||
type wsLaunchAgentRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type"`
|
||||
WorkspacePath string `json:"workspace_path"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentProfileID string `json:"agent_profile_id"`
|
||||
WorkspacePath string `json:"workspace_path"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handlers) wsLaunchAgent(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
@@ -61,23 +61,23 @@ func (h *Handlers) wsLaunchAgent(ctx context.Context, msg *ws.Message) (*ws.Mess
|
||||
if req.TaskID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "task_id is required", nil)
|
||||
}
|
||||
if req.AgentType == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "agent_type is required", nil)
|
||||
if req.AgentProfileID == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "agent_profile_id is required", nil)
|
||||
}
|
||||
if req.WorkspacePath == "" {
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeValidation, "workspace_path is required", nil)
|
||||
}
|
||||
|
||||
resp, err := h.controller.LaunchAgent(ctx, dto.LaunchAgentRequest{
|
||||
TaskID: req.TaskID,
|
||||
AgentType: req.AgentType,
|
||||
WorkspacePath: req.WorkspacePath,
|
||||
Env: req.Env,
|
||||
TaskID: req.TaskID,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
WorkspacePath: req.WorkspacePath,
|
||||
Env: req.Env,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to launch agent",
|
||||
zap.String("task_id", req.TaskID),
|
||||
zap.String("agent_type", req.AgentType),
|
||||
zap.String("agent_profile_id", req.AgentProfileID),
|
||||
zap.Error(err))
|
||||
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to launch agent: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ const AgentCtlPort = 9999
|
||||
|
||||
// AgentInstance represents a running agent instance
|
||||
type AgentInstance struct {
|
||||
ID string
|
||||
TaskID string
|
||||
AgentType string
|
||||
ContainerID string
|
||||
ID string
|
||||
TaskID string
|
||||
AgentProfileID string
|
||||
ContainerID string
|
||||
ContainerIP string // IP address of the container for agentctl communication
|
||||
WorkspacePath string // Path to the workspace (worktree or repository path)
|
||||
Status v1.AgentStatus
|
||||
@@ -66,7 +66,7 @@ func (ai *AgentInstance) GetAgentCtlClient() *agentctl.Client {
|
||||
type LaunchRequest struct {
|
||||
TaskID string
|
||||
TaskTitle string // Human-readable task title for semantic worktree naming
|
||||
AgentType string
|
||||
AgentProfileID string
|
||||
WorkspacePath string // Host path to workspace (original repository path)
|
||||
TaskDescription string // Task description to send via ACP prompt
|
||||
Env map[string]string // Additional env vars
|
||||
@@ -84,14 +84,32 @@ type CredentialsManager interface {
|
||||
GetCredentialValue(ctx context.Context, key string) (value string, err error)
|
||||
}
|
||||
|
||||
// AgentProfileInfo contains resolved profile information
|
||||
type AgentProfileInfo struct {
|
||||
ProfileID string
|
||||
ProfileName string
|
||||
AgentID string
|
||||
AgentName string // e.g., "auggie", "claude", "codex"
|
||||
Model string
|
||||
AutoApprove bool
|
||||
DangerouslySkipPermissions bool
|
||||
Plan string
|
||||
}
|
||||
|
||||
// ProfileResolver resolves agent profile IDs to profile information
|
||||
type ProfileResolver interface {
|
||||
ResolveProfile(ctx context.Context, profileID string) (*AgentProfileInfo, error)
|
||||
}
|
||||
|
||||
// Manager manages agent instance lifecycles
|
||||
type Manager struct {
|
||||
docker *docker.Client
|
||||
registry *registry.Registry
|
||||
eventBus bus.EventBus
|
||||
credsMgr CredentialsManager
|
||||
worktreeMgr *worktree.Manager
|
||||
logger *logger.Logger
|
||||
docker *docker.Client
|
||||
registry *registry.Registry
|
||||
eventBus bus.EventBus
|
||||
credsMgr CredentialsManager
|
||||
profileResolver ProfileResolver
|
||||
worktreeMgr *worktree.Manager
|
||||
logger *logger.Logger
|
||||
|
||||
// Agent runtime configuration
|
||||
agentCfg config.AgentConfig
|
||||
@@ -155,6 +173,11 @@ func (m *Manager) SetCredentialsManager(credsMgr CredentialsManager) {
|
||||
m.credsMgr = credsMgr
|
||||
}
|
||||
|
||||
// SetProfileResolver sets the profile resolver for looking up agent profiles
|
||||
func (m *Manager) SetProfileResolver(resolver ProfileResolver) {
|
||||
m.profileResolver = resolver
|
||||
}
|
||||
|
||||
// SetWorktreeManager sets the worktree manager for Git worktree isolation
|
||||
func (m *Manager) SetWorktreeManager(worktreeMgr *worktree.Manager) {
|
||||
m.worktreeMgr = worktreeMgr
|
||||
@@ -193,10 +216,10 @@ func (m *Manager) Start(ctx context.Context) error {
|
||||
|
||||
// RecoveredInstance contains info about an instance recovered from Docker
|
||||
type RecoveredInstance struct {
|
||||
InstanceID string
|
||||
TaskID string
|
||||
ContainerID string
|
||||
AgentType string
|
||||
InstanceID string
|
||||
TaskID string
|
||||
ContainerID string
|
||||
AgentProfileID string
|
||||
}
|
||||
|
||||
// reconcileWithDocker finds running kandev containers and re-populates tracking maps
|
||||
@@ -242,7 +265,7 @@ func (m *Manager) reconcileWithDocker(ctx context.Context) ([]RecoveredInstance,
|
||||
|
||||
instanceID := labels["kandev.instance_id"]
|
||||
taskID := labels["kandev.task_id"]
|
||||
agentType := labels["kandev.agent_type"]
|
||||
agentProfileID := labels["kandev.agent_profile_id"]
|
||||
|
||||
if instanceID == "" || taskID == "" {
|
||||
m.logger.Warn("container missing required labels",
|
||||
@@ -261,10 +284,10 @@ func (m *Manager) reconcileWithDocker(ctx context.Context) ([]RecoveredInstance,
|
||||
|
||||
// Create instance record
|
||||
instance := &AgentInstance{
|
||||
ID: instanceID,
|
||||
TaskID: taskID,
|
||||
AgentType: agentType,
|
||||
ContainerID: ctr.ID,
|
||||
ID: instanceID,
|
||||
TaskID: taskID,
|
||||
AgentProfileID: agentProfileID,
|
||||
ContainerID: ctr.ID,
|
||||
ContainerIP: containerIP,
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(), // We don't know exact start time
|
||||
@@ -279,10 +302,10 @@ func (m *Manager) reconcileWithDocker(ctx context.Context) ([]RecoveredInstance,
|
||||
m.byContainer[ctr.ID] = instanceID
|
||||
|
||||
recovered = append(recovered, RecoveredInstance{
|
||||
InstanceID: instanceID,
|
||||
TaskID: taskID,
|
||||
ContainerID: ctr.ID,
|
||||
AgentType: agentType,
|
||||
InstanceID: instanceID,
|
||||
TaskID: taskID,
|
||||
ContainerID: ctr.ID,
|
||||
AgentProfileID: agentProfileID,
|
||||
})
|
||||
|
||||
// Reconnect to WebSocket streams for updates and permissions
|
||||
@@ -311,10 +334,10 @@ func (m *Manager) GetRecoveredInstances() []RecoveredInstance {
|
||||
result := make([]RecoveredInstance, 0, len(m.instances))
|
||||
for _, inst := range m.instances {
|
||||
result = append(result, RecoveredInstance{
|
||||
InstanceID: inst.ID,
|
||||
TaskID: inst.TaskID,
|
||||
ContainerID: inst.ContainerID,
|
||||
AgentType: inst.AgentType,
|
||||
InstanceID: inst.ID,
|
||||
TaskID: inst.TaskID,
|
||||
ContainerID: inst.ContainerID,
|
||||
AgentProfileID: inst.AgentProfileID,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -334,20 +357,42 @@ func (m *Manager) Stop() error {
|
||||
func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentInstance, error) {
|
||||
m.logger.Info("launching agent",
|
||||
zap.String("task_id", req.TaskID),
|
||||
zap.String("agent_type", req.AgentType),
|
||||
zap.String("agent_profile_id", req.AgentProfileID),
|
||||
zap.Bool("use_worktree", req.UseWorktree))
|
||||
|
||||
// 1. Validate the agent type exists in registry
|
||||
agentConfig, err := m.registry.Get(req.AgentType)
|
||||
// 1. Resolve the agent profile to get agent type info
|
||||
var agentTypeName string
|
||||
var profileInfo *AgentProfileInfo
|
||||
var err error
|
||||
if m.profileResolver != nil {
|
||||
profileInfo, err = m.profileResolver.ResolveProfile(ctx, req.AgentProfileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve agent profile: %w", err)
|
||||
}
|
||||
// Map agent name to registry ID (e.g., "auggie" -> "auggie-agent")
|
||||
agentTypeName = profileInfo.AgentName + "-agent"
|
||||
m.logger.Info("resolved agent profile",
|
||||
zap.String("profile_id", req.AgentProfileID),
|
||||
zap.String("agent_name", profileInfo.AgentName),
|
||||
zap.String("agent_type", agentTypeName))
|
||||
} else {
|
||||
// Fallback: treat AgentProfileID as agent type directly (for backward compat)
|
||||
agentTypeName = req.AgentProfileID
|
||||
m.logger.Warn("no profile resolver configured, using profile ID as agent type",
|
||||
zap.String("agent_type", agentTypeName))
|
||||
}
|
||||
|
||||
// 2. Get agent config from registry
|
||||
agentConfig, err := m.registry.Get(agentTypeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("agent type not found: %w", err)
|
||||
return nil, fmt.Errorf("agent type %q not found in registry: %w", agentTypeName, err)
|
||||
}
|
||||
|
||||
if !agentConfig.Enabled {
|
||||
return nil, fmt.Errorf("agent type %q is disabled", req.AgentType)
|
||||
return nil, fmt.Errorf("agent type %q is disabled", agentTypeName)
|
||||
}
|
||||
|
||||
// 2. Check if task already has an agent running
|
||||
// 3. Check if task already has an agent running
|
||||
m.mu.RLock()
|
||||
if existingID, exists := m.byTask[req.TaskID]; exists {
|
||||
m.mu.RUnlock()
|
||||
@@ -355,7 +400,7 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentInstanc
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// 3. Handle worktree creation/reuse if enabled
|
||||
// 4. Handle worktree creation/reuse if enabled
|
||||
workspacePath := req.WorkspacePath
|
||||
var mainRepoGitDir string // Path to main repo's .git directory for container mount
|
||||
var worktreeID string // Store worktree ID for session resumption
|
||||
@@ -393,25 +438,44 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentInstanc
|
||||
zap.String("workspace_path", workspacePath))
|
||||
}
|
||||
|
||||
// 4. Generate a new instance ID
|
||||
// 5. Generate a new instance ID
|
||||
instanceID := uuid.New().String()
|
||||
|
||||
// 5. Prepare request with worktree path
|
||||
// 6. Prepare request with worktree path
|
||||
reqWithWorktree := *req
|
||||
reqWithWorktree.WorkspacePath = workspacePath
|
||||
|
||||
// 6. Launch via appropriate runtime (Docker or Standalone)
|
||||
// 6b. Add profile settings to environment
|
||||
if profileInfo != nil {
|
||||
if reqWithWorktree.Env == nil {
|
||||
reqWithWorktree.Env = make(map[string]string)
|
||||
}
|
||||
if profileInfo.Model != "" {
|
||||
reqWithWorktree.Env["AGENT_MODEL"] = profileInfo.Model
|
||||
}
|
||||
if profileInfo.AutoApprove {
|
||||
reqWithWorktree.Env["AGENT_AUTO_APPROVE"] = "true"
|
||||
}
|
||||
if profileInfo.DangerouslySkipPermissions {
|
||||
reqWithWorktree.Env["AGENT_DANGEROUSLY_SKIP_PERMISSIONS"] = "true"
|
||||
}
|
||||
if profileInfo.Plan != "" {
|
||||
reqWithWorktree.Env["AGENT_PLAN"] = profileInfo.Plan
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Launch via appropriate runtime (Docker or Standalone)
|
||||
var instance *AgentInstance
|
||||
if m.IsStandaloneMode() {
|
||||
instance, err = m.launchStandalone(ctx, instanceID, &reqWithWorktree, agentConfig, worktreeID, worktreeBranch)
|
||||
instance, err = m.launchStandalone(ctx, instanceID, &reqWithWorktree, agentConfig, profileInfo, worktreeID, worktreeBranch)
|
||||
} else {
|
||||
instance, err = m.launchDocker(ctx, instanceID, &reqWithWorktree, agentConfig, mainRepoGitDir, worktreeID, worktreeBranch)
|
||||
instance, err = m.launchDocker(ctx, instanceID, &reqWithWorktree, agentConfig, profileInfo, mainRepoGitDir, worktreeID, worktreeBranch)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 7. Track the instance
|
||||
// 8. Track the instance
|
||||
m.mu.Lock()
|
||||
m.instances[instanceID] = instance
|
||||
m.byTask[req.TaskID] = instanceID
|
||||
@@ -420,10 +484,10 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentInstanc
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// 8. Publish agent.started event
|
||||
// 9. Publish agent.started event
|
||||
m.publishEvent(ctx, events.AgentStarted, instance)
|
||||
|
||||
// 9. Wait for agentctl to be ready and start the agent process
|
||||
// 10. Wait for agentctl to be ready and start the agent process
|
||||
go m.initializeAgent(instance, req.TaskDescription)
|
||||
|
||||
m.logger.Info("agent launched successfully",
|
||||
@@ -435,9 +499,9 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentInstanc
|
||||
}
|
||||
|
||||
// launchDocker launches an agent in a Docker container
|
||||
func (m *Manager) launchDocker(ctx context.Context, instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, mainRepoGitDir, worktreeID, worktreeBranch string) (*AgentInstance, error) {
|
||||
func (m *Manager) launchDocker(ctx context.Context, instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, profileInfo *AgentProfileInfo, mainRepoGitDir, worktreeID, worktreeBranch string) (*AgentInstance, error) {
|
||||
// Build container config from registry config
|
||||
containerConfig := m.buildContainerConfig(instanceID, req, agentConfig, mainRepoGitDir)
|
||||
containerConfig := m.buildContainerConfig(instanceID, req, agentConfig, profileInfo, mainRepoGitDir)
|
||||
|
||||
// Create and start the container
|
||||
containerID, err := m.docker.CreateContainer(ctx, containerConfig)
|
||||
@@ -479,22 +543,22 @@ func (m *Manager) launchDocker(ctx context.Context, instanceID string, req *Laun
|
||||
zap.String("container_ip", containerIP))
|
||||
|
||||
return &AgentInstance{
|
||||
ID: instanceID,
|
||||
TaskID: req.TaskID,
|
||||
AgentType: req.AgentType,
|
||||
ContainerID: containerID,
|
||||
ContainerIP: containerIP,
|
||||
WorkspacePath: "/workspace", // Docker mounts workspace to /workspace
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
Progress: 0,
|
||||
Metadata: instanceMetadata,
|
||||
agentctl: agentctlClient,
|
||||
ID: instanceID,
|
||||
TaskID: req.TaskID,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
ContainerID: containerID,
|
||||
ContainerIP: containerIP,
|
||||
WorkspacePath: "/workspace", // Docker mounts workspace to /workspace
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
Progress: 0,
|
||||
Metadata: instanceMetadata,
|
||||
agentctl: agentctlClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// launchStandalone launches an agent via standalone agentctl
|
||||
func (m *Manager) launchStandalone(ctx context.Context, instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, worktreeID, worktreeBranch string) (*AgentInstance, error) {
|
||||
func (m *Manager) launchStandalone(ctx context.Context, instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, profileInfo *AgentProfileInfo, worktreeID, worktreeBranch string) (*AgentInstance, error) {
|
||||
// Build environment variables for the agent
|
||||
env := m.buildStandaloneEnv(instanceID, req, agentConfig)
|
||||
|
||||
@@ -504,6 +568,10 @@ func (m *Manager) launchStandalone(ctx context.Context, instanceID string, req *
|
||||
if len(agentConfig.Cmd) > 0 {
|
||||
agentCommand = strings.Join(agentConfig.Cmd, " ")
|
||||
}
|
||||
// Append model flag if agent supports it and profile has a model configured
|
||||
if agentConfig.ModelFlag != "" && profileInfo != nil && profileInfo.Model != "" {
|
||||
agentCommand = agentCommand + " " + agentConfig.ModelFlag + " " + profileInfo.Model
|
||||
}
|
||||
// If empty, agentctl will use its default (auggie --acp)
|
||||
|
||||
// Create instance via control API
|
||||
@@ -544,7 +612,7 @@ func (m *Manager) launchStandalone(ctx context.Context, instanceID string, req *
|
||||
return &AgentInstance{
|
||||
ID: instanceID,
|
||||
TaskID: req.TaskID,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
WorkspacePath: req.WorkspacePath,
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
@@ -1421,7 +1489,7 @@ func (m *Manager) updateInstanceError(instanceID, errorMsg string) {
|
||||
// buildContainerConfig builds a Docker container config from agent registry config
|
||||
// mainRepoGitDir is the host path to the main repository's .git directory (e.g., /path/to/repo/.git)
|
||||
// If non-empty, it will be mounted into the container at the same path so git worktree commands work correctly
|
||||
func (m *Manager) buildContainerConfig(instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, mainRepoGitDir string) docker.ContainerConfig {
|
||||
func (m *Manager) buildContainerConfig(instanceID string, req *LaunchRequest, agentConfig *registry.AgentTypeConfig, profileInfo *AgentProfileInfo, mainRepoGitDir string) docker.ContainerConfig {
|
||||
// Build image name with tag
|
||||
imageName := agentConfig.Image
|
||||
if agentConfig.Tag != "" {
|
||||
@@ -1511,20 +1579,27 @@ func (m *Manager) buildContainerConfig(instanceID string, req *LaunchRequest, ag
|
||||
|
||||
containerName := fmt.Sprintf("kandev-agent-%s", instanceID[:8])
|
||||
|
||||
// Build command, appending model flag if agent supports it and profile has a model
|
||||
cmd := make([]string, len(agentConfig.Cmd))
|
||||
copy(cmd, agentConfig.Cmd)
|
||||
if agentConfig.ModelFlag != "" && profileInfo != nil && profileInfo.Model != "" {
|
||||
cmd = append(cmd, agentConfig.ModelFlag, profileInfo.Model)
|
||||
}
|
||||
|
||||
return docker.ContainerConfig{
|
||||
Name: containerName,
|
||||
Image: imageName,
|
||||
Cmd: agentConfig.Cmd,
|
||||
Cmd: cmd,
|
||||
Env: env,
|
||||
WorkingDir: agentConfig.WorkingDir,
|
||||
Mounts: mounts,
|
||||
Memory: memoryBytes,
|
||||
CPUQuota: cpuQuota,
|
||||
Labels: map[string]string{
|
||||
"kandev.managed": "true",
|
||||
"kandev.instance_id": instanceID,
|
||||
"kandev.task_id": req.TaskID,
|
||||
"kandev.agent_type": req.AgentType,
|
||||
"kandev.managed": "true",
|
||||
"kandev.instance_id": instanceID,
|
||||
"kandev.task_id": req.TaskID,
|
||||
"kandev.agent_profile_id": req.AgentProfileID,
|
||||
},
|
||||
AutoRemove: false, // We manage cleanup ourselves
|
||||
}
|
||||
@@ -1921,14 +1996,14 @@ func (m *Manager) publishEvent(ctx context.Context, eventType string, instance *
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"instance_id": instance.ID,
|
||||
"task_id": instance.TaskID,
|
||||
"agent_type": instance.AgentType,
|
||||
"container_id": instance.ContainerID,
|
||||
"status": string(instance.Status),
|
||||
"started_at": instance.StartedAt,
|
||||
"progress": instance.Progress,
|
||||
"error_message": instance.ErrorMessage,
|
||||
"instance_id": instance.ID,
|
||||
"task_id": instance.TaskID,
|
||||
"agent_profile_id": instance.AgentProfileID,
|
||||
"container_id": instance.ContainerID,
|
||||
"status": string(instance.Status),
|
||||
"started_at": instance.StartedAt,
|
||||
"progress": instance.Progress,
|
||||
"error_message": instance.ErrorMessage,
|
||||
}
|
||||
|
||||
if instance.FinishedAt != nil {
|
||||
|
||||
@@ -178,12 +178,12 @@ func TestManager_GetInstance(t *testing.T) {
|
||||
|
||||
// Manually add an instance for testing
|
||||
instance := &AgentInstance{
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentType: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentProfileID: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
mgr.mu.Lock()
|
||||
@@ -215,12 +215,12 @@ func TestManager_GetInstanceByTaskID(t *testing.T) {
|
||||
mgr := NewManager(nil, reg, eventBus, testAgentConfig(), log)
|
||||
|
||||
instance := &AgentInstance{
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentType: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentProfileID: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
mgr.mu.Lock()
|
||||
@@ -251,12 +251,12 @@ func TestManager_GetInstanceByContainerID(t *testing.T) {
|
||||
mgr := NewManager(nil, reg, eventBus, testAgentConfig(), log)
|
||||
|
||||
instance := &AgentInstance{
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentType: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentProfileID: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
mgr.mu.Lock()
|
||||
@@ -380,13 +380,13 @@ func TestManager_MarkCompleted_Success(t *testing.T) {
|
||||
mgr := NewManager(nil, reg, eventBus, testAgentConfig(), log)
|
||||
|
||||
instance := &AgentInstance{
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentType: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
Progress: 50,
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentProfileID: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
Progress: 50,
|
||||
}
|
||||
|
||||
mgr.mu.Lock()
|
||||
@@ -421,12 +421,12 @@ func TestManager_MarkCompleted_Failure(t *testing.T) {
|
||||
mgr := NewManager(nil, reg, eventBus, testAgentConfig(), log)
|
||||
|
||||
instance := &AgentInstance{
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentType: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
ID: "test-instance-id",
|
||||
TaskID: "test-task-id",
|
||||
AgentProfileID: "test-agent",
|
||||
ContainerID: "container-123",
|
||||
Status: v1.AgentStatusRunning,
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
mgr.mu.Lock()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package lifecycle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kandev/kandev/internal/agent/settings/store"
|
||||
)
|
||||
|
||||
// StoreProfileResolver implements ProfileResolver using the agent settings store
|
||||
type StoreProfileResolver struct {
|
||||
store store.Repository
|
||||
}
|
||||
|
||||
// NewStoreProfileResolver creates a new profile resolver using the given store
|
||||
func NewStoreProfileResolver(store store.Repository) *StoreProfileResolver {
|
||||
return &StoreProfileResolver{store: store}
|
||||
}
|
||||
|
||||
// ResolveProfile looks up an agent profile by ID and returns the profile info
|
||||
func (r *StoreProfileResolver) ResolveProfile(ctx context.Context, profileID string) (*AgentProfileInfo, error) {
|
||||
// Get the profile from the store
|
||||
profile, err := r.store.GetAgentProfile(ctx, profileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("profile not found: %w", err)
|
||||
}
|
||||
|
||||
// Get the parent agent to get the agent name
|
||||
agent, err := r.store.GetAgent(ctx, profile.AgentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("agent not found for profile: %w", err)
|
||||
}
|
||||
|
||||
return &AgentProfileInfo{
|
||||
ProfileID: profile.ID,
|
||||
ProfileName: profile.Name,
|
||||
AgentID: agent.ID,
|
||||
AgentName: agent.Name,
|
||||
Model: profile.Model,
|
||||
AutoApprove: profile.AutoApprove,
|
||||
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
|
||||
Plan: profile.Plan,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ package registry
|
||||
func DefaultAgents() []*AgentTypeConfig {
|
||||
return []*AgentTypeConfig{
|
||||
{
|
||||
ID: "augment-agent",
|
||||
ID: "auggie-agent",
|
||||
Name: "Augment Coding Agent",
|
||||
Description: "Auggie CLI-powered autonomous coding agent. Requires AUGMENT_SESSION_AUTH for authentication.",
|
||||
Image: "kandev/augment-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"auggie", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{"AUGMENT_SESSION_AUTH"},
|
||||
Env: map[string]string{
|
||||
// Auto-approve permissions for now (until full permission flow is implemented)
|
||||
// This allows the agent to proceed without waiting for user approval
|
||||
"AGENTCTL_AUTO_APPROVE_PERMISSIONS": "true",
|
||||
},
|
||||
Mounts: []MountTemplate{
|
||||
@@ -23,11 +22,121 @@ func DefaultAgents() []*AgentTypeConfig {
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600, // 1 hour
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
{
|
||||
ID: "claude-agent",
|
||||
Name: "Claude Code Agent",
|
||||
Description: "Claude Code CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/claude-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"claude", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
{
|
||||
ID: "codex-agent",
|
||||
Name: "OpenAI Codex Agent",
|
||||
Description: "OpenAI Codex CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/codex-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"codex", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
{
|
||||
ID: "gemini-agent",
|
||||
Name: "Gemini CLI Agent",
|
||||
Description: "Google Gemini CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/gemini-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"gemini", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
{
|
||||
ID: "opencode-agent",
|
||||
Name: "OpenCode Agent",
|
||||
Description: "OpenCode CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/opencode-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"opencode", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
{
|
||||
ID: "copilot-agent",
|
||||
Name: "GitHub Copilot Agent",
|
||||
Description: "GitHub Copilot CLI-powered autonomous coding agent.",
|
||||
Image: "kandev/copilot-agent",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"copilot", "--acp"},
|
||||
WorkingDir: "/workspace",
|
||||
RequiredEnv: []string{},
|
||||
Env: map[string]string{},
|
||||
Mounts: []MountTemplate{
|
||||
{Source: "{workspace}", Target: "/workspace", ReadOnly: false},
|
||||
},
|
||||
ResourceLimits: ResourceLimits{
|
||||
MemoryMB: 4096,
|
||||
CPUCores: 2.0,
|
||||
TimeoutSeconds: 3600,
|
||||
},
|
||||
Capabilities: []string{"code_generation", "code_review", "refactoring", "testing", "shell_execution"},
|
||||
Enabled: true,
|
||||
ModelFlag: "--model",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ type AgentTypeConfig struct {
|
||||
ResourceLimits ResourceLimits `json:"resource_limits"`
|
||||
Capabilities []string `json:"capabilities"` // What the agent can do
|
||||
Enabled bool `json:"enabled"`
|
||||
ModelFlag string `json:"model_flag,omitempty"` // CLI flag for model selection (e.g., "--model")
|
||||
}
|
||||
|
||||
// MountTemplate defines a mount with template support
|
||||
|
||||
@@ -91,8 +91,8 @@ type WorktreeConfig struct {
|
||||
// AgentConfig holds agent runtime configuration.
|
||||
type AgentConfig struct {
|
||||
// Runtime specifies the agent runtime mode: "docker" or "standalone"
|
||||
// - "docker": Agents run in Docker containers (default)
|
||||
// - "standalone": Agents run via standalone agentctl on the host machine
|
||||
// - "standalone": Agents run via standalone agentctl on the host machine (default)
|
||||
// - "docker": Agents run in Docker containers
|
||||
Runtime string `mapstructure:"runtime"`
|
||||
|
||||
// StandaloneHost is the host where standalone agentctl is running (default: localhost)
|
||||
@@ -149,7 +149,7 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("docker.volumeBasePath", "/var/lib/kandev/volumes")
|
||||
|
||||
// Agent defaults
|
||||
v.SetDefault("agent.runtime", "docker")
|
||||
v.SetDefault("agent.runtime", "standalone")
|
||||
v.SetDefault("agent.standaloneHost", "localhost")
|
||||
v.SetDefault("agent.standalonePort", 9999)
|
||||
|
||||
|
||||
@@ -83,8 +83,10 @@ func (c *Client) ReadPump(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the message
|
||||
c.handleMessage(ctx, &msg)
|
||||
// Process the message in a goroutine to avoid blocking the read pump
|
||||
// This allows concurrent message handling so long-running handlers
|
||||
// (like orchestrator.prompt) don't block other requests (like workspace.tree.get)
|
||||
go c.handleMessage(ctx, &msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,11 +60,11 @@ type SimulatedAgentManagerClient struct {
|
||||
|
||||
// simulatedInstance tracks a simulated agent instance
|
||||
type simulatedInstance struct {
|
||||
id string
|
||||
taskID string
|
||||
agentType string
|
||||
status v1.AgentStatus
|
||||
stopCh chan struct{}
|
||||
id string
|
||||
taskID string
|
||||
agentProfileID string
|
||||
status v1.AgentStatus
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewSimulatedAgentManager creates a new simulated agent manager
|
||||
@@ -121,11 +121,11 @@ func (s *SimulatedAgentManagerClient) LaunchAgent(ctx context.Context, req *exec
|
||||
|
||||
s.mu.Lock()
|
||||
instance := &simulatedInstance{
|
||||
id: instanceID,
|
||||
taskID: req.TaskID,
|
||||
agentType: req.AgentType,
|
||||
status: v1.AgentStatusStarting,
|
||||
stopCh: make(chan struct{}),
|
||||
id: instanceID,
|
||||
taskID: req.TaskID,
|
||||
agentProfileID: req.AgentProfileID,
|
||||
status: v1.AgentStatusStarting,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
s.instances[instanceID] = instance
|
||||
s.mu.Unlock()
|
||||
@@ -175,13 +175,13 @@ func (s *SimulatedAgentManagerClient) runAgentSimulation(instance *simulatedInst
|
||||
// publishAgentEvent publishes an agent lifecycle event
|
||||
func (s *SimulatedAgentManagerClient) publishAgentEvent(eventType string, instance *simulatedInstance) {
|
||||
data := map[string]interface{}{
|
||||
"instance_id": instance.id,
|
||||
"task_id": instance.taskID,
|
||||
"agent_type": instance.agentType,
|
||||
"container_id": "sim-container-" + instance.id[:8],
|
||||
"status": string(instance.status),
|
||||
"started_at": time.Now(),
|
||||
"progress": 50,
|
||||
"instance_id": instance.id,
|
||||
"task_id": instance.taskID,
|
||||
"agent_profile_id": instance.agentProfileID,
|
||||
"container_id": "sim-container-" + instance.id[:8],
|
||||
"status": string(instance.status),
|
||||
"started_at": time.Now(),
|
||||
"progress": 50,
|
||||
}
|
||||
|
||||
event := bus.NewEvent(eventType, "simulated-agent-manager", data)
|
||||
@@ -278,10 +278,10 @@ func (s *SimulatedAgentManagerClient) GetAgentStatus(ctx context.Context, agentI
|
||||
}
|
||||
|
||||
return &v1.AgentInstance{
|
||||
ID: instance.id,
|
||||
TaskID: instance.taskID,
|
||||
AgentType: instance.agentType,
|
||||
Status: instance.status,
|
||||
ID: instance.id,
|
||||
TaskID: instance.taskID,
|
||||
AgentProfileID: instance.agentProfileID,
|
||||
Status: instance.status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -568,8 +568,8 @@ func (ts *OrchestratorTestServer) Close() {
|
||||
ts.EventBus.Close()
|
||||
}
|
||||
|
||||
// CreateTestTask creates a task with agent_type set for testing
|
||||
func (ts *OrchestratorTestServer) CreateTestTask(t *testing.T, agentType string, priority int) string {
|
||||
// CreateTestTask creates a task with agent_profile_id set for testing
|
||||
func (ts *OrchestratorTestServer) CreateTestTask(t *testing.T, agentProfileID string, priority int) string {
|
||||
t.Helper()
|
||||
|
||||
workspace, err := ts.TaskSvc.CreateWorkspace(context.Background(), &taskservice.CreateWorkspaceRequest{
|
||||
@@ -594,16 +594,16 @@ func (ts *OrchestratorTestServer) CreateTestTask(t *testing.T, agentType string,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create task with agent type
|
||||
// Create task with agent profile ID
|
||||
task, err := ts.TaskSvc.CreateTask(context.Background(), &taskservice.CreateTaskRequest{
|
||||
WorkspaceID: workspace.ID,
|
||||
BoardID: board.ID,
|
||||
ColumnID: col.ID,
|
||||
Title: "Test Task",
|
||||
Description: "This is a test task for the orchestrator",
|
||||
Priority: priority,
|
||||
AgentType: agentType,
|
||||
RepositoryURL: "/tmp/repo",
|
||||
WorkspaceID: workspace.ID,
|
||||
BoardID: board.ID,
|
||||
ColumnID: col.ID,
|
||||
Title: "Test Task",
|
||||
Description: "This is a test task for the orchestrator",
|
||||
Priority: priority,
|
||||
AgentProfileID: agentProfileID,
|
||||
RepositoryURL: "/tmp/repo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -805,8 +805,8 @@ func TestOrchestratorStartTask(t *testing.T) {
|
||||
ts := NewOrchestratorTestServer(t)
|
||||
defer ts.Close()
|
||||
|
||||
// Create a task with agent_type
|
||||
taskID := ts.CreateTestTask(t, "augment-agent", 2)
|
||||
// Create a task with agent_profile_id
|
||||
taskID := ts.CreateTestTask(t, "test-profile-id", 2)
|
||||
|
||||
client := NewOrchestratorWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
@@ -856,11 +856,11 @@ func TestOrchestratorStartTaskWithAgentTypeOverride(t *testing.T) {
|
||||
client := NewOrchestratorWSClient(t, ts.Server.URL)
|
||||
defer client.Close()
|
||||
|
||||
// Start with different agent type
|
||||
// Start with different agent profile
|
||||
resp, err := client.SendRequest("start-1", ws.ActionOrchestratorStart, map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
"agent_type": "auggie-cli",
|
||||
"priority": 3,
|
||||
"task_id": taskID,
|
||||
"agent_profile_id": "override-profile-id",
|
||||
"priority": 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1347,16 +1347,16 @@ func TestOrchestratorEndToEndWorkflow(t *testing.T) {
|
||||
require.NoError(t, colResp.ParsePayload(&colPayload))
|
||||
colID := colPayload["id"].(string)
|
||||
|
||||
// 3. Create task with agent type
|
||||
// 3. Create task with agent_profile_id
|
||||
taskResp, err := client.SendRequest("task-1", ws.ActionTaskCreate, map[string]interface{}{
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": colID,
|
||||
"title": "Implement feature X",
|
||||
"description": "Create a new feature with tests",
|
||||
"priority": 2,
|
||||
"agent_type": "augment-agent",
|
||||
"repository_url": "/tmp/repo",
|
||||
"workspace_id": workspaceID,
|
||||
"board_id": boardID,
|
||||
"column_id": colID,
|
||||
"title": "Implement feature X",
|
||||
"description": "Create a new feature with tests",
|
||||
"priority": 2,
|
||||
"agent_profile_id": "test-profile-id",
|
||||
"repository_url": "/tmp/repo",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ func (c *Controller) TriggerTask(ctx context.Context, req dto.TriggerTaskRequest
|
||||
|
||||
// StartTask starts task execution
|
||||
func (c *Controller) StartTask(ctx context.Context, req dto.StartTaskRequest) (dto.StartTaskResponse, error) {
|
||||
execution, err := c.service.StartTask(ctx, req.TaskID, req.AgentType, req.Priority)
|
||||
execution, err := c.service.StartTask(ctx, req.TaskID, req.AgentProfileID, req.Priority)
|
||||
if err != nil {
|
||||
return dto.StartTaskResponse{}, err
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (c *Controller) GetTaskExecution(ctx context.Context, req dto.GetTaskExecut
|
||||
HasExecution: true,
|
||||
TaskID: execution.TaskID,
|
||||
AgentInstanceID: execution.AgentInstanceID,
|
||||
AgentType: execution.AgentType,
|
||||
AgentProfileID: execution.AgentProfileID,
|
||||
Status: string(execution.Status),
|
||||
Progress: execution.Progress,
|
||||
StartedAt: execution.StartedAt.Format("2006-01-02T15:04:05Z"),
|
||||
|
||||
@@ -44,9 +44,9 @@ type TriggerTaskResponse struct {
|
||||
|
||||
// StartTaskRequest is the payload for orchestrator.start
|
||||
type StartTaskRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
// StartTaskResponse is the response for orchestrator.start
|
||||
@@ -128,7 +128,7 @@ type TaskExecutionResponse struct {
|
||||
HasExecution bool `json:"has_execution"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentInstanceID string `json:"agent_instance_id,omitempty"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Progress int `json:"progress,omitempty"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrMaxConcurrentReached = errors.New("maximum concurrent executions reached")
|
||||
ErrNoAgentType = errors.New("task has no agent_type configured")
|
||||
ErrExecutionNotFound = errors.New("execution not found")
|
||||
ErrMaxConcurrentReached = errors.New("maximum concurrent executions reached")
|
||||
ErrNoAgentProfileID = errors.New("task has no agent_profile_id configured")
|
||||
ErrExecutionNotFound = errors.New("execution not found")
|
||||
)
|
||||
|
||||
// PromptResult contains the result of a prompt operation
|
||||
@@ -63,7 +63,7 @@ type AgentManagerClient interface {
|
||||
type LaunchAgentRequest struct {
|
||||
TaskID string
|
||||
TaskTitle string // Human-readable task title for semantic worktree naming
|
||||
AgentType string
|
||||
AgentProfileID string
|
||||
RepositoryURL string
|
||||
Branch string
|
||||
TaskDescription string // Task description to send via ACP prompt
|
||||
@@ -90,7 +90,7 @@ type LaunchAgentResponse struct {
|
||||
type TaskExecution struct {
|
||||
TaskID string
|
||||
AgentInstanceID string
|
||||
AgentType string
|
||||
AgentProfileID string
|
||||
StartedAt time.Time
|
||||
Status v1.AgentStatus
|
||||
Progress int
|
||||
@@ -107,7 +107,7 @@ func FromAgentSession(s *models.AgentSession) *TaskExecution {
|
||||
return &TaskExecution{
|
||||
TaskID: s.TaskID,
|
||||
AgentInstanceID: s.AgentInstanceID,
|
||||
AgentType: s.AgentType,
|
||||
AgentProfileID: s.AgentType, // AgentSession still uses AgentType field in DB
|
||||
StartedAt: s.StartedAt,
|
||||
Status: agentSessionStatusToV1(s.Status),
|
||||
Progress: s.Progress,
|
||||
@@ -227,10 +227,10 @@ func (e *Executor) LoadActiveSessionsFromDB(ctx context.Context) error {
|
||||
|
||||
// RecoveredInstanceInfo contains info about an instance recovered from Docker
|
||||
type RecoveredInstanceInfo struct {
|
||||
InstanceID string
|
||||
TaskID string
|
||||
ContainerID string
|
||||
AgentType string
|
||||
InstanceID string
|
||||
TaskID string
|
||||
ContainerID string
|
||||
AgentProfileID string
|
||||
}
|
||||
|
||||
// SyncWithRecoveredInstances ensures the executor's cache is in sync with
|
||||
@@ -249,7 +249,7 @@ func (e *Executor) SyncWithRecoveredInstances(ctx context.Context, instances []R
|
||||
e.executions[inst.TaskID] = &TaskExecution{
|
||||
TaskID: inst.TaskID,
|
||||
AgentInstanceID: inst.InstanceID,
|
||||
AgentType: inst.AgentType,
|
||||
AgentProfileID: inst.AgentProfileID,
|
||||
StartedAt: time.Now(), // We don't know exact start time
|
||||
Status: v1.AgentStatusRunning,
|
||||
Progress: 0,
|
||||
@@ -272,17 +272,17 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
|
||||
return nil, ErrMaxConcurrentReached
|
||||
}
|
||||
|
||||
// Verify task has an agent_type configured
|
||||
if task.AgentType == nil || *task.AgentType == "" {
|
||||
e.logger.Error("task has no agent_type configured", zap.String("task_id", task.ID))
|
||||
return nil, ErrNoAgentType
|
||||
// Verify task has an agent_profile_id configured
|
||||
if task.AgentProfileID == nil || *task.AgentProfileID == "" {
|
||||
e.logger.Error("task has no agent_profile_id configured", zap.String("task_id", task.ID))
|
||||
return nil, ErrNoAgentProfileID
|
||||
}
|
||||
|
||||
// Create a LaunchAgentRequest from the task
|
||||
req := &LaunchAgentRequest{
|
||||
TaskID: task.ID,
|
||||
TaskTitle: task.Title,
|
||||
AgentType: *task.AgentType,
|
||||
AgentProfileID: *task.AgentProfileID,
|
||||
TaskDescription: task.Description,
|
||||
Priority: task.Priority,
|
||||
Metadata: task.Metadata,
|
||||
@@ -308,7 +308,7 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
|
||||
|
||||
e.logger.Info("launching agent for task",
|
||||
zap.String("task_id", task.ID),
|
||||
zap.String("agent_type", *task.AgentType),
|
||||
zap.String("agent_profile_id", *task.AgentProfileID),
|
||||
zap.Bool("use_worktree", req.UseWorktree))
|
||||
|
||||
// Call the AgentManager to launch the container
|
||||
@@ -328,7 +328,7 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
|
||||
TaskID: task.ID,
|
||||
AgentInstanceID: resp.AgentInstanceID,
|
||||
ContainerID: resp.ContainerID,
|
||||
AgentType: *task.AgentType,
|
||||
AgentType: *task.AgentProfileID, // AgentSession still uses AgentType field in DB
|
||||
Status: v1StatusToAgentSessionStatus(resp.Status),
|
||||
Progress: 0,
|
||||
StartedAt: now,
|
||||
@@ -346,7 +346,7 @@ func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution,
|
||||
execution := &TaskExecution{
|
||||
TaskID: task.ID,
|
||||
AgentInstanceID: resp.AgentInstanceID,
|
||||
AgentType: *task.AgentType,
|
||||
AgentProfileID: *task.AgentProfileID,
|
||||
StartedAt: now,
|
||||
Status: resp.Status,
|
||||
Progress: 0,
|
||||
@@ -727,7 +727,7 @@ func NewMockAgentManagerClient(log *logger.Logger) *MockAgentManagerClient {
|
||||
func (m *MockAgentManagerClient) LaunchAgent(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error) {
|
||||
m.logger.Info("mock: launching agent",
|
||||
zap.String("task_id", req.TaskID),
|
||||
zap.String("agent_type", req.AgentType),
|
||||
zap.String("agent_profile_id", req.AgentProfileID),
|
||||
zap.String("repository_url", req.RepositoryURL),
|
||||
zap.String("branch", req.Branch))
|
||||
|
||||
@@ -752,9 +752,9 @@ func (m *MockAgentManagerClient) GetAgentStatus(ctx context.Context, agentInstan
|
||||
zap.String("agent_instance_id", agentInstanceID))
|
||||
|
||||
return &v1.AgentInstance{
|
||||
ID: agentInstanceID,
|
||||
Status: v1.AgentStatusRunning,
|
||||
AgentType: "mock-agent",
|
||||
ID: agentInstanceID,
|
||||
Status: v1.AgentStatusRunning,
|
||||
AgentProfileID: "mock-agent",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ func (h *Handlers) wsTriggerTask(ctx context.Context, msg *ws.Message) (*ws.Mess
|
||||
}
|
||||
|
||||
type wsStartTaskRequest struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handlers) wsStartTask(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
|
||||
@@ -95,9 +95,9 @@ func (h *Handlers) wsStartTask(ctx context.Context, msg *ws.Message) (*ws.Messag
|
||||
}
|
||||
|
||||
resp, err := h.controller.StartTask(ctx, dto.StartTaskRequest{
|
||||
TaskID: req.TaskID,
|
||||
AgentType: req.AgentType,
|
||||
Priority: req.Priority,
|
||||
TaskID: req.TaskID,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
Priority: req.Priority,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("failed to start task", zap.String("task_id", req.TaskID), zap.Error(err))
|
||||
|
||||
@@ -18,12 +18,12 @@ var (
|
||||
|
||||
// QueuedTask represents a task in the priority queue
|
||||
type QueuedTask struct {
|
||||
TaskID string
|
||||
Priority int // Higher priority = processed first
|
||||
AgentType string
|
||||
QueuedAt time.Time
|
||||
Task *v1.Task // Full task data
|
||||
index int // Index in the heap (used by container/heap)
|
||||
TaskID string
|
||||
Priority int // Higher priority = processed first
|
||||
AgentProfileID string
|
||||
QueuedAt time.Time
|
||||
Task *v1.Task // Full task data
|
||||
index int // Index in the heap (used by container/heap)
|
||||
}
|
||||
|
||||
// taskHeap implements heap.Interface for priority queue
|
||||
@@ -95,17 +95,17 @@ func (q *TaskQueue) Enqueue(task *v1.Task) error {
|
||||
return ErrQueueFull
|
||||
}
|
||||
|
||||
agentType := ""
|
||||
if task.AgentType != nil {
|
||||
agentType = *task.AgentType
|
||||
agentProfileID := ""
|
||||
if task.AgentProfileID != nil {
|
||||
agentProfileID = *task.AgentProfileID
|
||||
}
|
||||
|
||||
qt := &QueuedTask{
|
||||
TaskID: task.ID,
|
||||
Priority: task.Priority,
|
||||
AgentType: agentType,
|
||||
QueuedAt: time.Now(),
|
||||
Task: task,
|
||||
TaskID: task.ID,
|
||||
Priority: task.Priority,
|
||||
AgentProfileID: agentProfileID,
|
||||
QueuedAt: time.Now(),
|
||||
Task: task,
|
||||
}
|
||||
|
||||
heap.Push(&q.heap, qt)
|
||||
|
||||
@@ -8,16 +8,16 @@ import (
|
||||
)
|
||||
|
||||
// createTestTask creates a task for testing with the given parameters
|
||||
func createTestTask(id string, priority int, agentType string) *v1.Task {
|
||||
func createTestTask(id string, priority int, agentProfileID string) *v1.Task {
|
||||
return &v1.Task{
|
||||
ID: id,
|
||||
BoardID: "test-board",
|
||||
Title: "Test Task " + id,
|
||||
Priority: priority,
|
||||
AgentType: &agentType,
|
||||
State: v1.TaskStateTODO,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ID: id,
|
||||
BoardID: "test-board",
|
||||
Title: "Test Task " + id,
|
||||
Priority: priority,
|
||||
AgentProfileID: &agentProfileID,
|
||||
State: v1.TaskStateTODO,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,16 +130,16 @@ func createTestExecutor(agentMgr *mockAgentManager, log *logger.Logger, maxConcu
|
||||
}
|
||||
|
||||
func createTestTask(id string, priority int) *v1.Task {
|
||||
agentType := "test-agent"
|
||||
agentProfileID := "test-agent"
|
||||
return &v1.Task{
|
||||
ID: id,
|
||||
BoardID: "test-board",
|
||||
Title: "Test Task " + id,
|
||||
Priority: priority,
|
||||
AgentType: &agentType,
|
||||
State: v1.TaskStateTODO,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ID: id,
|
||||
BoardID: "test-board",
|
||||
Title: "Test Task " + id,
|
||||
Priority: priority,
|
||||
AgentProfileID: &agentProfileID,
|
||||
State: v1.TaskStateTODO,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,10 +278,10 @@ func (s *Service) EnqueueTask(ctx context.Context, task *v1.Task) error {
|
||||
}
|
||||
|
||||
// StartTask manually starts agent execution for a task
|
||||
func (s *Service) StartTask(ctx context.Context, taskID string, agentType string, priority int) (*executor.TaskExecution, error) {
|
||||
func (s *Service) StartTask(ctx context.Context, taskID string, agentProfileID string, priority int) (*executor.TaskExecution, error) {
|
||||
s.logger.Info("manually starting task",
|
||||
zap.String("task_id", taskID),
|
||||
zap.String("agent_type", agentType),
|
||||
zap.String("agent_profile_id", agentProfileID),
|
||||
zap.Int("priority", priority))
|
||||
|
||||
// Fetch the task from the repository to get complete task info
|
||||
@@ -293,9 +293,9 @@ func (s *Service) StartTask(ctx context.Context, taskID string, agentType string
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Override agent_type and priority if provided in the request
|
||||
if agentType != "" {
|
||||
task.AgentType = &agentType
|
||||
// Override agent_profile_id and priority if provided in the request
|
||||
if agentProfileID != "" {
|
||||
task.AgentProfileID = &agentProfileID
|
||||
}
|
||||
if priority > 0 {
|
||||
task.Priority = priority
|
||||
@@ -425,9 +425,9 @@ func (s *Service) handleTaskStateChanged(ctx context.Context, data watcher.TaskE
|
||||
s.logger.Debug("handling task state changed",
|
||||
zap.String("task_id", data.TaskID))
|
||||
|
||||
// Add task to queue if state is TODO and agent_type is set
|
||||
// Add task to queue if state is TODO and agent_profile_id is set
|
||||
if data.NewState != nil && *data.NewState == v1.TaskStateTODO {
|
||||
if data.Task != nil && data.Task.AgentType != nil && *data.Task.AgentType != "" {
|
||||
if data.Task != nil && data.Task.AgentProfileID != nil && *data.Task.AgentProfileID != "" {
|
||||
if err := s.scheduler.EnqueueTask(data.Task); err != nil {
|
||||
s.logger.Error("failed to enqueue task on state change",
|
||||
zap.String("task_id", data.TaskID),
|
||||
@@ -435,7 +435,7 @@ func (s *Service) handleTaskStateChanged(ctx context.Context, data watcher.TaskE
|
||||
} else {
|
||||
s.logger.Info("task enqueued on state change to TODO",
|
||||
zap.String("task_id", data.TaskID),
|
||||
zap.String("agent_type", *data.Task.AgentType))
|
||||
zap.String("agent_profile_id", *data.Task.AgentProfileID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ type TaskEventData struct {
|
||||
type AgentEventData struct {
|
||||
TaskID string `json:"task_id"`
|
||||
AgentInstanceID string `json:"agent_instance_id"`
|
||||
AgentType string `json:"agent_type"`
|
||||
AgentProfileID string `json:"agent_profile_id"`
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (c *TaskController) CreateTask(ctx context.Context, req dto.CreateTaskReque
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
RepositoryURL: req.RepositoryURL,
|
||||
Branch: req.Branch,
|
||||
AssignedTo: req.AssignedTo,
|
||||
@@ -92,7 +92,7 @@ func (c *TaskController) UpdateTask(ctx context.Context, req dto.UpdateTaskReque
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
AssignedTo: req.AssignedTo,
|
||||
Position: req.Position,
|
||||
Metadata: req.Metadata,
|
||||
|
||||
@@ -100,7 +100,7 @@ type TaskDTO struct {
|
||||
Description string `json:"description"`
|
||||
State v1.TaskState `json:"state"`
|
||||
Priority int `json:"priority"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL *string `json:"repository_url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
AssignedAgentID *string `json:"assigned_agent_id,omitempty"`
|
||||
@@ -311,9 +311,9 @@ func FromLocalRepository(repo service.LocalRepository) LocalRepositoryDTO {
|
||||
}
|
||||
|
||||
func FromTask(task *models.Task) TaskDTO {
|
||||
var agentType *string
|
||||
if task.AgentType != "" {
|
||||
agentType = &task.AgentType
|
||||
var agentProfileID *string
|
||||
if task.AgentProfileID != "" {
|
||||
agentProfileID = &task.AgentProfileID
|
||||
}
|
||||
var repositoryURL *string
|
||||
if task.RepositoryURL != "" {
|
||||
@@ -337,7 +337,7 @@ func FromTask(task *models.Task) TaskDTO {
|
||||
Description: task.Description,
|
||||
State: task.State,
|
||||
Priority: task.Priority,
|
||||
AgentType: agentType,
|
||||
AgentProfileID: agentProfileID,
|
||||
RepositoryURL: repositoryURL,
|
||||
Branch: branch,
|
||||
AssignedAgentID: assignedAgentID,
|
||||
|
||||
@@ -236,7 +236,7 @@ type CreateTaskRequest struct {
|
||||
Description string
|
||||
Priority int
|
||||
State *v1.TaskState
|
||||
AgentType string
|
||||
AgentProfileID string
|
||||
RepositoryURL string
|
||||
Branch string
|
||||
AssignedTo string
|
||||
@@ -250,7 +250,7 @@ type UpdateTaskRequest struct {
|
||||
Description *string
|
||||
Priority *int
|
||||
State *v1.TaskState
|
||||
AgentType *string
|
||||
AgentProfileID *string
|
||||
AssignedTo *string
|
||||
Position *int
|
||||
Metadata map[string]interface{}
|
||||
|
||||
@@ -80,7 +80,7 @@ type httpCreateTaskRequest struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
@@ -110,7 +110,7 @@ func (h *TaskHandlers) httpCreateTask(c *gin.Context) {
|
||||
Description: body.Description,
|
||||
Priority: body.Priority,
|
||||
State: body.State,
|
||||
AgentType: body.AgentType,
|
||||
AgentProfileID: body.AgentProfileID,
|
||||
RepositoryURL: body.RepositoryURL,
|
||||
Branch: body.Branch,
|
||||
AssignedTo: body.AssignedTo,
|
||||
@@ -129,7 +129,7 @@ type httpUpdateTaskRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
AssignedTo *string `json:"assigned_to,omitempty"`
|
||||
Position *int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
@@ -147,7 +147,7 @@ func (h *TaskHandlers) httpUpdateTask(c *gin.Context) {
|
||||
Description: body.Description,
|
||||
Priority: body.Priority,
|
||||
State: body.State,
|
||||
AgentType: body.AgentType,
|
||||
AgentProfileID: body.AgentProfileID,
|
||||
AssignedTo: body.AssignedTo,
|
||||
Position: body.Position,
|
||||
Metadata: body.Metadata,
|
||||
@@ -228,7 +228,7 @@ type wsCreateTaskRequest struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
@@ -265,7 +265,7 @@ func (h *TaskHandlers) wsCreateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
RepositoryURL: req.RepositoryURL,
|
||||
Branch: req.Branch,
|
||||
AssignedTo: req.AssignedTo,
|
||||
@@ -305,7 +305,7 @@ type wsUpdateTaskRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
AssignedTo *string `json:"assigned_to,omitempty"`
|
||||
Position *int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
@@ -326,7 +326,7 @@ func (h *TaskHandlers) wsUpdateTask(ctx context.Context, msg *ws.Message) (*ws.M
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: req.State,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
AssignedTo: req.AssignedTo,
|
||||
Position: req.Position,
|
||||
Metadata: req.Metadata,
|
||||
|
||||
@@ -16,7 +16,7 @@ type Task struct {
|
||||
Description string `json:"description"`
|
||||
State v1.TaskState `json:"state"`
|
||||
Priority int `json:"priority"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
@@ -291,9 +291,9 @@ type Environment struct {
|
||||
|
||||
// ToAPI converts internal Task to API type
|
||||
func (t *Task) ToAPI() *v1.Task {
|
||||
var agentType *string
|
||||
if t.AgentType != "" {
|
||||
agentType = &t.AgentType
|
||||
var agentProfileID *string
|
||||
if t.AgentProfileID != "" {
|
||||
agentProfileID = &t.AgentProfileID
|
||||
}
|
||||
|
||||
var repositoryURL *string
|
||||
@@ -319,7 +319,7 @@ func (t *Task) ToAPI() *v1.Task {
|
||||
Description: t.Description,
|
||||
State: t.State,
|
||||
Priority: t.Priority,
|
||||
AgentType: agentType,
|
||||
AgentProfileID: agentProfileID,
|
||||
RepositoryURL: repositoryURL,
|
||||
Branch: branch,
|
||||
AssignedAgentID: assignedAgentID,
|
||||
|
||||
@@ -40,17 +40,17 @@ func TestTaskStructInitialization(t *testing.T) {
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateTODO,
|
||||
Priority: 5,
|
||||
AgentType: "coding",
|
||||
AssignedTo: "agent-001",
|
||||
Position: 1,
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateTODO,
|
||||
Priority: 5,
|
||||
AgentProfileID: "coding",
|
||||
AssignedTo: "agent-001",
|
||||
Position: 1,
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if task.ID != "task-123" {
|
||||
@@ -77,8 +77,8 @@ func TestTaskStructInitialization(t *testing.T) {
|
||||
if task.Priority != 5 {
|
||||
t.Errorf("expected Priority 5, got %d", task.Priority)
|
||||
}
|
||||
if task.AgentType != "coding" {
|
||||
t.Errorf("expected AgentType 'coding', got %s", task.AgentType)
|
||||
if task.AgentProfileID != "coding" {
|
||||
t.Errorf("expected AgentProfileID 'coding', got %s", task.AgentProfileID)
|
||||
}
|
||||
if task.AssignedTo != "agent-001" {
|
||||
t.Errorf("expected AssignedTo 'agent-001', got %s", task.AssignedTo)
|
||||
@@ -151,17 +151,17 @@ func TestTaskToAPI(t *testing.T) {
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateInProgress,
|
||||
Priority: 3,
|
||||
AgentType: "coding",
|
||||
AssignedTo: "agent-001",
|
||||
Position: 2,
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateInProgress,
|
||||
Priority: 3,
|
||||
AgentProfileID: "coding",
|
||||
AssignedTo: "agent-001",
|
||||
Position: 2,
|
||||
Metadata: map[string]interface{}{"key": "value"},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
apiTask := task.ToAPI()
|
||||
@@ -187,8 +187,8 @@ func TestTaskToAPI(t *testing.T) {
|
||||
if apiTask.Priority != task.Priority {
|
||||
t.Errorf("expected Priority %d, got %d", task.Priority, apiTask.Priority)
|
||||
}
|
||||
if apiTask.AgentType == nil || *apiTask.AgentType != task.AgentType {
|
||||
t.Errorf("expected AgentType %s, got %v", task.AgentType, apiTask.AgentType)
|
||||
if apiTask.AgentProfileID == nil || *apiTask.AgentProfileID != task.AgentProfileID {
|
||||
t.Errorf("expected AgentProfileID %s, got %v", task.AgentProfileID, apiTask.AgentProfileID)
|
||||
}
|
||||
if apiTask.AssignedAgentID == nil || *apiTask.AssignedAgentID != task.AssignedTo {
|
||||
t.Errorf("expected AssignedAgentID %s, got %v", task.AssignedTo, apiTask.AssignedAgentID)
|
||||
@@ -202,25 +202,25 @@ func TestTaskToAPIWithEmptyOptionalFields(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
task := &Task{
|
||||
ID: "task-123",
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateTODO,
|
||||
Priority: 0,
|
||||
AgentType: "",
|
||||
AssignedTo: "",
|
||||
Position: 0,
|
||||
Metadata: nil,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
WorkspaceID: "workspace-001",
|
||||
BoardID: "board-456",
|
||||
ColumnID: "column-789",
|
||||
Title: "Test Task",
|
||||
Description: "A test task description",
|
||||
State: v1.TaskStateTODO,
|
||||
Priority: 0,
|
||||
AgentProfileID: "",
|
||||
AssignedTo: "",
|
||||
Position: 0,
|
||||
Metadata: nil,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
apiTask := task.ToAPI()
|
||||
|
||||
if apiTask.AgentType != nil {
|
||||
t.Errorf("expected AgentType nil, got %v", apiTask.AgentType)
|
||||
if apiTask.AgentProfileID != nil {
|
||||
t.Errorf("expected AgentProfileID nil, got %v", apiTask.AgentProfileID)
|
||||
}
|
||||
if apiTask.AssignedAgentID != nil {
|
||||
t.Errorf("expected AssignedAgentID nil, got %v", apiTask.AssignedAgentID)
|
||||
|
||||
@@ -150,7 +150,7 @@ func (r *SQLiteRepository) initSchema() error {
|
||||
description TEXT DEFAULT '',
|
||||
state TEXT DEFAULT 'TODO',
|
||||
priority INTEGER DEFAULT 0,
|
||||
agent_type TEXT DEFAULT '',
|
||||
agent_profile_id TEXT DEFAULT '',
|
||||
repository_url TEXT DEFAULT '',
|
||||
branch TEXT DEFAULT '',
|
||||
assigned_to TEXT DEFAULT '',
|
||||
@@ -793,9 +793,9 @@ func (r *SQLiteRepository) CreateTask(ctx context.Context, task *models.Task) er
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO tasks (id, workspace_id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at)
|
||||
INSERT INTO tasks (id, workspace_id, board_id, column_id, title, description, state, priority, agent_profile_id, repository_url, branch, assigned_to, position, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, task.ID, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt)
|
||||
`, task.ID, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentProfileID, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.CreatedAt, task.UpdatedAt)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -815,12 +815,12 @@ func (r *SQLiteRepository) CreateTask(ctx context.Context, task *models.Task) er
|
||||
func (r *SQLiteRepository) GetTask(ctx context.Context, id string) (*models.Task, error) {
|
||||
task := &models.Task{}
|
||||
var metadata string
|
||||
var agentType, repositoryURL, branch, assignedTo sql.NullString
|
||||
var agentProfileID, repositoryURL, branch, assignedTo sql.NullString
|
||||
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, workspace_id, board_id, column_id, title, description, state, priority, agent_type, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
SELECT id, workspace_id, board_id, column_id, title, description, state, priority, agent_profile_id, repository_url, branch, assigned_to, position, metadata, created_at, updated_at
|
||||
FROM tasks WHERE id = ?
|
||||
`, id).Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &agentType, &repositoryURL, &branch, &assignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
`, id).Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &agentProfileID, &repositoryURL, &branch, &assignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("task not found: %s", id)
|
||||
@@ -829,7 +829,7 @@ func (r *SQLiteRepository) GetTask(ctx context.Context, id string) (*models.Task
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.AgentType = agentType.String
|
||||
task.AgentProfileID = agentProfileID.String
|
||||
task.RepositoryURL = repositoryURL.String
|
||||
task.Branch = branch.String
|
||||
task.AssignedTo = assignedTo.String
|
||||
@@ -847,9 +847,9 @@ func (r *SQLiteRepository) UpdateTask(ctx context.Context, task *models.Task) er
|
||||
}
|
||||
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
UPDATE tasks SET workspace_id = ?, board_id = ?, column_id = ?, title = ?, description = ?, state = ?, priority = ?, agent_type = ?, repository_url = ?, branch = ?, assigned_to = ?, position = ?, metadata = ?, updated_at = ?
|
||||
UPDATE tasks SET workspace_id = ?, board_id = ?, column_id = ?, title = ?, description = ?, state = ?, priority = ?, agent_profile_id = ?, repository_url = ?, branch = ?, assigned_to = ?, position = ?, metadata = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentType, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.UpdatedAt, task.ID)
|
||||
`, task.WorkspaceID, task.BoardID, task.ColumnID, task.Title, task.Description, task.State, task.Priority, task.AgentProfileID, task.RepositoryURL, task.Branch, task.AssignedTo, task.Position, string(metadata), task.UpdatedAt, task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -884,7 +884,7 @@ func (r *SQLiteRepository) DeleteTask(ctx context.Context, id string) error {
|
||||
// ListTasks returns all tasks for a board
|
||||
func (r *SQLiteRepository) ListTasks(ctx context.Context, boardID string) ([]*models.Task, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_type, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_profile_id, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
tc.board_id, tc.column_id, tc.position
|
||||
FROM tasks t
|
||||
INNER JOIN task_columns tc ON tc.task_id = t.id
|
||||
@@ -902,7 +902,7 @@ func (r *SQLiteRepository) ListTasks(ctx context.Context, boardID string) ([]*mo
|
||||
// ListTasksByColumn returns all tasks in a column
|
||||
func (r *SQLiteRepository) ListTasksByColumn(ctx context.Context, columnID string) ([]*models.Task, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_type, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
SELECT t.id, t.workspace_id, t.title, t.description, t.state, t.priority, t.agent_profile_id, t.repository_url, t.branch, t.assigned_to, t.metadata, t.created_at, t.updated_at,
|
||||
tc.board_id, tc.column_id, tc.position
|
||||
FROM tasks t
|
||||
INNER JOIN task_columns tc ON tc.task_id = t.id
|
||||
@@ -922,12 +922,12 @@ func (r *SQLiteRepository) scanTasks(rows *sql.Rows) ([]*models.Task, error) {
|
||||
for rows.Next() {
|
||||
task := &models.Task{}
|
||||
var metadata string
|
||||
var agentType, repositoryURL, branch, assignedTo sql.NullString
|
||||
err := rows.Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &agentType, &repositoryURL, &branch, &assignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
var agentProfileID, repositoryURL, branch, assignedTo sql.NullString
|
||||
err := rows.Scan(&task.ID, &task.WorkspaceID, &task.BoardID, &task.ColumnID, &task.Title, &task.Description, &task.State, &task.Priority, &agentProfileID, &repositoryURL, &branch, &assignedTo, &task.Position, &metadata, &task.CreatedAt, &task.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task.AgentType = agentType.String
|
||||
task.AgentProfileID = agentProfileID.String
|
||||
task.RepositoryURL = repositoryURL.String
|
||||
task.Branch = branch.String
|
||||
task.AssignedTo = assignedTo.String
|
||||
@@ -942,7 +942,7 @@ func (r *SQLiteRepository) scanTasksWithPlacement(rows *sql.Rows) ([]*models.Tas
|
||||
for rows.Next() {
|
||||
task := &models.Task{}
|
||||
var metadata string
|
||||
var agentType, repositoryURL, branch, assignedTo sql.NullString
|
||||
var agentProfileID, repositoryURL, branch, assignedTo sql.NullString
|
||||
err := rows.Scan(
|
||||
&task.ID,
|
||||
&task.WorkspaceID,
|
||||
@@ -950,7 +950,7 @@ func (r *SQLiteRepository) scanTasksWithPlacement(rows *sql.Rows) ([]*models.Tas
|
||||
&task.Description,
|
||||
&task.State,
|
||||
&task.Priority,
|
||||
&agentType,
|
||||
&agentProfileID,
|
||||
&repositoryURL,
|
||||
&branch,
|
||||
&assignedTo,
|
||||
@@ -964,7 +964,7 @@ func (r *SQLiteRepository) scanTasksWithPlacement(rows *sql.Rows) ([]*models.Tas
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task.AgentType = agentType.String
|
||||
task.AgentProfileID = agentProfileID.String
|
||||
task.RepositoryURL = repositoryURL.String
|
||||
task.Branch = branch.String
|
||||
task.AssignedTo = assignedTo.String
|
||||
|
||||
@@ -58,7 +58,7 @@ type CreateTaskRequest struct {
|
||||
Description string `json:"description"`
|
||||
Priority int `json:"priority"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType string `json:"agent_type,omitempty"`
|
||||
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
AssignedTo string `json:"assigned_to,omitempty"`
|
||||
@@ -72,7 +72,7 @@ type UpdateTaskRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
State *v1.TaskState `json:"state,omitempty"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
AssignedTo *string `json:"assigned_to,omitempty"`
|
||||
Position *int `json:"position,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
@@ -228,7 +228,7 @@ func (s *Service) CreateTask(ctx context.Context, req *CreateTaskRequest) (*mode
|
||||
Description: req.Description,
|
||||
State: state,
|
||||
Priority: req.Priority,
|
||||
AgentType: req.AgentType,
|
||||
AgentProfileID: req.AgentProfileID,
|
||||
RepositoryURL: req.RepositoryURL,
|
||||
Branch: req.Branch,
|
||||
AssignedTo: req.AssignedTo,
|
||||
@@ -276,8 +276,8 @@ func (s *Service) UpdateTask(ctx context.Context, id string, req *UpdateTaskRequ
|
||||
task.State = *req.State
|
||||
stateChanged = true
|
||||
}
|
||||
if req.AgentType != nil {
|
||||
task.AgentType = *req.AgentType
|
||||
if req.AgentProfileID != nil {
|
||||
task.AgentProfileID = *req.AgentProfileID
|
||||
}
|
||||
if req.AssignedTo != nil {
|
||||
task.AssignedTo = *req.AssignedTo
|
||||
@@ -1054,8 +1054,8 @@ func (s *Service) publishTaskEvent(ctx context.Context, eventType string, task *
|
||||
"updated_at": task.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if task.AgentType != "" {
|
||||
data["agent_type"] = task.AgentType
|
||||
if task.AgentProfileID != "" {
|
||||
data["agent_profile_id"] = task.AgentProfileID
|
||||
}
|
||||
if task.AssignedTo != "" {
|
||||
data["assigned_to"] = task.AssignedTo
|
||||
|
||||
@@ -26,7 +26,7 @@ type ResourceLimits struct {
|
||||
type AgentInstance struct {
|
||||
ID string `json:"id"`
|
||||
TaskID string `json:"task_id"`
|
||||
AgentType string `json:"agent_type"`
|
||||
AgentProfileID string `json:"agent_profile_id"`
|
||||
ContainerID *string `json:"container_id,omitempty"`
|
||||
ContainerName *string `json:"container_name,omitempty"`
|
||||
Status AgentStatus `json:"status"`
|
||||
|
||||
@@ -27,7 +27,7 @@ type Task struct {
|
||||
Description string `json:"description"`
|
||||
State TaskState `json:"state"`
|
||||
Priority int `json:"priority"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL *string `json:"repository_url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
AssignedAgentID *string `json:"assigned_agent_id,omitempty"`
|
||||
@@ -43,8 +43,8 @@ type Task struct {
|
||||
type CreateTaskRequest struct {
|
||||
Title string `json:"title" binding:"required,max=500"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
Priority int `json:"priority" binding:"min=0,max=10"`
|
||||
AgentType *string `json:"agent_type,omitempty"`
|
||||
Priority int `json:"priority" binding:"min=0,max=10"`
|
||||
AgentProfileID *string `json:"agent_profile_id,omitempty"`
|
||||
RepositoryURL *string `json:"repository_url,omitempty"`
|
||||
Branch *string `json:"branch,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
|
||||
@@ -35,9 +35,13 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
items: workspaces.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
description: workspace.description ?? null,
|
||||
owner_id: workspace.owner_id,
|
||||
default_executor_id: workspace.default_executor_id ?? null,
|
||||
default_environment_id: workspace.default_environment_id ?? null,
|
||||
default_agent_profile_id: workspace.default_agent_profile_id ?? null,
|
||||
created_at: workspace.created_at,
|
||||
updated_at: workspace.updated_at,
|
||||
})),
|
||||
activeId: activeWorkspaceId,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Suspense, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { IconCloud, IconServer } from '@tabler/icons-react';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
@@ -16,6 +16,14 @@ const EXECUTOR_TYPES = ['local_docker', 'remote_docker'] as const;
|
||||
type ExecutorType = (typeof EXECUTOR_TYPES)[number];
|
||||
|
||||
export default function ExecutorCreatePage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="p-4">Loading...</div>}>
|
||||
<ExecutorCreatePageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutorCreatePageContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const initialType = searchParams.get('type');
|
||||
|
||||
@@ -31,16 +31,24 @@ export function WorkspacesPageClient() {
|
||||
{
|
||||
id: created.id,
|
||||
name: created.name,
|
||||
description: created.description ?? null,
|
||||
owner_id: created.owner_id,
|
||||
default_executor_id: created.default_executor_id ?? null,
|
||||
default_environment_id: created.default_environment_id ?? null,
|
||||
default_agent_profile_id: created.default_agent_profile_id ?? null,
|
||||
created_at: created.created_at,
|
||||
updated_at: created.updated_at,
|
||||
},
|
||||
...items.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
description: workspace.description ?? null,
|
||||
owner_id: workspace.owner_id,
|
||||
default_executor_id: workspace.default_executor_id ?? null,
|
||||
default_environment_id: workspace.default_environment_id ?? null,
|
||||
default_agent_profile_id: workspace.default_agent_profile_id ?? null,
|
||||
created_at: workspace.created_at,
|
||||
updated_at: workspace.updated_at,
|
||||
})),
|
||||
]);
|
||||
setNewWorkspaceName('');
|
||||
|
||||
@@ -6,6 +6,10 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@kandev/ui
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@kandev/ui/tabs';
|
||||
import { TooltipProvider } from '@kandev/ui/tooltip';
|
||||
import { Textarea } from '@kandev/ui/textarea';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@kandev/ui/dialog';
|
||||
import { Button } from '@kandev/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
|
||||
import { Label } from '@kandev/ui/label';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { getLocalStorage, setLocalStorage } from '@/lib/local-storage';
|
||||
import { TaskChatPanel } from '@/components/task/task-chat-panel';
|
||||
@@ -22,6 +26,13 @@ import { useAppStore, useAppStoreApi } from '@/components/state-provider';
|
||||
import { useRepositories } from '@/hooks/use-repositories';
|
||||
import { useRepositoryBranches } from '@/hooks/use-repository-branches';
|
||||
|
||||
type AgentProfile = {
|
||||
id: string;
|
||||
name: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
};
|
||||
|
||||
const AGENTS = [
|
||||
{ id: 'codex', label: 'Codex' },
|
||||
{ id: 'claude', label: 'Claude Code' },
|
||||
@@ -191,10 +202,14 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
|
||||
const client = getWebSocketClient();
|
||||
if (!client) return;
|
||||
|
||||
// Require agent_profile_id to be set on the task
|
||||
if (!task.agent_profile_id) {
|
||||
console.error('No agent profile configured for this task. Please edit the task to select an agent profile.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAgentLoading(true);
|
||||
try {
|
||||
// Use task's agent_type if set, otherwise default to 'augment-agent'
|
||||
const agentType = task.agent_type ?? 'augment-agent';
|
||||
interface StartResponse {
|
||||
success: boolean;
|
||||
task_id: string;
|
||||
@@ -205,7 +220,7 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
|
||||
}
|
||||
const response = await client.request<StartResponse>('orchestrator.start', {
|
||||
task_id: task.id,
|
||||
agent_type: agentType,
|
||||
agent_profile_id: task.agent_profile_id,
|
||||
}, 15000);
|
||||
setIsAgentRunning(true);
|
||||
|
||||
@@ -219,7 +234,7 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
|
||||
} finally {
|
||||
setIsAgentLoading(false);
|
||||
}
|
||||
}, [task?.id, task?.agent_type]);
|
||||
}, [task?.id, task?.agent_profile_id]);
|
||||
|
||||
const handleStopAgent = useCallback(async () => {
|
||||
if (!task?.id) return;
|
||||
@@ -344,6 +359,7 @@ export default function TaskPage({ task: initialTask }: TaskPageClientProps) {
|
||||
onSend={handleSendMessage}
|
||||
isLoading={isLoadingComments}
|
||||
isAgentWorking={isAgentRunning}
|
||||
taskDescription={task.description}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
|
||||
@@ -229,7 +229,7 @@ export function TaskCreateDialog({
|
||||
description: description.trim(),
|
||||
repository_url: repository?.local_path ?? '',
|
||||
branch: branch || '',
|
||||
agent_type: startAgent ? 'default' : '',
|
||||
agent_profile_id: agentProfileId || undefined,
|
||||
environment_id: environmentId || undefined,
|
||||
executor_id: executorId || undefined,
|
||||
state: targetState,
|
||||
|
||||
@@ -44,6 +44,7 @@ type TaskChatPanelProps = {
|
||||
onSend: (message: string) => void;
|
||||
isLoading?: boolean;
|
||||
isAgentWorking?: boolean;
|
||||
taskDescription?: string;
|
||||
};
|
||||
|
||||
type ToolCallMetadata = {
|
||||
@@ -199,7 +200,7 @@ function TypingIndicator() {
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking }: TaskChatPanelProps) {
|
||||
export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking, taskDescription }: TaskChatPanelProps) {
|
||||
const [messageInput, setMessageInput] = useState('');
|
||||
const [selectedAgent, setSelectedAgent] = useState(agents[0]?.id ?? '');
|
||||
const [planModeEnabled, setPlanModeEnabled] = useState(false);
|
||||
@@ -217,6 +218,21 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking }: Tas
|
||||
(c) => c.type === 'message' || c.type === 'content' || c.type === 'tool_call' || !c.type
|
||||
);
|
||||
|
||||
// Create a synthetic "user" message for the task description
|
||||
const taskDescriptionMessage: Comment | null = taskDescription ? {
|
||||
id: 'task-description',
|
||||
task_id: commentsState?.taskId ?? '',
|
||||
author_type: 'user',
|
||||
content: taskDescription,
|
||||
type: 'message',
|
||||
created_at: '',
|
||||
} : null;
|
||||
|
||||
// Combine task description with visible comments
|
||||
const allMessages = taskDescriptionMessage
|
||||
? [taskDescriptionMessage, ...visibleComments]
|
||||
: visibleComments;
|
||||
|
||||
// Count agent messages to detect new responses
|
||||
const agentMessageCount = visibleComments.filter((c) => c.author_type !== 'user').length;
|
||||
|
||||
@@ -265,12 +281,12 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking }: Tas
|
||||
<span>Loading comments...</span>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !commentsLoading && visibleComments.length === 0 && (
|
||||
{!isLoading && !commentsLoading && allMessages.length === 0 && (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
<span>No messages yet. Start the conversation!</span>
|
||||
</div>
|
||||
)}
|
||||
{visibleComments.map((comment) => {
|
||||
{allMessages.map((comment) => {
|
||||
// Tool call comments have a special rendering
|
||||
if (comment.type === 'tool_call') {
|
||||
return <ToolCallCard key={comment.id} comment={comment} />;
|
||||
@@ -278,18 +294,36 @@ export function TaskChatPanel({ agents, onSend, isLoading, isAgentWorking }: Tas
|
||||
|
||||
// Regular message or agent response - render as markdown
|
||||
const isUser = comment.author_type === 'user';
|
||||
const isTaskDescription = comment.id === 'task-description';
|
||||
|
||||
// Determine label and styling
|
||||
let label: string;
|
||||
let containerClass: string;
|
||||
|
||||
if (isTaskDescription) {
|
||||
label = 'Task';
|
||||
containerClass = 'ml-auto bg-primary/20 text-foreground border border-primary/40';
|
||||
} else if (isUser) {
|
||||
label = 'You';
|
||||
containerClass = 'ml-auto bg-primary text-primary-foreground';
|
||||
} else {
|
||||
label = 'Agent';
|
||||
containerClass = 'bg-muted text-foreground';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comment.id}
|
||||
className={cn(
|
||||
'max-w-[85%] rounded-lg px-4 py-3 text-sm',
|
||||
isUser
|
||||
? 'ml-auto bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-foreground'
|
||||
containerClass
|
||||
)}
|
||||
>
|
||||
<p className="text-[11px] uppercase tracking-wide mb-2 opacity-70">
|
||||
{isUser ? 'You' : 'Agent'}
|
||||
<p className={cn(
|
||||
"text-[11px] uppercase tracking-wide mb-2",
|
||||
isTaskDescription ? "text-primary font-medium" : "opacity-70"
|
||||
)}>
|
||||
{label}
|
||||
</p>
|
||||
{isUser ? (
|
||||
<p className="whitespace-pre-wrap">{formatContent(comment)}</p>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function useUserDisplaySettings({
|
||||
fetchUserSettings({ cache: 'no-store' })
|
||||
.then((data) => {
|
||||
if (!data?.settings) return;
|
||||
const repositoryIds = Array.from(new Set(data.settings.repository_ids ?? [])).sort();
|
||||
const repositoryIds = Array.from(new Set<string>(data.settings.repository_ids ?? [])).sort();
|
||||
setUserSettings({
|
||||
workspaceId: data.settings.workspace_id || null,
|
||||
boardId: data.settings.board_id || null,
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function createTask(
|
||||
position?: number;
|
||||
repository_url?: string;
|
||||
branch?: string;
|
||||
agent_type?: string;
|
||||
agent_profile_id?: string;
|
||||
environment_id?: string;
|
||||
executor_id?: string;
|
||||
state?: Task['state'];
|
||||
@@ -112,7 +112,7 @@ export async function updateTask(
|
||||
title?: string;
|
||||
description?: string;
|
||||
position?: number;
|
||||
agent_type?: string;
|
||||
agent_profile_id?: string;
|
||||
assigned_to?: string;
|
||||
state?: Task['state'];
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@ import type { KanbanState } from '@/lib/state/store';
|
||||
|
||||
export type KanbanTask = KanbanState['tasks'][number];
|
||||
|
||||
// Minimal task type for filtering - only needs id and repositoryUrl
|
||||
export type FilterableTask = {
|
||||
id: string;
|
||||
repositoryUrl?: string;
|
||||
};
|
||||
|
||||
export function mapSelectedRepositoryPaths(
|
||||
repositories: Repository[],
|
||||
selectedIds: string[]
|
||||
@@ -21,10 +27,10 @@ export function mapSelectedRepositoryPaths(
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function filterTasksByRepositories(
|
||||
tasks: KanbanTask[],
|
||||
export function filterTasksByRepositories<T extends FilterableTask>(
|
||||
tasks: T[],
|
||||
selectedRepositoryPaths: Set<string>
|
||||
): KanbanTask[] {
|
||||
): T[] {
|
||||
if (selectedRepositoryPaths.size === 0) {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
@@ -30,9 +30,13 @@ export type WorkspaceState = {
|
||||
items: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
owner_id: string;
|
||||
default_executor_id?: string | null;
|
||||
default_environment_id?: string | null;
|
||||
default_agent_profile_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>;
|
||||
activeId: string | null;
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export type Task = {
|
||||
description: string;
|
||||
state: TaskState;
|
||||
priority: number;
|
||||
agent_type?: string | null;
|
||||
agent_profile_id?: string | null;
|
||||
repository_url?: string | null;
|
||||
branch?: string | null;
|
||||
assigned_agent_id?: string | null;
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import type { StoreApi } from 'zustand';
|
||||
import type { AppState } from '@/lib/state/store';
|
||||
import type { WsHandlers } from '@/lib/ws/handlers/types';
|
||||
import type { Environment } from '@/lib/types/http';
|
||||
|
||||
export function registerEnvironmentsHandlers(store: StoreApi<AppState>): WsHandlers {
|
||||
return {
|
||||
'environment.created': (message) => {
|
||||
const payload = message.payload;
|
||||
const environment: Environment = {
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
kind: payload.kind,
|
||||
is_system: payload.is_system,
|
||||
worktree_root: payload.worktree_root ?? null,
|
||||
image_tag: payload.image_tag ?? null,
|
||||
dockerfile: payload.dockerfile ?? null,
|
||||
build_config: payload.build_config ?? null,
|
||||
created_at: payload.created_at ?? new Date().toISOString(),
|
||||
updated_at: payload.updated_at ?? new Date().toISOString(),
|
||||
};
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
environments: {
|
||||
items: [
|
||||
...state.environments.items.filter((item) => item.id !== message.payload.id),
|
||||
message.payload,
|
||||
],
|
||||
items: [...state.environments.items.filter((item) => item.id !== environment.id), environment],
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import type { StoreApi } from 'zustand';
|
||||
import type { AppState } from '@/lib/state/store';
|
||||
import type { WsHandlers } from '@/lib/ws/handlers/types';
|
||||
import type { Executor } from '@/lib/types/http';
|
||||
|
||||
export function registerExecutorsHandlers(store: StoreApi<AppState>): WsHandlers {
|
||||
return {
|
||||
'executor.created': (message) => {
|
||||
const payload = message.payload;
|
||||
const executor: Executor = {
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
type: payload.type,
|
||||
status: payload.status,
|
||||
is_system: payload.is_system,
|
||||
config: payload.config,
|
||||
created_at: payload.created_at ?? new Date().toISOString(),
|
||||
updated_at: payload.updated_at ?? new Date().toISOString(),
|
||||
};
|
||||
store.setState((state) => ({
|
||||
...state,
|
||||
executors: {
|
||||
items: [
|
||||
...state.executors.items.filter((item) => item.id !== message.payload.id),
|
||||
message.payload,
|
||||
],
|
||||
items: [...state.executors.items.filter((item) => item.id !== executor.id), executor],
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import type { StoreApi } from 'zustand';
|
||||
import type { AppState } from '@/lib/state/store';
|
||||
import type { AppState, WorkspaceState } from '@/lib/state/store';
|
||||
import type { WsHandlers } from '@/lib/ws/handlers/types';
|
||||
|
||||
type WorkspaceItem = WorkspaceState['items'][number];
|
||||
|
||||
export function registerWorkspacesHandlers(store: StoreApi<AppState>): WsHandlers {
|
||||
return {
|
||||
'workspace.created': (message) => {
|
||||
store.setState((state) => {
|
||||
const exists = state.workspaces.items.some((item) => item.id === message.payload.id);
|
||||
const payload = message.payload;
|
||||
const newWorkspace: WorkspaceItem = {
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
description: payload.description ?? null,
|
||||
owner_id: payload.owner_id ?? '',
|
||||
default_executor_id: payload.default_executor_id ?? null,
|
||||
default_environment_id: payload.default_environment_id ?? null,
|
||||
default_agent_profile_id: payload.default_agent_profile_id ?? null,
|
||||
created_at: payload.created_at ?? new Date().toISOString(),
|
||||
updated_at: payload.updated_at ?? new Date().toISOString(),
|
||||
};
|
||||
const exists = state.workspaces.items.some((item) => item.id === payload.id);
|
||||
const items = exists
|
||||
? state.workspaces.items.map((item) =>
|
||||
item.id === message.payload.id
|
||||
? {
|
||||
...item,
|
||||
name: message.payload.name,
|
||||
default_executor_id: message.payload.default_executor_id ?? null,
|
||||
default_environment_id: message.payload.default_environment_id ?? null,
|
||||
default_agent_profile_id: message.payload.default_agent_profile_id ?? null,
|
||||
}
|
||||
: item
|
||||
)
|
||||
: [
|
||||
{
|
||||
id: message.payload.id,
|
||||
name: message.payload.name,
|
||||
default_executor_id: message.payload.default_executor_id ?? null,
|
||||
default_environment_id: message.payload.default_environment_id ?? null,
|
||||
default_agent_profile_id: message.payload.default_agent_profile_id ?? null,
|
||||
},
|
||||
...state.workspaces.items,
|
||||
];
|
||||
const activeId = state.workspaces.activeId ?? message.payload.id;
|
||||
? state.workspaces.items.map((item) => (item.id === payload.id ? { ...item, ...newWorkspace } : item))
|
||||
: [newWorkspace, ...state.workspaces.items];
|
||||
const activeId = state.workspaces.activeId ?? payload.id;
|
||||
return {
|
||||
...state,
|
||||
workspaces: {
|
||||
@@ -49,9 +44,11 @@ export function registerWorkspacesHandlers(store: StoreApi<AppState>): WsHandler
|
||||
? {
|
||||
...item,
|
||||
name: message.payload.name,
|
||||
description: message.payload.description ?? item.description,
|
||||
default_executor_id: message.payload.default_executor_id ?? null,
|
||||
default_environment_id: message.payload.default_environment_id ?? null,
|
||||
default_agent_profile_id: message.payload.default_agent_profile_id ?? null,
|
||||
updated_at: message.payload.updated_at ?? item.updated_at,
|
||||
}
|
||||
: item
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user