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
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
}
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"
+2 -14
View File
@@ -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
+4 -3
View File
@@ -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"},
}
}
+10 -6
View File
@@ -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))
+20 -12
View File
@@ -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/<pid>/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.
+1 -1
View File
@@ -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 {