Files
agents_and_robots/dev/issues/completed/003-bot-interaction.md
T
egutierrez f561f686c4 refactor: migrar tasks/ a dev/issues/ con estructura de desarrollo
Se mueve la documentación de issues/tasks de .claude/tasks/ a dev/issues/
para separar la planificación de desarrollo de la configuración de Claude.
Se añade dev/README.md como índice de la carpeta de desarrollo. Los issues
completados se mueven a dev/issues/completed/. Esto permite que dev/ sea
el punto central de documentación interna del proyecto.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:41:16 +00:00

8.1 KiB
Raw Blame History

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/<id>/ 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

# 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.01.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.

// 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.01.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

// 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