Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,79 +1,278 @@
|
||||
# Plan: Interacción entre bots en una sala compartida
|
||||
# Plan: Multi-bot Orchestration — Middleware invisible
|
||||
|
||||
## Objetivo
|
||||
Que múltiples bots convivan en una sala Matrix, se "escuchen" entre sí y puedan
|
||||
colaborar en tareas — sin crear bucles infinitos.
|
||||
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: pendiente
|
||||
|
||||
---
|
||||
|
||||
## Problema central: evitar bucles
|
||||
Si bot-A responde a bot-B y bot-B responde a bot-A, se genera un bucle.
|
||||
## Arquitectura: `agents/specials/`
|
||||
|
||||
### Solución: reglas de anti-bucle en el motor de decisión
|
||||
1. Cada mensaje lleva metadatos `sender` y opcionalmente `m.relates_to`
|
||||
2. El `MatchFunc` de las reglas puede filtrar por `IsBot(sender)` y
|
||||
aplicar lógica de cooldown o turno
|
||||
3. El sistema de bus interno (`shell/bus/`) puede coordinar turnos
|
||||
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)
|
||||
devops/ → bot normal
|
||||
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 |
|
||||
|
||||
---
|
||||
|
||||
## Diseño
|
||||
## Config del orquestador
|
||||
|
||||
### Nuevo tipo de sala: `SharedRoom`
|
||||
Configurar en `config.yaml` bajo una sección `rooms`:
|
||||
```yaml
|
||||
rooms:
|
||||
- id: "!roomid:matrix.server.com"
|
||||
type: "shared" # sala compartida entre bots
|
||||
participants: # agentes que participan
|
||||
- assistant-bot
|
||||
- devops-bot
|
||||
turn_based: false # si true, los bots se turnan por ronda
|
||||
respond_to_bots: true # si false, solo responden a humanos
|
||||
# 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
|
||||
- id: devops-bot
|
||||
```
|
||||
|
||||
### Lógica en `agents/runtime.go`
|
||||
- Al recibir un evento en una sala compartida, verificar si `sender` es un bot conocido
|
||||
- Aplicar una regla de "bot puede responder a bot" solo si:
|
||||
- La sala tiene `respond_to_bots: true`
|
||||
- No hay respuesta pendiente del mismo bot en los últimos N segundos (cooldown)
|
||||
- No es una respuesta a un mensaje propio
|
||||
|
||||
### Coordinación con el bus (`shell/bus/`)
|
||||
- Publicar en el bus interno cuando un bot habla en una sala compartida
|
||||
- Los otros bots suscritos al bus pueden reaccionar sin pasar por Matrix
|
||||
- Posible uso: bot-A pide ayuda a bot-B por el bus, bot-B responde en Matrix
|
||||
|
||||
---
|
||||
|
||||
## Reglas puras (`pkg/decision/`)
|
||||
Nuevas funciones de match:
|
||||
## 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
|
||||
func SenderIsBot(knownBots []string) MatchFunc
|
||||
func NotRepliedRecently(cooldown time.Duration) MatchFunc // requiere estado externo
|
||||
func SenderIsHuman(knownBots []string) MatchFunc
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-bucle: cooldown simple
|
||||
En `agents/runtime.go` mantener `map[roomID]time.Time` con el último mensaje enviado.
|
||||
Si `now - lastSent < cooldown`, no responder en esa sala.
|
||||
Cooldown configurable: `rooms[].response_cooldown_seconds`.
|
||||
## 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
|
||||
- devops-bot: SSH, deployments, infraestructura
|
||||
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: [...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Archivos a crear/modificar
|
||||
- `internal/config/schema.go` — añadir `RoomCfg` con campos shared room
|
||||
- `pkg/decision/matchers.go` — SenderIsBot, SenderIsHuman
|
||||
- `agents/runtime.go` — lógica de salas compartidas y cooldown
|
||||
- `shell/bus/bus.go` — publicar eventos cross-bot
|
||||
- `agents/<id>/agent.go` — reglas específicas para salas compartidas
|
||||
## Comportamiento de los bots en sala orquestada
|
||||
|
||||
## Notas
|
||||
- Fase 1: solo cooldown simple — un bot no responde más de 1 vez por minuto en
|
||||
una sala compartida
|
||||
- Fase 2: turno basado en bus interno
|
||||
- Fase 3: colaboración estructurada (bot-A delega tarea a bot-B y espera resultado)
|
||||
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] devops-bot respondió · evaluando..."` → topic actualizado en tiempo real
|
||||
|
||||
Reference in New Issue
Block a user