feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
// Command agentctl manages Matrix agents: list, start, stop, remove.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// agentctl list # all agents with their status
|
||||
// agentctl start # start all enabled agents
|
||||
// agentctl start assistant-bot # start a specific agent
|
||||
// agentctl stop # stop all running agents
|
||||
// agentctl stop assistant-bot # stop a specific agent
|
||||
// agentctl remove assistant-bot # disable agent (keeps data)
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/enmanuel/agents/shell/process"
|
||||
)
|
||||
|
||||
const (
|
||||
runDir = "run"
|
||||
agentsGlob = "agents/*/config.yaml"
|
||||
)
|
||||
|
||||
// ── entry point ───────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
var binPath string
|
||||
|
||||
mgr := process.NewManager(runDir, agentsGlob, "")
|
||||
|
||||
root := &cobra.Command{
|
||||
Use: "agentctl",
|
||||
Short: "Manage Matrix agents",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return os.MkdirAll(runDir, 0o755)
|
||||
},
|
||||
}
|
||||
|
||||
root.PersistentFlags().StringVar(&binPath, "bin", "",
|
||||
"Launcher binary path. Defaults to ./bin/launcher, falls back to 'go run ./cmd/launcher'")
|
||||
|
||||
root.AddCommand(
|
||||
listCmd(mgr),
|
||||
startCmd(mgr, &binPath),
|
||||
stopCmd(mgr),
|
||||
reloadCmd(mgr),
|
||||
removeCmd(mgr),
|
||||
)
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ── list ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func listCmd(mgr *process.Manager) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all agents and their current status",
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
statuses, err := mgr.StatusAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(statuses) == 0 {
|
||||
fmt.Println("No agents found under agents/*/config.yaml")
|
||||
return nil
|
||||
}
|
||||
|
||||
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 %-14s %-8s %-4d %s\n",
|
||||
s.ID,
|
||||
statusLabel(s),
|
||||
s.Version,
|
||||
s.Instances,
|
||||
truncate(s.Desc, 32),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── start ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func startCmd(mgr *process.Manager, binPath *string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "start [agent-id...]",
|
||||
Short: "Start one or all enabled agents",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
statuses, err := mgr.StatusAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targets := filterTargets(statuses, args)
|
||||
if len(targets) == 0 {
|
||||
return fmt.Errorf("no matching agents found")
|
||||
}
|
||||
|
||||
started := 0
|
||||
for _, s := range targets {
|
||||
if !s.Enabled {
|
||||
fmt.Printf("skip %-20s (disabled in config)\n", s.ID)
|
||||
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 (instances: %d) log → %s\n",
|
||||
s.ID, mgr.ReadPID(s.ID), mgr.InstanceCount(s.ID), mgr.LogPath(s.ID))
|
||||
started++
|
||||
}
|
||||
|
||||
if started == 0 {
|
||||
fmt.Println("Nothing started.")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── stop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func stopCmd(mgr *process.Manager) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "stop [agent-id...]",
|
||||
Short: "Stop one or all running agents",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
statuses, err := mgr.StatusAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targets := filterTargets(statuses, args)
|
||||
if len(targets) == 0 {
|
||||
return fmt.Errorf("no matching agents found")
|
||||
}
|
||||
|
||||
stopped := 0
|
||||
for _, s := range targets {
|
||||
if !s.Running {
|
||||
fmt.Printf("skip %-20s (not running)\n", s.ID)
|
||||
continue
|
||||
}
|
||||
pid := s.PID
|
||||
if err := mgr.Stop(s.ID); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fail %-20s %v\n", s.ID, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("stop %-20s stopped PID %d\n", s.ID, pid)
|
||||
stopped++
|
||||
}
|
||||
|
||||
if stopped == 0 {
|
||||
fmt.Println("Nothing stopped.")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── reload ────────────────────────────────────────────────────────────────
|
||||
|
||||
func reloadCmd(mgr *process.Manager) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "reload [agent-id]",
|
||||
Short: "Hot-reload an agent (or all agents) without stopping the launcher",
|
||||
Long: `Sends SIGHUP to the running launcher, which triggers a hot-reload.
|
||||
If an agent-id is given, only that agent is reloaded.
|
||||
If no agent-id is given, all agents are reloaded.
|
||||
|
||||
The launcher must be running. Use 'agentctl start' first if needed.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
pid := mgr.UnifiedPID()
|
||||
if pid <= 0 {
|
||||
return fmt.Errorf("launcher is not running")
|
||||
}
|
||||
|
||||
target := ""
|
||||
if len(args) == 1 {
|
||||
target = args[0]
|
||||
}
|
||||
|
||||
if target != "" {
|
||||
if err := os.WriteFile("run/reload.txt", []byte(target), 0o644); err != nil {
|
||||
return fmt.Errorf("write reload target: %w", err)
|
||||
}
|
||||
fmt.Printf("reload %-20s sending SIGHUP to PID %d\n", target, pid)
|
||||
} else {
|
||||
// Remove any stale reload.txt so SIGHUP reloads all agents.
|
||||
_ = os.Remove("run/reload.txt")
|
||||
fmt.Printf("reload %-20s sending SIGHUP to PID %d\n", "(all)", pid)
|
||||
}
|
||||
|
||||
if err := syscall.Kill(pid, syscall.SIGHUP); err != nil {
|
||||
return fmt.Errorf("kill -HUP %d: %w", pid, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── remove ────────────────────────────────────────────────────────────────
|
||||
|
||||
func removeCmd(mgr *process.Manager) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "remove <agent-id>",
|
||||
Short: "Disable an agent (sets enabled: false). Does not delete data.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
id := args[0]
|
||||
|
||||
statuses, err := mgr.StatusAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var target *process.AgentStatus
|
||||
for i := range statuses {
|
||||
if statuses[i].ID == id {
|
||||
target = &statuses[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if target == nil {
|
||||
return fmt.Errorf("agent %q not found", id)
|
||||
}
|
||||
|
||||
if target.Running {
|
||||
if err := mgr.Stop(id); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warn stop failed: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("stop %-20s stopped PID %d\n", id, target.PID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setEnabled(target.ConfigPath, false); err != nil {
|
||||
return fmt.Errorf("update config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("ok %-20s marked as disabled in %s\n", id, target.ConfigPath)
|
||||
fmt.Printf(" Data preserved at agents/%s/data/\n", id)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
func filterTargets(statuses []process.AgentStatus, ids []string) []process.AgentStatus {
|
||||
if len(ids) == 0 {
|
||||
return statuses
|
||||
}
|
||||
set := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
set[id] = true
|
||||
}
|
||||
var out []process.AgentStatus
|
||||
for _, s := range statuses {
|
||||
if set[s.ID] {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func statusLabel(s process.AgentStatus) string {
|
||||
switch {
|
||||
case !s.Enabled:
|
||||
return "disabled"
|
||||
case s.Running:
|
||||
if s.Instances > 1 {
|
||||
return fmt.Sprintf("● running(%d)", s.Instances)
|
||||
}
|
||||
return "● running"
|
||||
default:
|
||||
return "○ stopped"
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max-1] + "…"
|
||||
}
|
||||
|
||||
// setEnabled flips `enabled: true/false` in the agent section of the YAML.
|
||||
func setEnabled(configPath string, enabled bool) error {
|
||||
raw, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
current := "enabled: true"
|
||||
replacement := "enabled: false"
|
||||
if enabled {
|
||||
current = "enabled: false"
|
||||
replacement = "enabled: true"
|
||||
}
|
||||
|
||||
updated := strings.Replace(string(raw), current, replacement, 1)
|
||||
if updated == string(raw) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, []byte(updated), 0o644)
|
||||
}
|
||||
Reference in New Issue
Block a user