2756557498
Se estandariza la numeración de todos los issues de 3 dígitos a 4 dígitos (e.g. 005 → 0005, 010 → 0010) para mantener consistencia con la convención definida en create_issue.md. Se actualiza el README con los nuevos nombres y links. No hay cambios de contenido en los issues, solo renombrado. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
276 lines
8.1 KiB
Markdown
276 lines
8.1 KiB
Markdown
# 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
|
||
|
||
```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
|