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>
This commit is contained in:
2026-03-06 09:05:42 +00:00
parent 6bef4283c6
commit 2667af52cc
14 changed files with 1001 additions and 7 deletions
+68
View File
@@ -22,6 +22,9 @@ import (
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.
@@ -58,6 +61,21 @@ func main() {
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
@@ -80,6 +98,25 @@ func main() {
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()
@@ -106,6 +143,37 @@ func main() {
}
}
// 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 {