2667af52cc
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>
200 lines
5.3 KiB
Go
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}))
|
|
}
|