From 1e5103eb7051665a361270ed804fd2f28ac1f3bf Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Wed, 4 Mar 2026 23:41:26 +0000 Subject: [PATCH] feat: enhance agent management with support for multiple instances and update UI accordingly --- .claude/plans/01-bot-tools.md | 2 +- cmd/agentctl/main.go | 21 ++++++++++----------- dev-scripts/start.sh | 16 ++-------------- pkg/tui/model.go | 7 ++++--- pkg/tui/view.go | 16 ++++++++++------ shell/process/manager.go | 32 ++++++++++++++++++++------------ shell/tui/adapter.go | 2 +- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.claude/plans/01-bot-tools.md b/.claude/plans/01-bot-tools.md index de4fea2..d532a13 100644 --- a/.claude/plans/01-bot-tools.md +++ b/.claude/plans/01-bot-tools.md @@ -4,7 +4,7 @@ Permitir que los bots ejecuten herramientas reales (funciones Go) como respuesta a decisiones del LLM — patrón function calling / tool use. -## Estado: pendiente +## Estado: COMPLETADO --- diff --git a/cmd/agentctl/main.go b/cmd/agentctl/main.go index 29ebb32..cd3da10 100644 --- a/cmd/agentctl/main.go +++ b/cmd/agentctl/main.go @@ -74,14 +74,15 @@ func listCmd(mgr *process.Manager) *cobra.Command { return nil } - fmt.Printf("%-20s %-12s %-8s %s\n", "ID", "STATUS", "VERSION", "DESCRIPTION") - fmt.Println(strings.Repeat("─", 72)) + fmt.Printf("%-20s %-14s %-8s %-4s %s\n", "ID", "STATUS", "VERSION", "INST", "DESCRIPTION") + fmt.Println(strings.Repeat("─", 78)) for _, s := range statuses { - fmt.Printf("%-20s %-12s %-8s %s\n", + fmt.Printf("%-20s %-14s %-8s %-4d %s\n", s.ID, statusLabel(s), s.Version, - truncate(s.Desc, 36), + s.Instances, + truncate(s.Desc, 32), ) } return nil @@ -112,18 +113,13 @@ func startCmd(mgr *process.Manager, binPath *string) *cobra.Command { fmt.Printf("skip %-20s (disabled in config)\n", s.ID) continue } - if s.Running { - fmt.Printf("skip %-20s (already running, PID %d)\n", s.ID, s.PID) - continue - } - if err := mgr.Start(s.AgentInfo); err != nil { fmt.Fprintf(os.Stderr, "fail %-20s %v\n", s.ID, err) continue } - fmt.Printf("start %-20s PID %d log → %s\n", - s.ID, mgr.ReadPID(s.ID), mgr.LogPath(s.ID)) + fmt.Printf("start %-20s PID %d (instances: %d) log → %s\n", + s.ID, mgr.ReadPID(s.ID), mgr.InstanceCount(s.ID), mgr.LogPath(s.ID)) started++ } @@ -244,6 +240,9 @@ func statusLabel(s process.AgentStatus) string { case !s.Enabled: return "disabled" case s.Running: + if s.Instances > 1 { + return fmt.Sprintf("● running(%d)", s.Instances) + } return "● running" default: return "○ stopped" diff --git a/dev-scripts/start.sh b/dev-scripts/start.sh index 0d7d7b9..f274536 100755 --- a/dev-scripts/start.sh +++ b/dev-scripts/start.sh @@ -16,14 +16,6 @@ start_agent() { local pid_f; pid_f="$(pid_file "$id")" local bin="$REPO_ROOT/bin/launcher" - # Check for duplicate instances already running - local existing; existing="$(count_instances "$id")" - if [[ "$existing" -gt 0 ]]; then - warn "$id already has $existing instance(s) running (orphan processes?)" - warn " Run ./dev-scripts/stop.sh $id first to clean up" - return 1 - fi - info "Iniciando $id..." # Build the binary first to avoid go run wrapper PID issues @@ -46,7 +38,8 @@ start_agent() { # Espera un momento y verifica que el proceso siga vivo sleep 1 if kill -0 "$pid" 2>/dev/null; then - ok "$id PID $pid → logs: $log" + local inst; inst="$(count_instances "$id")" + ok "$id PID $pid (instances: $inst) → logs: $log" else rm -f "$pid_f" fail "$id arrancó pero murió — revisa: tail -f $log" @@ -64,11 +57,6 @@ while IFS='|' read -r id version enabled desc cfg; do continue fi - if is_running "$id"; then - warn "$id (ya corriendo, PID $(read_pid "$id"))" - continue - fi - start_agent "$id" "$cfg" ((started++)) || true diff --git a/pkg/tui/model.go b/pkg/tui/model.go index cc083e4..011c971 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -71,9 +71,10 @@ func ServerMenuOptions() []MenuOption { func AgentActionOptions(running bool) []MenuOption { if running { return []MenuOption{ - {Label: "Stop", Desc: "Detener el agente"}, - {Label: "Restart", Desc: "Reiniciar"}, - {Label: "Kill", Desc: "SIGKILL forzado"}, + {Label: "Start", Desc: "Iniciar otra instancia"}, + {Label: "Stop", Desc: "Detener todas las instancias"}, + {Label: "Restart", Desc: "Reiniciar (stop all + start)"}, + {Label: "Kill", Desc: "SIGKILL forzado a todas"}, {Label: "Logs", Desc: "Ver log del agente"}, } } diff --git a/pkg/tui/view.go b/pkg/tui/view.go index 354c7ab..7683f08 100644 --- a/pkg/tui/view.go +++ b/pkg/tui/view.go @@ -75,15 +75,15 @@ func viewAgentList(m Model) string { status = "disabled" } else if a.Running { icon = "●" - status = fmt.Sprintf("running PID %d", a.PID) + if a.Instances > 1 { + status = fmt.Sprintf("running %d instances", a.Instances) + } else { + status = fmt.Sprintf("running PID %d", a.PID) + } } b.WriteString(fmt.Sprintf(" %s%s %-20s %-8s %s\n", cursor, icon, a.ID, a.Version, status)) - - if a.Instances > 1 { - b.WriteString(fmt.Sprintf(" ⚠ WARNING: %d instances running!\n", a.Instances)) - } } if m.StatusMsg != "" { @@ -104,7 +104,11 @@ func viewAgentActions(m Model) string { a := m.Selected icon := "○ stopped" if a.Running { - icon = fmt.Sprintf("● running PID %d", a.PID) + if a.Instances > 1 { + icon = fmt.Sprintf("● running %d instances", a.Instances) + } else { + icon = fmt.Sprintf("● running PID %d", a.PID) + } } b.WriteString(fmt.Sprintf("\n %s %s\n", a.ID, icon)) diff --git a/shell/process/manager.go b/shell/process/manager.go index 8beccdd..9b6ff64 100644 --- a/shell/process/manager.go +++ b/shell/process/manager.go @@ -29,8 +29,9 @@ type AgentInfo struct { // AgentStatus combines agent metadata with runtime state. type AgentStatus struct { AgentInfo - Running bool - PID int + Running bool + PID int + Instances int } // ProcessStats holds resource usage for a running process. @@ -82,9 +83,17 @@ func (m *Manager) Scan() ([]AgentInfo, error) { // Status returns the runtime status for a single agent. func (m *Manager) Status(info AgentInfo) AgentStatus { - pid := m.resolveRunningPID(info.ID) - running := pid > 0 - return AgentStatus{AgentInfo: info, Running: running, PID: pid} + pids := m.findProcessPIDs(info.ID) + primary := 0 + if len(pids) > 0 { + primary = pids[0] + } + return AgentStatus{ + AgentInfo: info, + Running: len(pids) > 0, + PID: primary, + Instances: len(pids), + } } // StatusAll returns status for every discovered agent. @@ -101,13 +110,8 @@ func (m *Manager) StatusAll() ([]AgentStatus, error) { } // Start launches an agent process in the background. +// Multiple instances of the same agent are allowed. func (m *Manager) Start(info AgentInfo) error { - // Check for orphan instances - if existing := m.findProcessPIDs(info.ID); len(existing) > 0 { - return fmt.Errorf("agent %q already has %d running instance(s) (PIDs: %v) — stop them first", - info.ID, len(existing), existing) - } - if err := os.MkdirAll(m.runDir, 0o755); err != nil { return fmt.Errorf("create run dir: %w", err) } @@ -233,7 +237,11 @@ func (m *Manager) Stats(id string) (ProcessStats, error) { if pid == 0 { return ProcessStats{}, fmt.Errorf("agent %q is not running", id) } + return m.statsForPID(pid, id), nil +} +// statsForPID gathers resource usage for a specific PID. +func (m *Manager) statsForPID(pid int, id string) ProcessStats { s := ProcessStats{PID: pid} // Uptime from /proc//stat @@ -278,7 +286,7 @@ func (m *Manager) Stats(id string) (ProcessStats, error) { s.LogBytes = info.Size() } - return s, nil + return s } // LogTail returns the last N lines of an agent's log. diff --git a/shell/tui/adapter.go b/shell/tui/adapter.go index 500e481..358ca8a 100644 --- a/shell/tui/adapter.go +++ b/shell/tui/adapter.go @@ -84,7 +84,7 @@ func (a *Adapter) loadAgents() tea.Cmd { Enabled: s.Enabled, Running: s.Running, PID: s.PID, - Instances: a.mgr.InstanceCount(s.ID), + Instances: s.Instances, } if s.Running {