feat: enhance agent management with support for multiple instances and update UI accordingly

This commit is contained in:
2026-03-04 23:41:26 +00:00
parent bcbbd974e3
commit 1e5103eb70
7 changed files with 48 additions and 48 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
Permitir que los bots ejecuten herramientas reales (funciones Go) como respuesta a Permitir que los bots ejecuten herramientas reales (funciones Go) como respuesta a
decisiones del LLM — patrón function calling / tool use. decisiones del LLM — patrón function calling / tool use.
## Estado: pendiente ## Estado: COMPLETADO
--- ---
+10 -11
View File
@@ -74,14 +74,15 @@ func listCmd(mgr *process.Manager) *cobra.Command {
return nil return nil
} }
fmt.Printf("%-20s %-12s %-8s %s\n", "ID", "STATUS", "VERSION", "DESCRIPTION") fmt.Printf("%-20s %-14s %-8s %-4s %s\n", "ID", "STATUS", "VERSION", "INST", "DESCRIPTION")
fmt.Println(strings.Repeat("─", 72)) fmt.Println(strings.Repeat("─", 78))
for _, s := range statuses { for _, s := range statuses {
fmt.Printf("%-20s %-12s %-8s %s\n", fmt.Printf("%-20s %-14s %-8s %-4d %s\n",
s.ID, s.ID,
statusLabel(s), statusLabel(s),
s.Version, s.Version,
truncate(s.Desc, 36), s.Instances,
truncate(s.Desc, 32),
) )
} }
return nil 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) fmt.Printf("skip %-20s (disabled in config)\n", s.ID)
continue 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 { if err := mgr.Start(s.AgentInfo); err != nil {
fmt.Fprintf(os.Stderr, "fail %-20s %v\n", s.ID, err) fmt.Fprintf(os.Stderr, "fail %-20s %v\n", s.ID, err)
continue continue
} }
fmt.Printf("start %-20s PID %d log → %s\n", fmt.Printf("start %-20s PID %d (instances: %d) log → %s\n",
s.ID, mgr.ReadPID(s.ID), mgr.LogPath(s.ID)) s.ID, mgr.ReadPID(s.ID), mgr.InstanceCount(s.ID), mgr.LogPath(s.ID))
started++ started++
} }
@@ -244,6 +240,9 @@ func statusLabel(s process.AgentStatus) string {
case !s.Enabled: case !s.Enabled:
return "disabled" return "disabled"
case s.Running: case s.Running:
if s.Instances > 1 {
return fmt.Sprintf("● running(%d)", s.Instances)
}
return "● running" return "● running"
default: default:
return "○ stopped" return "○ stopped"
+2 -14
View File
@@ -16,14 +16,6 @@ start_agent() {
local pid_f; pid_f="$(pid_file "$id")" local pid_f; pid_f="$(pid_file "$id")"
local bin="$REPO_ROOT/bin/launcher" 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..." info "Iniciando $id..."
# Build the binary first to avoid go run wrapper PID issues # 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 # Espera un momento y verifica que el proceso siga vivo
sleep 1 sleep 1
if kill -0 "$pid" 2>/dev/null; then 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 else
rm -f "$pid_f" rm -f "$pid_f"
fail "$id arrancó pero murió — revisa: tail -f $log" fail "$id arrancó pero murió — revisa: tail -f $log"
@@ -64,11 +57,6 @@ while IFS='|' read -r id version enabled desc cfg; do
continue continue
fi fi
if is_running "$id"; then
warn "$id (ya corriendo, PID $(read_pid "$id"))"
continue
fi
start_agent "$id" "$cfg" start_agent "$id" "$cfg"
((started++)) || true ((started++)) || true
+4 -3
View File
@@ -71,9 +71,10 @@ func ServerMenuOptions() []MenuOption {
func AgentActionOptions(running bool) []MenuOption { func AgentActionOptions(running bool) []MenuOption {
if running { if running {
return []MenuOption{ return []MenuOption{
{Label: "Stop", Desc: "Detener el agente"}, {Label: "Start", Desc: "Iniciar otra instancia"},
{Label: "Restart", Desc: "Reiniciar"}, {Label: "Stop", Desc: "Detener todas las instancias"},
{Label: "Kill", Desc: "SIGKILL forzado"}, {Label: "Restart", Desc: "Reiniciar (stop all + start)"},
{Label: "Kill", Desc: "SIGKILL forzado a todas"},
{Label: "Logs", Desc: "Ver log del agente"}, {Label: "Logs", Desc: "Ver log del agente"},
} }
} }
+10 -6
View File
@@ -75,15 +75,15 @@ func viewAgentList(m Model) string {
status = "disabled" status = "disabled"
} else if a.Running { } else if a.Running {
icon = "●" 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", b.WriteString(fmt.Sprintf(" %s%s %-20s %-8s %s\n",
cursor, icon, a.ID, a.Version, status)) 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 != "" { if m.StatusMsg != "" {
@@ -104,7 +104,11 @@ func viewAgentActions(m Model) string {
a := m.Selected a := m.Selected
icon := "○ stopped" icon := "○ stopped"
if a.Running { 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)) b.WriteString(fmt.Sprintf("\n %s %s\n", a.ID, icon))
+20 -12
View File
@@ -29,8 +29,9 @@ type AgentInfo struct {
// AgentStatus combines agent metadata with runtime state. // AgentStatus combines agent metadata with runtime state.
type AgentStatus struct { type AgentStatus struct {
AgentInfo AgentInfo
Running bool Running bool
PID int PID int
Instances int
} }
// ProcessStats holds resource usage for a running process. // 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. // Status returns the runtime status for a single agent.
func (m *Manager) Status(info AgentInfo) AgentStatus { func (m *Manager) Status(info AgentInfo) AgentStatus {
pid := m.resolveRunningPID(info.ID) pids := m.findProcessPIDs(info.ID)
running := pid > 0 primary := 0
return AgentStatus{AgentInfo: info, Running: running, PID: pid} 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. // 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. // Start launches an agent process in the background.
// Multiple instances of the same agent are allowed.
func (m *Manager) Start(info AgentInfo) error { 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 { if err := os.MkdirAll(m.runDir, 0o755); err != nil {
return fmt.Errorf("create run dir: %w", err) return fmt.Errorf("create run dir: %w", err)
} }
@@ -233,7 +237,11 @@ func (m *Manager) Stats(id string) (ProcessStats, error) {
if pid == 0 { if pid == 0 {
return ProcessStats{}, fmt.Errorf("agent %q is not running", id) 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} s := ProcessStats{PID: pid}
// Uptime from /proc/<pid>/stat // Uptime from /proc/<pid>/stat
@@ -278,7 +286,7 @@ func (m *Manager) Stats(id string) (ProcessStats, error) {
s.LogBytes = info.Size() s.LogBytes = info.Size()
} }
return s, nil return s
} }
// LogTail returns the last N lines of an agent's log. // LogTail returns the last N lines of an agent's log.
+1 -1
View File
@@ -84,7 +84,7 @@ func (a *Adapter) loadAgents() tea.Cmd {
Enabled: s.Enabled, Enabled: s.Enabled,
Running: s.Running, Running: s.Running,
PID: s.PID, PID: s.PID,
Instances: a.mgr.InstanceCount(s.ID), Instances: s.Instances,
} }
if s.Running { if s.Running {