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>
This commit is contained in:
2026-03-07 17:41:16 +00:00
parent 9deffc12c9
commit f561f686c4
21 changed files with 38 additions and 26 deletions
+275
View File
@@ -0,0 +1,275 @@
# 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.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.
```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.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
```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