Files
agents_and_robots/cmd/launcher/main.go
T
egutierrez 2667af52cc feat: implement multi-bot orchestration system with LLM routing
Implementa el sistema de orquestación para salas Matrix con múltiples bots.
El orquestador es un "special agent" sin identidad Matrix que coordina qué bot
responde y cuándo, usando LLM (Claude) para routing y evaluación de calidad.

Cambios principales:
- pkg/orchestration/task.go: tipos puros (TaskEvent, BotResponse, QualityScore, RoutingDecision)
- shell/orchestration/: runtime del orquestador (orchestrator.go, router.go, evaluator.go)
- agents/specials/orchestrator/: config + prompts (routing, quality, refinement)
- internal/config/: SpecialConfig, OrchestrationCfg, LoadSpecial()
- shell/bus/bus.go: protocolo request-reply (SendAndWait, Reply) para delegación
- shell/matrix/listener.go: InterceptFunc para interceptar eventos en salas orquestadas
- agents/runtime.go: SetBus, listenBus, handleTaskEvent para recibir tareas del orquestador
- cmd/launcher/main.go: creación de bus compartido, arranque del orquestador antes de bots

Incluye deduplicación para evitar que múltiples listeners en la misma sala
disparen el orquestador más de una vez por mensaje.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:05:42 +00:00

200 lines
5.3 KiB
Go

// Command launcher starts one or more agents from their config files.
//
// Usage:
//
// go run ./cmd/launcher # auto-discovers agents/*/config.yaml
// go run ./cmd/launcher -c agents/assistant/config.yaml
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"github.com/spf13/cobra"
"github.com/enmanuel/agents/agents"
assistantagent "github.com/enmanuel/agents/agents/assistant-bot"
asistente2agent "github.com/enmanuel/agents/agents/asistente-2"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/decision"
"github.com/enmanuel/agents/pkg/orchestration"
"github.com/enmanuel/agents/shell/bus"
orchshell "github.com/enmanuel/agents/shell/orchestration"
)
// rulesRegistry maps agent IDs to their rule factories.
// Add a new entry here when you create a new agent package.
var rulesRegistry = map[string]func() []decision.Rule{
"assistant-bot": assistantagent.Rules,
"asistente-2": asistente2agent.Rules,
}
func main() {
var (
configPaths []string
logLevel string
)
root := &cobra.Command{
Use: "launcher",
Short: "Start Matrix agents from config files",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if len(configPaths) == 0 {
matches, _ := filepath.Glob("agents/*/config.yaml")
configPaths = matches
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
logger := newLogger(logLevel)
if len(configPaths) == 0 {
logger.Warn("no agent configs found — nothing to start")
return nil
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// ── Shared bus for inter-agent communication ──
agentBus := bus.New()
// ── Start special agents (orchestrator, etc.) BEFORE normal bots ──
orch, err := startOrchestrator(agentBus, logger)
if err != nil {
// Non-fatal: orchestration is optional
logger.Warn("orchestrator not started", "err", err)
} else {
logger.Info("orchestrator ready",
"managed_rooms", len(orch.cfg.Orchestration.Rooms),
)
}
// ── Start normal agents ──
var wg sync.WaitGroup
for _, path := range configPaths {
path := path
cfg, err := config.Load(path)
if err != nil {
logger.Error("failed to load config", "path", path, "err", err)
continue
}
if !cfg.Agent.Enabled {
logger.Info("agent disabled, skipping", "id", cfg.Agent.ID)
continue
}
rules := rulesFor(cfg.Agent.ID, logger)
agentLogger := logger.With("agent", cfg.Agent.ID)
a, err := agents.New(cfg, rules, agentLogger)
if err != nil {
logger.Error("failed to create agent", "id", cfg.Agent.ID, "err", err)
continue
}
// Connect agent to bus for orchestration
a.SetBus(agentBus)
// If orchestrator is active, set interceptor so bots don't
// handle events directly in orchestrated rooms.
// The first bot's listener to receive the event will trigger orchestration.
if orch != nil {
a.SetInterceptor(orch.orchestrator.Intercept)
}
// Register this agent as a participant in the orchestrator
if orch != nil {
orch.orchestrator.RegisterParticipant(orchestration.ParticipantInfo{
ID: cfg.Agent.ID,
Description: cfg.Agent.Description,
Capabilities: cfg.Agent.Tags,
})
}
wg.Add(1)
go func() {
defer wg.Done()
agentLogger.Info("agent running")
if err := a.Run(ctx); err != nil {
agentLogger.Error("agent stopped with error", "err", err)
}
}()
}
wg.Wait()
logger.Info("all agents stopped")
return nil
},
}
root.Flags().StringSliceVarP(&configPaths, "config", "c", nil,
"Agent config file(s). If omitted, discovers all agents/*/config.yaml")
root.Flags().StringVar(&logLevel, "log-level", "info",
"Log level: debug | info | warn | error")
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
// orchHandle wraps a running orchestrator with its config for the launcher.
type orchHandle struct {
orchestrator *orchshell.Orchestrator
cfg *config.SpecialConfig
}
// startOrchestrator scans agents/specials/orchestrator/config.yaml and
// initializes the orchestrator if found and enabled.
func startOrchestrator(agentBus *bus.Bus, logger *slog.Logger) (*orchHandle, error) {
cfgPath := filepath.Join("agents", "specials", "orchestrator", "config.yaml")
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
return nil, err
}
cfg, err := config.LoadSpecial(cfgPath)
if err != nil {
return nil, err
}
if !cfg.Special.Enabled {
return nil, nil
}
orchLogger := logger.With("component", "orchestrator")
orch, err := orchshell.New(cfg, agentBus, orchLogger)
if err != nil {
return nil, err
}
return &orchHandle{orchestrator: orch, cfg: cfg}, nil
}
func rulesFor(agentID string, logger *slog.Logger) []decision.Rule {
factory, ok := rulesRegistry[agentID]
if !ok {
logger.Warn("no rules registered for agent, using empty ruleset", "id", agentID)
return nil
}
return factory()
}
func newLogger(level string) *slog.Logger {
var lvl slog.Level
switch level {
case "debug":
lvl = slog.LevelDebug
case "warn":
lvl = slog.LevelWarn
case "error":
lvl = slog.LevelError
default:
lvl = slog.LevelInfo
}
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
}