# Plan: Multi-bot Orchestration — Middleware invisible ## Objetivo Cuando hay más de un bot en una sala, un **orquestador invisible** (sin identidad Matrix) coordina quién responde y cuándo. Opera como middleware en el proceso del launcher — los humanos solo ven a los bots especializados respondiendo. ## Estado: Completo --- ## Arquitectura: `agents/specials/` Los **special agents** son componentes de sistema sin identidad Matrix. Viven en `agents/specials//` y el launcher los instancia de forma diferente a los bots normales: sin token, sin listener propio, sin `user_id`. ``` agents/ assistant/ → bot normal (Matrix user, token, listener) specials/ → componentes de sistema, sin identidad Matrix orchestrator/ → middleware de coordinación multi-bot scheduler/ → (futuro) cron runner memory/ → (futuro) gestor de historial cross-bot ``` ### Diferencias vs bot normal | | Bot normal | Special agent | |---|---|---| | Matrix user | ✓ (@bot:server) | ✗ | | Token propio | ✓ | ✗ | | Listener Matrix | ✓ | ✗ | | LLM propio | opcional | ✓ (para decisiones) | | Instanciado por | launcher vía rulesRegistry | launcher vía specialsRegistry | | Visible en salas | ✓ | ✗ nunca | --- ## Config del orquestador ```yaml # agents/specials/orchestrator/config.yaml special: id: orchestrator type: orchestrator # clave para que el launcher sepa cómo instanciarlo enabled: true description: "Middleware de coordinación multi-bot. Sin identidad Matrix." llm: primary: provider: anthropic model: claude-sonnet-4-6 api_key_env: ANTHROPIC_API_KEY max_tokens: 512 # respuestas cortas: solo IDs de bots y scores temperature: 0.2 # determinista para routing orchestration: max_iterations: 3 # máximo de bots que responden por pregunta quality_threshold: 0.8 # score mínimo para cortar el pipeline (0.0–1.0) silent: true # no emite mensajes Matrix propios delegation_timeout: 30s # tiempo máximo esperando respuesta de un bot rooms: - room_id: "${MATRIX_ROOM_SHARED}" participants: # bots que participan en esta sala - id: assistant-bot ``` --- ## Flujo de eventos ``` Matrix event (room compartida) │ ▼ Launcher (event router) │ ├─► ¿hay orquestador activo para este room? ──No──► dispatch normal │ ▼ Sí Orchestrator.Route(event, participants) │ │ LLM Call 1: "¿Qué bot responde primero?" ▼ Bus.Dispatch(taskEvent → bot-A) │ ▼ bot-A.Handle(task) → SendMessage(room, respuesta) │ ▼ Orchestrator.Evaluate(pregunta, respuesta-A) │ LLM Call 2: score + continue? │ ├─► score >= threshold ──► fin del pipeline │ ▼ continuar Bus.Dispatch(taskEvent → bot-B) # bot-B ≠ bot-A (exclusión del último) (taskEvent incluye pregunta + respuesta-A como contexto) │ ▼ bot-B.Handle(task) → SendMessage(room, respuesta mejorada) │ ▼ Orchestrator.Evaluate(...) # repite hasta max_iterations o threshold ``` --- ## Protocolo interno: TaskEvent El orquestador no usa Matrix para comunicarse con los bots — usa el bus interno (`shell/bus`). Todos los bots corren en el mismo proceso del launcher. ```go // pkg/orchestration/task.go type TaskEvent struct { TargetBotID string TargetRoomID string OriginalQuestion string Iteration int PreviousResponses []BotResponse // vacío en primera iteración } type BotResponse struct { BotID string Text string } type QualityScore struct { Score float64 // 0.0–1.0 Continue bool Reason string } ``` --- ## LLM calls del orquestador ### Call 1: Routing inicial ``` System (prompts/routing.md): Eres un coordinador de agentes. Disponibles: - assistant-bot: Asistente general, preguntas, resúmenes, redacción Responde SOLO con el ID del bot más adecuado. User: [pregunta del humano] ``` ### Call 2: Evaluación de calidad ``` System (prompts/quality.md): Evalúa si la respuesta resuelve completamente la pregunta. Responde en JSON: {"score": 0.0-1.0, "continue": bool, "reason": "..."} User: Pregunta: [...] Respuesta de [bot-X]: [...] ``` ### Call 3: Routing de refinamiento (si continue=true) ``` System: La respuesta necesita mejora. Bots disponibles (excluido [último]): - [lista sin el último respondedor] Responde SOLO con el ID del bot. User: Pregunta: [...] | Respuesta actual: [...] ``` --- ## Comportamiento de los bots en sala orquestada Los bots **no saben** que están siendo orquestados. El launcher simplemente no les entrega el evento Matrix directamente. En su lugar reciben un `TaskEvent` via bus con el contexto correcto. Un bot en sala orquestada responde al `TaskEvent` igual que responde a un mensaje normal: genera texto y llama a `SendMessage(targetRoomID, text)`. La diferencia la gestiona el launcher, no el bot. Esto preserva el principio **pure core / impure shell** — los bots siguen siendo puros, el orquestador es shell. --- ## Launcher: registro de specials ```go // cmd/launcher/main.go — nuevo registro análogo a rulesRegistry var specialsRegistry = map[string]special.Factory{ "orchestrator": orchestration.New, // "scheduler": scheduler.New, // futuro // "memory": memory.New, // futuro } ``` El launcher escanea `agents/specials/*/config.yaml`, lee el campo `special.type`, busca en `specialsRegistry` y lo instancia. Los specials se arrancan antes que los bots normales (son infraestructura). --- ## Anti-bucle: garantías | Escenario | Mitigación | |-----------|-----------| | Bot responde sin ser delegado | El launcher no entrega eventos Matrix en salas orquestadas directamente | | Loop de refinamiento infinito | `max_iterations` hard limit | | Orquestador elige el mismo bot dos veces seguidas | Exclusión explícita del último respondedor en Call 3 | | Bot no responde (timeout) | `delegation_timeout` → orquestador corta o elige otro bot | | Sala con 1 solo bot | El orquestador detecta `len(participants)==1` y hace dispatch directo sin LLM | --- ## Archivos a crear ``` agents/specials/orchestrator/ config.yaml → config del orquestador (LLM + rooms) prompts/routing.md → system prompt para routing inicial prompts/quality.md → system prompt para evaluación de calidad prompts/refinement.md → system prompt para routing de refinamiento pkg/orchestration/ task.go → TaskEvent, BotResponse, QualityScore (tipos puros) protocol.go → serialización/deserialización de TaskEvent shell/orchestration/ orchestrator.go → Orchestrator struct, Route(), Evaluate() runner.go → loop de coordinación, gestión de timeouts internal/config/ schema.go → SpecialCfg, OrchestrationCfg (nuevas secciones) loader.go → LoadSpecial() análogo a Load() cmd/launcher/ main.go → specialsRegistry + arranque de specials specials.go → scanSpecials(), instanciación ``` ### Modificados ``` agents/runtime.go → aceptar TaskEvent además de eventos Matrix shell/bus/bus.go → soporte para TaskEvent routing ``` --- ## Fases de implementación ### Fase 1 — Scaffold + protocolo básico - Estructura `agents/specials/` y scanner en launcher - `pkg/orchestration/task.go` con tipos puros - Dispatch via bus sin LLM (keyword matching simple) - Un bot responde, sin refinamiento ### Fase 2 — LLM routing - Call 1 y Call 3 con LLM real - Exclusión del último respondedor - `max_iterations` funcional ### Fase 3 — Quality evaluation - Call 2 con score de calidad - `quality_threshold` para corte automático - Logs de orquestación en `run/orchestrator.log` ### Fase 4 — Observabilidad - Topic del room refleja estado del pipeline en curso - `"[2/3] bot respondió · evaluando..."` → topic actualizado en tiempo real