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>
8.1 KiB
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.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.
// 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
// 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.gocon 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_iterationsfuncional
Fase 3 — Quality evaluation
- Call 2 con score de calidad
quality_thresholdpara 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