docs: crear issues 0036-0041 — nuevas features del sistema

Issues planificados:
- 0036: Claude Code streaming de progreso en Matrix
- 0037: Agente que crea otros agentes/bots via Matrix
- 0038: Webapps y dashboards embebidos en Element via widgets
- 0039: Recordatorios dinámicos y crons que invocan agentes
- 0040: Soporte para mensajes de voz (audio → STT)
- 0041: Videollamadas con agentes via LiveKit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 21:19:09 +00:00
parent 9ec0c16038
commit 52d5632d89
22 changed files with 1557 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
# 0036 — Agente Claude Code completo con streaming de progreso
**Estado:** pendiente
## Objetivo
Transformar el provider `claude-code` para soportar streaming en tiempo real, permitiendo que los agentes que lo usan muestren en Matrix el progreso de uso de herramientas (e.g. "🔧 Ejecutando Bash: ls..." → "📝 Editando file.go..." → respuesta final). El usuario debe ver al agente trabajar como una sesion completa de Claude Code, no solo esperar en silencio hasta que termine.
## Contexto
- El provider `claude-code` vive en `shell/llm/claudecode.go` y ejecuta `claude -p --output-format json` como subproceso.
- Actualmente usa `bytes.Buffer` para capturar stdout completo, espera a que el proceso termine, y luego parsea el JSON final. Durante todo este tiempo el usuario solo ve el typing indicator.
- Claude CLI soporta `--output-format stream-json` que emite lineas JSON individuales conforme trabaja: eventos de `tool_use`, `tool_result`, `text` parcial, y `result` final.
- El cliente Matrix (`shell/matrix/client.go`) tiene `SendMarkdown`, `SendReplyMarkdown`, `SendThreadMarkdown` pero **no tiene** `EditMessage` (m.replace). Las relaciones `m.relates_to` ya se manejan para threads.
- El runner de effects (`shell/effects/runner.go`) ejecuta `[]decision.Action` pero no tiene concepto de mensajes progresivos.
- `ClaudeCodeCfg` en `internal/config/schema.go` ya tiene campos para binary, timeout, tools, working_dir, permission_mode, model, etc. No tiene campos de streaming.
## Arquitectura
### Patron pure core / impure shell
- `pkg/llm/types.go` — tipos puros para streaming events (solo datos, sin I/O)
- `shell/llm/claudecode.go` — impuro: pipe de stdout, parsing de stream JSON, invocacion de callbacks
- `shell/matrix/client.go` — impuro: nuevo metodo `EditMessage` usando m.replace
- `devagents/handler.go` — composicion: conectar progress reporter cuando el provider es claude-code con streaming
- `internal/config/schema.go` — datos puros: nuevos campos en `ClaudeCodeCfg`
### Fase 1 — Streaming del subproceso
```
claude -p --output-format stream-json
├── {"type":"tool_use", "tool":"Bash", "input":"ls -la"} ← tool empezando
├── {"type":"tool_result", "tool":"Bash", "output":"..."} ← tool terminó
├── {"type":"text", "content":"Analizando...", "partial":true} ← texto parcial
└── {"type":"result", "result":"...", "usage":{...}} ← resultado final
```
Se parsea linea a linea y se emite un `StreamEvent` por cada linea. El caller recibe eventos via callback `StreamFunc`.
### Fase 2 — Mensajes progresivos en Matrix
```
StreamEvent(tool_use, "Bash", "ls -la")
→ sender.SendMarkdown("⏳ Procesando...") ← mensaje inicial
→ sender.EditMessage(eventID, "🔧 Bash: ls -la") ← editar con progreso
StreamEvent(tool_use, "Read", "main.go")
→ sender.EditMessage(eventID, "📖 Read: main.go") ← editar de nuevo
StreamEvent(result, content)
→ sender.EditMessage(eventID, content) ← reemplazar con resultado final
```
Un solo mensaje que se edita progresivamente. Evita spam de multiples mensajes.
### Fase 3 — Config y UX
Nuevos campos en `ClaudeCodeCfg`:
```yaml
claude_code:
streaming: true # usar stream-json en vez de json
show_tool_progress: true # mostrar progreso de tools en Matrix
```
## Tareas
### Fase 1 — Streaming del subproceso
- [ ] **1.1** Añadir tipos puros de streaming a `pkg/llm/types.go`:
- `StreamEventKind` (string type): `StreamToolUse`, `StreamToolResult`, `StreamText`, `StreamResult`, `StreamError`
- `StreamEvent` struct: `Kind StreamEventKind`, `ToolName string`, `ToolInput string`, `Content string`, `IsPartial bool`, `Error error`
- `StreamFunc` callback type: `func(event StreamEvent)`
- Estos tipos son datos puros, sin side effects — coherente con el resto de `pkg/llm/`
- [ ] **1.2** Refactorizar `NewClaudeCodeComplete` para soportar modo streaming:
- Nueva funcion `NewClaudeCodeStream(cfg, log) (CompleteFunc, StreamFunc setter)` o añadir `StreamFunc` al closure
- Cuando `cfg.Streaming == true`: usar `cmd.StdoutPipe()` en vez de `bytes.Buffer`
- Leer stdout linea a linea con `bufio.Scanner`
- Parsear cada linea JSON e invocar `StreamFunc(event)` si no es nil
- Acumular el resultado final para retornarlo como `CompletionResponse` normal (compatibilidad)
- Si `cfg.Streaming == false` o `StreamFunc == nil`: mantener comportamiento actual (buffered)
- [ ] **1.3** Implementar parser de eventos stream-json:
- Investigar formato exacto de `claude --output-format stream-json` (ejecutar `claude --help` para confirmar)
- Funcion pura `parseStreamLine(line []byte) (StreamEvent, error)` en `shell/llm/claudecode.go`
- Mapear los tipos de evento del CLI a `StreamEventKind`
- Manejar lineas vacias y JSON malformado sin crash
- [ ] **1.4** Preservar cleanup de process group:
- `cmd.SysProcAttr`, `cmd.Cancel` y el kill post-Run deben funcionar identicamente en modo streaming
- El pipe de stdout debe cerrarse correctamente cuando el contexto se cancela
- [ ] **1.5** Tests unitarios para parsing de eventos:
- Test `parseStreamLine` con samples de cada tipo de evento
- Test de streaming completo con mock de stdout (io.Pipe)
- Test de fallback a modo buffered cuando streaming == false
- Test de cancelacion via contexto durante streaming
### Fase 2 — Mensajes progresivos en Matrix
- [ ] **2.1** Añadir `EditMessage` a `shell/matrix/client.go`:
- Metodo `EditMessage(ctx, roomID, eventID, markdown string) error`
- Usar `m.relates_to` con `rel_type: "m.replace"` y `event_id: <original>`
- Incluir `m.new_content` con el nuevo body (formatted_body para markdown)
- Retornar error si el eventID original no existe o la edicion falla
- [ ] **2.2** Añadir `SendMarkdownWithID` o modificar `SendMarkdown` para retornar `eventID`:
- El progress reporter necesita el eventID del mensaje inicial para poder editarlo
- Evaluar: nuevo metodo que retorna `(id.EventID, error)` vs cambiar firma existente (breaking change)
- Recomendacion: nuevo metodo `SendMarkdownGetID(ctx, roomID, markdown) (string, error)` para no romper callers existentes
- [ ] **2.3** Implementar progress reporter en `shell/effects/progress.go` (NEW):
- `ProgressReporter` struct con: sender (interface), roomID, eventID del mensaje actual
- Metodo `HandleEvent(event StreamEvent)` que:
- En primer evento: envia mensaje inicial "⏳ Procesando..." y guarda eventID
- En `StreamToolUse`: edita a "🔧 {ToolName}: {ToolInput truncado a 80 chars}"
- En `StreamToolResult`: edita a "✅ {ToolName} completado"
- En `StreamText` parcial: ignora (demasiadas ediciones)
- En `StreamResult`: edita con el contenido final completo
- En `StreamError`: edita con "❌ Error: {mensaje}"
- Rate limiter interno: maximo 1 edit por segundo para evitar rate limits de Matrix
- Formateo con emojis configurable via config
- [ ] **2.4** Conectar progress reporter en `devagents/handler.go`:
- En el flujo LLM del handler, si el provider es `claude-code` y `cfg.ClaudeCode.Streaming`:
- Crear `ProgressReporter` con el sender y roomID
- Pasar `reporter.HandleEvent` como `StreamFunc` al provider
- El resultado final llega como `CompletionResponse` normal (el handler no cambia su flujo)
- Si streaming deshabilitado: flujo actual sin cambios
- [ ] **2.5** Tests del progress reporter:
- Test con mock sender que registra llamadas a SendMarkdown y EditMessage
- Verificar secuencia: Send → Edit → Edit → Edit(final)
- Verificar rate limiting: multiples eventos rapidos → solo 1 edit/segundo
- Verificar truncado de ToolInput largo
### Fase 3 — Config y polish
- [ ] **3.1** Añadir campos a `ClaudeCodeCfg` en `internal/config/schema.go`:
```go
Streaming bool `yaml:"streaming"` // use stream-json output format (default false)
ShowToolProgress bool `yaml:"show_tool_progress"` // show tool progress via message edits (default false)
```
- [ ] **3.2** Actualizar `buildClaudeArgs` para usar `--output-format stream-json` cuando `cfg.Streaming == true`
- [ ] **3.3** Actualizar templates de config (`agents/_template/config.yaml`) con opciones de streaming (comentadas):
```yaml
# claude_code:
# streaming: true # streaming en tiempo real (default: false)
# show_tool_progress: true # mostrar progreso de tools en Matrix
```
- [ ] **3.4** Documentar en el system prompt template (`.claude/templates/security-prompt.md` o README) que el agente puede mostrar progreso de trabajo
### Fase 4 — Tests de integracion y cleanup
- [ ] **4.1** Test de integracion: simular un flujo completo stream → progress reporter → mock sender
- [ ] **4.2** Verificar que agentes con `streaming: false` no se ven afectados (regression)
- [ ] **4.3** Actualizar `CLAUDE.md` si se añaden nuevas secciones de arquitectura relevantes
## Ejemplo de uso
### Config del agente
```yaml
# agents/asistente-2/config.yaml
llm:
primary:
provider: claude-code
claude_code:
working_dir: "/tmp/claude-agents/asistente-2"
permission_mode: "bypassPermissions"
streaming: true
show_tool_progress: true
```
### Flujo en Matrix
```
Usuario: Analiza la estructura de este proyecto y dame un resumen
Bot (mensaje inicial):
⏳ Procesando...
Bot (edit 1, ~2s despues):
🔧 Bash: find . -name '*.go' | head -20
Bot (edit 2, ~4s despues):
📖 Read: cmd/launcher/main.go
Bot (edit 3, ~6s despues):
🔧 Bash: wc -l pkg/**/*.go
Bot (edit final, ~15s despues):
## Estructura del proyecto
El proyecto es un monorepo Go con 45 archivos .go organizados en:
- `pkg/` — core puro con tipos y reglas de decision
- `shell/` — I/O impuro (Matrix, LLM, SSH)
- `agents/` — definiciones de agentes
[... respuesta completa ...]
```
### Sin streaming (comportamiento actual)
```
Usuario: Analiza la estructura de este proyecto y dame un resumen
[typing indicator durante 15 segundos]
Bot:
## Estructura del proyecto
[... respuesta completa ...]
```
## Decisiones de diseño
1. **Edit en vez de multiples mensajes**: usar `m.replace` para editar un solo mensaje progresivamente evita spam en el chat. Si el homeserver no soporta ediciones, el fallback es enviar solo el resultado final (sin progreso intermedio).
2. **StreamFunc como callback, no como channel**: un callback `func(StreamEvent)` es mas simple que un channel y no requiere goroutine de consumo. El caller decide que hacer con cada evento sincrónicamente.
3. **Rate limit de 1 edit/segundo**: Matrix homeservers tipicamente tienen rate limits de 5-10 requests/segundo. Con 1 edit/segundo dejamos margen para otros mensajes del agente y evitamos 429 Too Many Requests.
4. **Tipos puros en `pkg/llm/`**: `StreamEvent` y `StreamFunc` son tipos de datos sin I/O. Estan en el package puro porque describen el contrato entre el provider (impuro) y el consumer (impuro). El tipo en si no tiene side effects.
5. **Backward compatible**: `streaming: false` (default) mantiene el comportamiento actual exacto. La refactorizacion de `claudecode.go` no cambia la firma de `CompleteFunc` — el streaming es un side channel via `StreamFunc`.
6. **`SendMarkdownGetID` nuevo en vez de cambiar firma**: añadir un metodo nuevo que retorne el eventID evita romper todos los callers existentes de `SendMarkdown`. El progress reporter usa el metodo nuevo; el resto del codigo no cambia.
## Prerequisitos
- Verificar que `claude --output-format stream-json` existe y documentar el formato exacto de sus eventos. Si el CLI no soporta `stream-json`, investigar alternativas:
- `--output-format json` con lectura line-buffered del proceso (puede no emitir JSON parcial)
- Parsear stderr del proceso para eventos de progreso
- Usar la API directa de Anthropic con streaming en vez del CLI
- El campo `ClaudeCodeCfg` ya existe en el schema — solo se añaden campos nuevos.
- `m.relates_to` con `m.replace` ya se parsea para threads en el listener — la logica de edicion es el inverso (enviar en vez de recibir).
## Riesgos
| Riesgo | Mitigacion |
|--------|------------|
| Formato de `stream-json` cambia entre versiones del CLI | Parseo defensivo: lineas no reconocidas se ignoran silenciosamente, el resultado final siempre se captura |
| Rate limits de Matrix en message edits | Rate limiter de 1 edit/segundo configurable; si falla un edit, se loguea warning y se continua |
| Tareas largas (>5min) timeout del subproceso | Ya manejado via `context.WithTimeout` y `cfg.Timeout`. El progress reporter muestra el ultimo estado antes del timeout |
| Homeserver no soporta m.replace | Detectar error 400 en primer edit; si falla, desactivar ediciones para esa sesion y enviar solo resultado final |
| Stdout pipe se bloquea si no se lee | `bufio.Scanner` en goroutine consume stdout continuamente; el pipe no se bloquea mientras el scanner este activo |
| `claude` CLI no esta instalado o no soporta stream-json | Fallback a modo buffered con warning en logs. La feature es opt-in (`streaming: false` por defecto) |
+239
View File
@@ -0,0 +1,239 @@
# 0037 — Agente que crea otros agentes y bots via Matrix
**Estado:** pendiente
## Objetivo
Crear un agente especializado ("creator-bot") que reciba peticiones en lenguaje natural via Matrix para crear nuevos agentes o robots. El usuario describe lo que necesita (ej: "crea un bot que monitoree servidores con SSH") y creator-bot ejecuta todo el pipeline automaticamente: scaffold, build, registro Matrix, configuracion, personalización del system prompt y reinicio del launcher.
## Contexto
- El proyecto ya tiene `dev-scripts/agent/create-full.sh` que ejecuta el pipeline completo de scaffold + build + register + verify E2EE. Funciona bien desde la terminal.
- Existen dos skills de Claude Code (`/create-agent` y `/create-bot`) que automatizan la creacion via el CLI de Claude, pero solo funcionan dentro de una sesion de Claude Code.
- El provider `claude-code` ya esta implementado (`shell/llm/claude_code.go`) y soporta `allowed_tools`, `add_dirs`, `permission_mode` y `working_dir`.
- No hay forma de crear agentes desde Matrix sin acceso SSH al servidor. Este issue cierra esa brecha: un usuario admin envia un mensaje y el agente lo resuelve end-to-end.
- La infraestructura de seguridad (grupos de usuarios, permisos por agente, ACLs en `security/`) permite restringir el acceso a este agente privilegiado.
## Arquitectura
### Provider y acceso
El creator-bot usa `provider: claude-code` con `working_dir` apuntando a la raiz del proyecto. Esto es una excepcion deliberada a la regla de sandbox (`working_dir` fuera del repo) porque el agente necesita acceso de lectura y escritura al arbol completo para crear archivos de agentes, editar el launcher y ejecutar scripts.
```
Usuario envia "crea un robot que responda !saludo"
→ Matrix event → listener
→ Rules: DM/mention → ActionKindLLM
→ claude-code provider recibe el mensaje + system prompt
→ claude -p ejecuta:
1. Analiza la peticion (tipo, nombre, descripcion, tools)
2. ./dev-scripts/agent/create-full.sh <id> "Name"
3. Personaliza config.yaml, agent.go, prompts/system.md
4. go build -tags goolm ./...
5. ./dev-scripts/server/restart.sh
6. Verifica logs del nuevo agente
→ Responde al usuario con resultado
```
**Pure core / impure shell:**
- `agents/creator-bot/agent.go` — PURO: reglas simples (DM/mention → LLM), sin side effects
- Toda la logica de creacion ocurre dentro del subprocess `claude -p` (impuro por naturaleza)
- No se anade nada a `pkg/` — el creator-bot es composicion pura de infraestructura existente
### Archivos afectados
```
agents/creator-bot/ NEW — directorio del agente
agents/creator-bot/agent.go NEW — reglas puras (DM/mention → LLM)
agents/creator-bot/config.yaml NEW — provider claude-code, working_dir al repo, ACL admin-only
agents/creator-bot/prompts/system.md NEW — guia completa de creacion de agentes
cmd/launcher/main.go MOD — blank import de creator-bot
security/permissions.yaml MOD — policy restrictiva para creator-bot (solo admins)
security/agent-groups.yaml MOD — grupo para agentes privilegiados
```
## Tareas
### Fase 1 — Scaffold y configuracion basica
- [ ] **1.1** Ejecutar `./dev-scripts/agent/create-full.sh creator-bot "Creator Bot"` para scaffold completo (registro Matrix, E2EE, env vars)
- [ ] **1.2** Configurar `agents/creator-bot/config.yaml`:
- `agent.type: agent`
- `agent.description: "Agente que crea otros agentes y robots via Matrix"`
- `llm.primary.provider: claude-code`
- `llm.primary.claude_code.working_dir: "/home/ubuntu/CodeProyects/agents_and_robots"` (raiz del proyecto)
- `llm.primary.claude_code.permission_mode: bypassPermissions`
- `llm.primary.claude_code.allowed_tools: [Bash, Read, Edit, Write, Glob, Grep]`
- `llm.primary.claude_code.add_dirs` con las rutas de referencia (ver Fase 2)
- [ ] **1.3** Escribir `agents/creator-bot/agent.go` con reglas simples:
- DM o mencion → `ActionKindLLM`
- Package name: `creator` (strip hyphens + strip `_bot`)
- `agents.Register("creator-bot", Rules)` en `init()`
- [ ] **1.4** Verificar blank import en `cmd/launcher/main.go`:
```go
_ "github.com/enmanuel/agents/agents/creator-bot"
```
- [ ] **1.5** Compilar y verificar: `go build -tags goolm ./...`
### Fase 2 — System prompt y knowledge
- [ ] **2.1** Escribir `agents/creator-bot/prompts/system.md` completo. Debe incluir:
- **Identidad**: "Eres Creator Bot, un agente especializado en crear otros agentes y robots para Matrix"
- **Flujo de trabajo completo**:
1. Entender la peticion del usuario (tipo agent/robot, nombre, descripcion, tools necesarias)
2. Elegir tipo (Agent vs Robot) segun la decision tree de `create_agent.md`
3. Ejecutar `./dev-scripts/agent/create-full.sh <id> "Display Name"`
4. Personalizar `config.yaml` (provider, tools, personality, etc.)
5. Escribir `prompts/system.md` del nuevo agente con instrucciones de seguridad
6. Personalizar `agent.go` si se necesitan reglas especificas
7. Compilar: `go build -tags goolm ./...`
8. Reiniciar launcher: `./dev-scripts/server/restart.sh`
9. Verificar que el nuevo agente arranca (revisar logs)
10. Confirmar al usuario con resumen del agente creado
- **Decision tree Agent vs Robot**: reproducir la tabla de `create_agent.md`
- **Referencia de config YAML**: secciones clave del schema (agent, llm, personality, tools, matrix, security)
- **Guia de system prompts**: como escribir buenos prompts para agentes, con ejemplo
- **Seccion de seguridad anti-injection** (obligatoria, copiar de template)
- **Reglas criticas**:
- Siempre compilar con `-tags goolm` despues de modificar Go
- `agent.id` debe coincidir con nombre del directorio
- Nunca commitear tokens ni passwords
- Incluir seccion de seguridad en todo system prompt creado
- Env vars siguen la convencion: `MATRIX_TOKEN_<NORMALIZED_ID>`
- [ ] **2.2** Configurar `add_dirs` en config.yaml para dar acceso a las referencias:
```yaml
claude_code:
add_dirs:
- ".claude/rules"
- "agents/_template"
- "agents/_template_robot"
- "agents/assistant-bot"
- "agents/asistente-2"
- "internal/config"
```
- [ ] **2.3** Test manual: enviar "crea un robot que responda !saludo con Hola mundo" y verificar que:
- Ejecuta `create-full.sh` correctamente
- Crea los archivos del robot con config `type: robot`
- El comando `!saludo` esta registrado
- Compila sin errores
- Reinicia el launcher
- El nuevo robot aparece en los logs como running
### Fase 3 — Seguridad y restriccion de acceso
- [ ] **3.1** Crear grupo de agentes privilegiados en `security/agent-groups.yaml`:
```yaml
privileged:
- creator-bot
```
- [ ] **3.2** Agregar policy restrictiva en `security/permissions.yaml`:
```yaml
- agent_group: privileged
permissions:
- user_group: admins
actions: ["*"]
```
Esto asegura que solo los admins puedan interactuar con creator-bot.
- [ ] **3.3** Verificar que un usuario no-admin recibe "permiso denegado" al escribir a creator-bot
### Fase 4 — Gestion del servidor
- [ ] **4.1** Asegurar que el system prompt incluye instrucciones para ejecutar `./dev-scripts/server/restart.sh` despues de crear un agente
- [ ] **4.2** Incluir verificacion post-reinicio: el creator-bot debe revisar `run/launcher.log` para confirmar que el nuevo agente arranco con los mensajes esperados (`"e2ee ready"`, `"agent running"`)
- [ ] **4.3** Si el reinicio falla o el agente no arranca, reportar el error al usuario con los logs relevantes
### Fase 5 — Tests y documentacion
- [ ] **5.1** Test E2E en `e2e/tests/creator-bot.spec.ts`:
- Login como admin
- Enviar peticion de creacion de un robot simple
- Verificar que creator-bot responde con confirmacion
- (Opcional) Verificar que el robot creado responde en su room
- [ ] **5.2** Test de acceso denegado: login como usuario no-admin, verificar que creator-bot rechaza la peticion
- [ ] **5.3** Documentar en el system prompt del creator-bot los casos de error comunes y como reportarlos al usuario
## Ejemplo de uso
### Crear un robot simple
```
Usuario: Crea un robot que responda al comando !hora con la hora actual
Creator Bot: Entendido. Voy a crear un robot llamado "hora-bot" que responde al comando !hora.
[Ejecutando pipeline...]
1. Scaffold completado: agents/hora-bot/
2. Configurado como robot (type: robot, sin LLM)
3. Comando !hora registrado con respuesta dinamica
4. Build exitoso
5. Launcher reiniciado
El robot "hora-bot" esta activo. Puedes invitarlo a un room y usar !hora para obtener la hora actual.
Resumen:
- ID: hora-bot
- Tipo: Robot (command-only)
- Comandos: !hora, !help, !ping
- Room: invitalo desde Element
```
### Crear un agente con tools
```
Usuario: Necesito un agente experto en Docker que pueda ejecutar comandos SSH en mis servidores
Creator Bot: Voy a crear un agente con LLM y herramientas SSH habilitadas.
[Ejecutando pipeline...]
1. Scaffold completado: agents/docker-expert/
2. Configurado como agent (type: agent, provider: openai, model: gpt-4o)
3. Tools habilitadas: ssh_command con targets configurables
4. System prompt escrito con expertise en Docker, docker-compose, Dockerfile best practices
5. Seccion de seguridad incluida en el prompt
6. Build exitoso
7. Launcher reiniciado
8. Verificado: "docker-expert" aparece en logs como running
El agente "docker-expert" esta activo. Configuracion SSH pendiente:
- Editar agents/docker-expert/config.yaml para agregar los SSH targets permitidos
- Agregar las SSH keys necesarias
Quieres que configure los targets SSH ahora?
```
## Decisiones de diseno
1. **Provider claude-code en vez de LLM regular**: la creacion de agentes requiere acceso al filesystem, ejecucion de scripts y edicion de archivos. Un LLM regular con tools no tiene la capacidad de ejecutar pipelines complejos de forma autonoma. claude-code puede usar Bash, Read, Edit, Write directamente.
2. **working_dir = raiz del proyecto**: excepcion necesaria a la regla de sandbox. El creator-bot necesita:
- Leer templates y reglas existentes
- Ejecutar `create-full.sh` que opera sobre el arbol del proyecto
- Editar `cmd/launcher/main.go` para agregar blank imports
- Ejecutar `go build` y `restart.sh`
Sin acceso al repo, nada de esto es posible.
3. **ACL admin-only**: dado que el agente tiene acceso de escritura completo al repo, es critico restringirlo a usuarios de confianza. Se usa el sistema de permisos existente (`security/permissions.yaml`) con un grupo de agentes "privileged".
4. **Sin codigo nuevo en pkg/**: el creator-bot es pura composicion de infraestructura existente (scripts, templates, config schema, security). Las reglas en `agent.go` son triviales (DM/mention → LLM). Toda la inteligencia esta en el system prompt que guia al subprocess claude -p.
5. **Reinicio del launcher**: despues de crear un agente, el launcher debe reiniciarse para cargarlo. Esto afecta temporalmente a todos los agentes en ejecucion. Es aceptable porque el reinicio es rapido (~2-3 segundos) y la creacion de agentes es una operacion infrecuente.
## Prerequisitos
- Provider `claude-code` funcional (`shell/llm/claude_code.go`) -- ya implementado
- Scripts de creacion (`dev-scripts/agent/create-full.sh`) -- ya implementados
- Sistema de permisos (`security/`) -- ya implementado (issue 0024)
- Templates de agente (`agents/_template/`, `agents/_template_robot/`) -- ya existen
## Riesgos
| Riesgo | Probabilidad | Mitigacion |
|--------|-------------|------------|
| creator-bot tiene write access al repo completo | Alta (by design) | ACL admin-only via `security/permissions.yaml`; el agente solo se configura para usuarios de maxima confianza |
| Script `create-full.sh` falla a mitad de ejecucion | Media | El system prompt debe instruir al creator-bot a reportar errores con logs y sugerir pasos de recovery manual |
| Reinicio del launcher afecta todos los agentes | Baja impacto | El reinicio es rapido (~2-3s); los agentes reconectan automaticamente al sync de Matrix |
| claude -p genera codigo incorrecto para el nuevo agente | Media | El system prompt incluye las convenciones y el creator-bot debe compilar (`go build`) antes de reiniciar; si falla, corrige y reintenta |
| Agente creado tiene configuracion insegura | Baja | El system prompt obliga a incluir seccion de seguridad anti-injection en todo prompt generado; las tools son deny-by-default |
| Doble ejecucion accidental (usuario repite la peticion) | Baja | El creator-bot debe verificar si ya existe un agente con el ID solicitado antes de ejecutar el pipeline |
@@ -0,0 +1,296 @@
# 0038 — Webapps y dashboards embebidos en Element via widgets
**Estado:** pendiente
## Objetivo
Incorporar un servidor HTTP embebido en el launcher que sirva dashboards y mini-apps de los agentes, integrables en rooms de Element como Matrix widgets. Los usuarios podran ver estado en tiempo real, metricas e interfaces interactivas de sus agentes directamente desde sus rooms Matrix, sin salir del cliente.
## Contexto
- El launcher ya arranca multiples agentes en paralelo (`cmd/launcher/main.go`) y tiene un logger centralizado con JSONL rotado por dia.
- `shell/logger/query.go` ya expone `ReadLogs()` y `ReadDayLogs()` para consultar logs JSONL por agente y fecha — reutilizable para las API de metricas.
- `internal/config/schema.go` define `AgentConfig` con todas las secciones; falta una seccion `WebCfg` para el servidor HTTP.
- Matrix soporta widgets via state events `im.vector.modular.widgets` (Element Web y Desktop). El agente puede enviar estos state events usando mautrix-go.
- Actualmente no existe `shell/web/` ni ningun endpoint HTTP en el proyecto.
- El issue 0035 (audit trail + `!metrics`) agrega metricas del dia actual; este issue va mas alla con visualizacion web persistente y en tiempo real.
## Arquitectura
### Pure core / impure shell
- **`pkg/`** — no se modifica. No hay logica pura nueva; la transformacion de datos de logs a metricas se puede hacer con funciones helper dentro de `shell/web/handlers.go` (son inherentemente I/O-bound: leen archivos).
- **`shell/web/`** — NEW, 100% impuro: servidor HTTP, handlers API, SSE streaming, widget registration via Matrix.
- **`internal/config/schema.go`** — MOD: agregar `WebCfg` al schema de configuracion.
- **`cmd/launcher/main.go`** — MOD: arrancar servidor web junto con los agentes.
### Fase 1 — Servidor HTTP embebido
```
shell/web/ NEW — package del servidor web
shell/web/server.go NEW — setup del servidor HTTP + routes
shell/web/handlers.go NEW — handlers de los endpoints API
shell/web/static/ NEW — archivos estaticos del dashboard (embed.FS)
```
Endpoints:
- `GET /api/agents` — lista de agentes en ejecucion con estado (running/stopped/error)
- `GET /api/agents/{id}` — detalle del agente (config filtrada, uptime, ultima actividad)
- `GET /api/agents/{id}/metrics` — metricas agregadas del dia (reutiliza `shell/logger/query.go`)
- `GET /api/agents/{id}/logs` — SSE stream de logs en tiempo real
- `GET /dashboard` — SPA del dashboard (HTML/JS/CSS embebido via `embed.FS`)
- `GET /dashboard/{id}` — vista filtrada por agente (util para widgets)
### Fase 2 — Integracion Matrix widget
```
shell/web/widget.go NEW — helper para registrar widgets en rooms Matrix
```
- Cuando un agente se une a un room, opcionalmente registra un widget via state event `im.vector.modular.widgets`.
- Widget URL apunta al dashboard embebido filtrado para ese agente: `{base_url}/dashboard/{agent-id}?room={room_id}`.
- Usa mautrix-go `client.SendStateEvent()` para enviar el state event.
- Config: `matrix.widgets.enabled`, `matrix.widgets.base_url`, `matrix.widgets.auto_register`.
### Fase 3 — Dashboard UI
```
shell/web/static/index.html NEW — SPA entry point
shell/web/static/app.js NEW — logica JS del dashboard
shell/web/static/style.css NEW — estilos
```
- SPA con vanilla JS (o Preact si crece), embebido en el binario Go via `embed.FS`.
- Vistas:
- **Lista de agentes**: estado (running/stopped/error), tipo (agent/robot), uptime.
- **Detalle de agente**: resumen de config, mensajes recientes, uso de tools.
- **Log viewer en vivo**: via SSE (`EventSource` en JS), muestra logs en tiempo real.
- **Graficas de metricas**: mensajes/hora, tool calls, errores, latencia LLM.
## Archivos afectados
| Archivo | Cambio | Descripcion |
|---------|--------|-------------|
| `shell/web/` | NEW | Package completo del servidor web |
| `shell/web/server.go` | NEW | Setup HTTP server, router, middleware |
| `shell/web/handlers.go` | NEW | Handlers API: agents, metrics, logs SSE |
| `shell/web/widget.go` | NEW | Helper para registrar widgets Matrix en rooms |
| `shell/web/static/` | NEW | Dashboard SPA (HTML/JS/CSS embebido) |
| `internal/config/schema.go` | MOD | Agregar `WebCfg` con Enabled, Port, Host, BasePath, Auth |
| `cmd/launcher/main.go` | MOD | Arrancar servidor web junto con los agentes |
| `shell/matrix/client.go` | MOD | Agregar metodo para enviar state events de widget (si no existe) |
## Tareas
### Fase 1 — Servidor HTTP embebido
- [ ] **1.1** Agregar `WebCfg` a `internal/config/schema.go`:
```go
type WebCfg struct {
Enabled bool `yaml:"enabled"` // habilitar servidor web (default false)
Host string `yaml:"host"` // bind address (default "127.0.0.1")
Port int `yaml:"port"` // puerto HTTP (default 8080)
BasePath string `yaml:"base_path"` // prefijo de rutas (default "/")
Auth WebAuthCfg `yaml:"auth"` // autenticacion
}
type WebAuthCfg struct {
Enabled bool `yaml:"enabled"` // requerir autenticacion
TokenEnv string `yaml:"token_env"` // env var con el token de acceso
}
```
Agregar campo `Web WebCfg yaml:"web"` a `AgentConfig` (o a un nuevo `LauncherConfig` si se decide no atar a cada agente).
- [ ] **1.2** Crear `shell/web/server.go`:
- Struct `Server` con `http.Server`, referencia a la lista de agentes en ejecucion, config, logger.
- Constructor `New(cfg WebCfg, agents []AgentInfo, logDir string, logger *slog.Logger) *Server`.
- Metodo `Start(ctx context.Context) error` — arranca el servidor HTTP en goroutine, se detiene con ctx.
- Router usando `http.ServeMux` de la stdlib (Go 1.22+ soporta `{id}` patterns).
- Middleware basico: logging, CORS (necesario para iframe de widgets), auth opcional.
- [ ] **1.3** Crear `shell/web/handlers.go` — handler `GET /api/agents`:
- Devuelve JSON array con: `id`, `name`, `type`, `status`, `uptime`, `description`.
- La info de agentes se obtiene de un registry que el launcher puebla al arrancar.
- [ ] **1.4** Handler `GET /api/agents/{id}`:
- Config del agente (filtrada: sin tokens, passwords, API keys).
- Uptime, ultima actividad, cantidad de mensajes procesados.
- Error si el `{id}` no existe.
- [ ] **1.5** Handler `GET /api/agents/{id}/metrics`:
- Reutilizar `shell/logger/ReadDayLogs()` para obtener logs del dia actual.
- Calcular: mensajes recibidos, comandos ejecutados, llamadas LLM (count + tokens + latencia media), tool calls (count + errores), errores totales.
- Devuelve JSON con los agregados.
- [ ] **1.6** Handler `GET /api/agents/{id}/logs` (SSE):
- Server-Sent Events stream con los ultimos N logs y nuevos logs en tiempo real.
- `Content-Type: text/event-stream`.
- Tail del archivo JSONL actual con polling o fsnotify.
- [ ] **1.7** Integrar arranque del servidor en `cmd/launcher/main.go`:
- Leer config web (puede ser una seccion nueva en un `launcher.yaml` o reutilizar env vars).
- Si `web.enabled`, crear `web.Server` y arrancarlo en el mismo `WaitGroup`.
- Pasar la lista de agentes al servidor para que los pueda consultar.
- [ ] **1.8** Tests: handlers con `httptest`:
- Test de `/api/agents` con lista de agentes mock.
- Test de `/api/agents/{id}` con agente existente y no existente.
- Test de `/api/agents/{id}/metrics` con logs JSONL de ejemplo en tmpdir.
- Test del middleware de auth (token valido, invalido, deshabilitado).
### Fase 2 — Integracion Matrix widget
- [ ] **2.1** Investigar formato del state event `im.vector.modular.widgets`:
- Campos requeridos: `type`, `url`, `name`, `id`, `creatorUserId`.
- Verificar compatibilidad con Element Web 1.x actual.
- [ ] **2.2** Crear `shell/web/widget.go`:
- Funcion `RegisterWidget(ctx context.Context, client *mautrix.Client, roomID, widgetID, widgetName, baseURL, agentID string) error`.
- Construye el state event content con la URL del dashboard filtrado.
- Envia via `client.SendStateEvent(roomID, "im.vector.modular.widgets", widgetID, content)`.
- Funcion `UnregisterWidget(...)` para limpiar al salir.
- [ ] **2.3** Agregar seccion `matrix.widgets.*` al config:
```yaml
matrix:
widgets:
enabled: false # habilitar registro automatico de widgets
base_url: "" # URL publica del servidor web (requerido si enabled)
auto_register: true # registrar widget al unirse a room
widget_name: "Dashboard" # nombre visible del widget
```
- [ ] **2.4** Integrar auto-registro en el runtime:
- En `devagents/runtime.go` o `devagents/handler.go`, despues de join a room, si `widgets.enabled` y `base_url` configurado, llamar a `RegisterWidget`.
- Manejar error gracefully (log warning, no romper el agente).
- [ ] **2.5** Tests:
- Test del formato del state event generado (campos requeridos presentes).
- Test de `RegisterWidget` con mock de mautrix client.
- Test de la URL generada (incluye agent ID y room ID como query params).
### Fase 3 — Dashboard UI
- [ ] **3.1** Crear `shell/web/static/index.html`:
- HTML minimo con viewport meta, link a CSS, script tag.
- Routing basico client-side (hash-based: `#/`, `#/agent/{id}`).
- [ ] **3.2** Crear `shell/web/static/app.js`:
- Fetch `/api/agents` y renderizar lista de agentes con indicadores de estado.
- Colores por status: verde (running), rojo (error), gris (stopped).
- Click en agente → navega a vista detalle.
- [ ] **3.3** Vista detalle de agente:
- Fetch `/api/agents/{id}` y `/api/agents/{id}/metrics`.
- Mostrar: nombre, tipo, uptime, descripcion, metricas del dia en tabla.
- Seccion de metricas con numeros grandes y colores.
- [ ] **3.4** Log viewer en vivo:
- Conectar a `/api/agents/{id}/logs` via `EventSource`.
- Mostrar logs en panel scrollable con auto-scroll.
- Colores por nivel: DEBUG (gris), INFO (blanco), WARN (amarillo), ERROR (rojo).
- [ ] **3.5** Graficas de metricas (simple):
- Canvas o SVG basico (sin librerias externas) para mensajes/hora y tool calls.
- Alternativa: ASCII-art charts si se quiere mantener minimalismo extremo.
- [ ] **3.6** Embed estaticos en Go:
```go
//go:embed static/*
var staticFS embed.FS
```
- Servir con `http.FileServer(http.FS(staticFS))` en el router.
- Fallback a `index.html` para SPA routing.
- [ ] **3.7** Tests del dashboard:
- Test de que `embed.FS` contiene los archivos esperados.
- Test de que `/dashboard` sirve HTML valido.
- Test de que las rutas SPA redirigen a `index.html`.
### Fase 4 — Tests de integracion y cleanup
- [ ] **4.1** Test de integracion end-to-end: arrancar servidor, verificar que todos los endpoints responden correctamente con agentes mock.
- [ ] **4.2** Documentar configuracion en el config.yaml template de `agents/_template/`.
- [ ] **4.3** Agregar seccion en `CLAUDE.md` sobre el servidor web y widgets.
## Ejemplo de uso
### Configuracion basica
```yaml
# En la config del launcher o en un agent config
web:
enabled: true
host: "0.0.0.0"
port: 8080
auth:
enabled: true
token_env: "WEB_DASHBOARD_TOKEN"
```
### Dashboard standalone
1. Habilitar en config: `web.enabled: true`, `web.port: 8080`
2. Arrancar launcher: `./dev-scripts/server/start.sh`
3. Navegar a `http://localhost:8080/dashboard`
4. Ver lista de agentes con estado, click en uno para ver metricas y logs en vivo
### Widget en Element
1. Configurar adicionalmente:
```yaml
matrix:
widgets:
enabled: true
base_url: "https://bots.example.com"
auto_register: true
```
2. Agente se une a un room → auto-registra widget
3. En Element Web aparece un panel con el dashboard filtrado para ese agente
4. El usuario ve metricas y logs sin salir del room
### Acceso directo a API
```bash
# Lista de agentes
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/agents
# Metricas de un agente
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/agents/asistente-2/metrics
# Stream de logs en vivo
curl -H "Authorization: Bearer $TOKEN" -N http://localhost:8080/api/agents/asistente-2/logs
```
## Decisiones de diseno
1. **`net/http` sin frameworks**: consistente con el estilo del proyecto (stdlib, sin dependencias externas para HTTP). Go 1.22+ tiene routing con path params nativo en `http.ServeMux`.
2. **`embed.FS` para estaticos**: deployment de un solo binario. No se necesitan archivos externos ni pasos de build frontend separados. El dashboard es lo suficientemente simple para vanilla JS.
3. **SSE en vez de WebSocket para logs en vivo**: SSE es mas simple, funciona a traves de proxies HTTP, reconexion automatica en el browser, y es suficiente para un flujo unidireccional (servidor → cliente). WebSocket seria overkill para este caso.
4. **Config `WebCfg` a nivel launcher, no por agente**: el servidor web es uno solo para todos los agentes (lo sirve el launcher). Evita N puertos por N agentes. La info por agente se filtra en los endpoints.
5. **Widget registration opcional**: el dashboard funciona standalone sin Matrix widgets. Los widgets son un bonus para integracion en Element. Si el usuario no configura `widgets.base_url`, simplemente no se registran widgets.
6. **Auth por token simple**: para la primera iteracion, un bearer token en env var es suficiente. Integracion con Matrix OIDC o session cookies se puede agregar despues si es necesario.
7. **Filtrar secrets del API**: el endpoint `/api/agents/{id}` nunca expone tokens, API keys, passwords ni recovery keys. Se filtran los campos `*_env`, `access_token_env`, etc. antes de serializar.
## Prerequisitos
- **Issue 0035 (audit trail + !metrics)**: no es bloqueante pero si esta implementado, el endpoint de metricas puede reutilizar la logica de agregacion. Sin el, se implementa directamente leyendo JSONL.
- `shell/logger/query.go` — ya existe y funciona.
- Go 1.22+ — necesario para `http.ServeMux` con path params (el proyecto usa Go 1.23.5, OK).
## Riesgos
| Riesgo | Mitigacion |
|--------|------------|
| Element widget support varia entre clientes (Web vs mobile vs Desktop) | Testear con Element Web primero (el cliente principal del proyecto). Mobile puede no soportar widgets custom. Documentar limitaciones. |
| CORS necesario para iframe de widgets | Agregar headers CORS configurables en el middleware del servidor. Restringir origenes al homeserver. |
| HTTPS obligatorio para widgets en produccion | Element requiere HTTPS para widgets. Documentar que en produccion se necesita reverse proxy (nginx/caddy) con TLS. En desarrollo localhost funciona sin HTTPS. |
| Dashboard crece en complejidad → SPA inmanejable con vanilla JS | Empezar simple. Si crece, migrar a Preact (~3KB) que se puede embeber sin build system. No usar React/Vue/frameworks pesados. |
| Servidor web expuesto → superficie de ataque | Auth por defecto deshabilitada → solo escucha en 127.0.0.1. En produccion, auth habilitada + HTTPS + reverse proxy. Nunca exponer secretos en la API. |
| SSE streaming consume memoria si hay muchos clientes | Limitar a N conexiones SSE simultaneas (configurable). Desconectar clientes idle. Buffer limitado de logs en memoria. |
| `embed.FS` aumenta tamano del binario | Los archivos estaticos son HTML/JS/CSS minimo (estimado <100KB). Impacto negligible vs las dependencias Go existentes. |
+264
View File
@@ -0,0 +1,264 @@
# 0039 — Recordatorios dinamicos y crons que invocan agentes
**Estado:** pendiente
## Objetivo
Extender el sistema cron para soportar (1) recordatorios dinamicos creados en runtime via tool calls del LLM ("recuerdame a las 3pm que...") y (2) un nuevo action kind `agent_call` que invoca a otro agente con un prompt, habilitando workflows automatizados agente-a-agente en horario.
## Contexto
- El scheduler actual (`shell/cron/scheduler.go`) soporta `send_message` y `llm_prompt` como action kinds, configurados estaticamente via YAML.
- No existe forma de crear schedules en runtime: si un usuario pide "recuerdame X", el agente no puede programar un disparo futuro sin editar config.
- El bus inter-agente (`shell/bus/`) ya permite comunicacion entre agentes via `SendAndWait`, pero no esta integrado con el cron.
- `ScheduledAction` en `internal/config/schema.go` define los campos para `send_message` y `llm_prompt` pero no tiene campos para invocacion de agentes.
- Las tools existentes siguen el patron subpackage en `tools/` (ej: `tools/file/`, `tools/ssh/`, `tools/clock/`).
- SQLite ya esta disponible via modernc (pure-Go, CGO_ENABLED=0) con el shim en `cmd/launcher/sqlite.go`.
## Arquitectura
### Fase 1 — Tipos puros y storage de reminders
```
pkg/reminder/types.go NEW — tipo Reminder puro (datos, sin I/O)
shell/reminder/store.go NEW — SQLite-backed store (Create, Delete, List, MarkFired, LoadActive)
shell/reminder/store_test.go NEW — tests CRUD del store
```
**Pure core / impure shell:**
- `pkg/reminder/` es 100% puro: solo define el struct `Reminder` y constantes. Sin imports de I/O.
- `shell/reminder/` es impuro: abre conexion SQLite, lee/escribe en disco.
### Fase 2 — Tools de recordatorios
```
tools/reminder/reminder.go NEW — create_reminder, delete_reminder, list_reminders
tools/reminder/reminder_test.go NEW — tests de validacion de params y parsing de tiempo
devagents/runtime.go MOD — registrar reminder tools cuando config lo habilita
```
### Fase 3 — Scheduler dinamico
```
shell/cron/scheduler.go MOD — AddSchedule, RemoveSchedule, soporte para IDs dinamicos
shell/cron/actions.go MOD — nuevo action kind "reminder" (mensaje personalizado a room/usuario)
shell/cron/scheduler_test.go MOD — tests de add/remove dinamico y one-shot auto-cleanup
```
### Fase 4 — Agent-to-agent cron calls
```
internal/config/schema.go MOD — campos AgentCall en ScheduledAction
shell/cron/actions.go MOD — nuevo action kind "agent_call" usando shell/bus/
shell/cron/scheduler.go MOD — inyeccion del bus como dependencia
```
## Tareas
### Fase 1 — Tipos puros y storage de reminders
- [ ] **1.1** Crear `pkg/reminder/types.go` con struct `Reminder`:
- Campos: `ID string`, `UserID string`, `RoomID string`, `Message string`, `CronExpr string`, `OneShot bool`, `CreatedAt time.Time`, `FiredAt *time.Time`
- Constantes para estados: `StatusActive`, `StatusFired`, `StatusDeleted`
- Sin imports de I/O, sin side effects
- [ ] **1.2** Crear `shell/reminder/store.go` con `Store` struct:
- Constructor `New(dbPath string) (*Store, error)` — abre SQLite, crea tabla si no existe
- `Create(ctx, Reminder) error` — inserta un reminder
- `Delete(ctx, id string) error` — borrado logico (marcar como deleted)
- `List(ctx, roomID string) ([]Reminder, error)` — listar activos de una room
- `MarkFired(ctx, id string) error` — marcar como disparado con timestamp
- `LoadActive(ctx) ([]Reminder, error)` — cargar todos los activos (para startup)
- Auto-crear tabla `reminders` en init (`CREATE TABLE IF NOT EXISTS`)
- [ ] **1.3** Tests del store en `shell/reminder/store_test.go`:
- Test CRUD completo: crear, listar, marcar fired, borrar
- Test que LoadActive no retorna reminders fired ni deleted
- Test de filtrado por roomID en List
- Usar tmpdir para base de datos de test
### Fase 2 — Tools de recordatorios
- [ ] **2.1** Crear `tools/reminder/reminder.go` con `NewCreateReminder(store, scheduler)`:
- Params: `message` (string, required), `time` (string, required — "15:00", "2026-04-10 15:00", "en 30 minutos"), `recurring` (bool, optional, default false), `cron` (string, optional — expresion cron para recurrentes)
- Parsear expresiones de tiempo naturales a cron expressions o timestamps absolutos
- Generar ID unico (UUID o nanoid)
- Persistir en store y registrar en scheduler
- [ ] **2.2** Crear `NewListReminders(store)`:
- Sin params requeridos (usa roomID del contexto del mensaje)
- Retorna lista formateada de reminders activos de la room
- [ ] **2.3** Crear `NewDeleteReminder(store, scheduler)`:
- Params: `id` (string, required)
- Borrar del store y remover del scheduler
- Validar que el reminder pertenece a la room del solicitante
- [ ] **2.4** Registrar tools en `devagents/runtime.go`:
- Condicion: nueva seccion `tools.reminders.enabled` en config
- Pasar referencia al store y al scheduler
- [ ] **2.5** Anadir `ReminderToolCfg` a `ToolsCfg` en `internal/config/schema.go`:
- Campos: `Enabled bool`, `MaxPerRoom int` (limite de reminders activos por room, default 50), `DBPath string` (default: `data/reminders.db`)
- [ ] **2.6** Tests en `tools/reminder/reminder_test.go`:
- Validacion de params requeridos
- Parsing de formatos de tiempo: "15:00", "2026-04-10 15:00", "en 30 minutos", "manana a las 9"
- Error si formato no reconocido
- Rate limit: error si se excede MaxPerRoom
### Fase 3 — Scheduler dinamico
- [ ] **3.1** Anadir `AddSchedule(id string, sc ScheduleCfg) error` al Scheduler:
- Registra un nuevo schedule en el cron runner en caliente
- Guardar referencia al `cron.EntryID` para poder remover despues
- Thread-safe (mutex sobre el mapa de entries)
- [ ] **3.2** Anadir `RemoveSchedule(id string) error` al Scheduler:
- Remover entry del cron runner por EntryID
- Limpiar del mapa interno
- [ ] **3.3** Implementar action kind `reminder` en `shell/cron/actions.go`:
- Envia mensaje personalizado al room: `"<prefix> @<user> Recordatorio: <mensaje>"`
- Nuevos campos en `ScheduledAction`: `UserID string` (para mention), `ReminderID string` (para tracking)
- [ ] **3.4** Logica one-shot:
- Despues de disparar un reminder one-shot, auto-remover del cron via `RemoveSchedule`
- Marcar como fired en el store via `MarkFired`
- Loguear: `"reminder_fired"`, `"reminder_auto_removed"`
- [ ] **3.5** On startup: cargar reminders persistidos en `devagents/runtime.go`:
- Despues de crear el Scheduler, llamar `store.LoadActive()`
- Registrar cada reminder activo via `scheduler.AddSchedule()`
- Descartar reminders one-shot cuya hora ya paso (marcar como fired)
- [ ] **3.6** Tests en `shell/cron/scheduler_test.go`:
- Test AddSchedule + RemoveSchedule (verificar que el cron entry existe/no existe)
- Test reminder action kind (mock MatrixSender, verificar mensaje con mention)
- Test one-shot auto-cleanup (verificar que despues de fire se remueve)
### Fase 4 — Agent-to-agent cron calls
- [ ] **4.1** Anadir campos a `ScheduledAction` en `internal/config/schema.go`:
- `TargetAgent string` — ID del agente destino
- `PromptTemplate string` — path al archivo .md con el prompt (reutilizar campo `Template`)
- [ ] **4.2** Inyectar `shell/bus.Bus` como dependencia del Scheduler:
- Nuevo campo `bus *bus.Bus` en Scheduler struct
- Parametro opcional en `New()` (nil si no hay bus disponible)
- [ ] **4.3** Implementar action kind `agent_call` en `shell/cron/actions.go`:
- Leer prompt desde `PromptTemplate` o inline `Prompt`
- Enviar via `bus.SendAndWait()` al agente destino con kind `"task"`
- El agente destino procesa el prompt via su LLM
- Enviar la respuesta al `OutputRoom` configurado
- Timeout configurable (default 2 minutos)
- [ ] **4.4** Documentar ejemplo de config en `crons/README.md` o similar
- [ ] **4.5** Tests en `shell/cron/scheduler_test.go`:
- Test `agent_call` con mock bus: verificar que envia mensaje correcto al agente destino
- Test timeout: verificar que si el agente no responde, se loguea error
- Test con bus nil: verificar que se loguea warning y se salta
### Fase 5 — Tests de integracion y cleanup
- [ ] **5.1** Test de integracion: crear reminder via tool → verificar que el scheduler lo tiene → fire → verificar store actualizado
- [ ] **5.2** Documentar nuevos action kinds en el system prompt de agentes que usen reminders
- [ ] **5.3** Actualizar `CLAUDE.md` con la nueva seccion de reminder tools si aplica
- [ ] **5.4** Verificar que `go build -tags goolm ./...` compila sin errores
- [ ] **5.5** Verificar que `go test -tags goolm ./...` pasa sin errores
## Ejemplo de uso
### Recordatorio one-shot via LLM
```
Usuario: "Recuerdame manana a las 9am que tengo reunion con el equipo"
Agente: [tool_call] create_reminder(message="Reunion con el equipo", time="2026-04-10 09:00", recurring=false)
Agente: "Listo, te recordare manana a las 9:00 AM."
→ 2026-04-10 09:00:
Agente envia: "⏰ @usuario Recordatorio: Reunion con el equipo"
→ Reminder auto-borrado del scheduler y marcado como fired en store.
```
### Recordatorio recurrente
```
Usuario: "Recuerdame todos los lunes a las 10am hacer el standup"
Agente: [tool_call] create_reminder(message="Hacer el standup", cron="0 10 * * 1", recurring=true)
Agente: "Configurado. Cada lunes a las 10:00 AM te recordare."
→ Cada lunes 10:00:
Agente envia: "⏰ @usuario Recordatorio: Hacer el standup"
```
### Listar y borrar reminders
```
Usuario: "Que recordatorios tengo?"
Agente: [tool_call] list_reminders()
Agente:
"Tienes 2 recordatorios activos:
1. [abc123] Reunion con el equipo — 2026-04-10 09:00 (one-shot)
2. [def456] Hacer el standup — lunes 10:00 (recurrente)"
Usuario: "Borra el del standup"
Agente: [tool_call] delete_reminder(id="def456")
Agente: "Recordatorio eliminado."
```
### Agent-to-agent cron call
Config en `agents/asistente-2/config.yaml`:
```yaml
schedules:
- name: daily-analysis
cron: "0 18 * * *"
action:
kind: agent_call
target_agent: "asistente-2"
prompt_template: "crons/daily-summary/prompts/prompt.md"
output_room: "!room:matrix-af2f3d.organic-machine.com"
```
Resultado: cada dia a las 18:00, el scheduler envia el prompt al agente `asistente-2` via bus. El agente procesa con su LLM y envia la respuesta al room configurado.
## Decisiones de diseno
1. **SQLite para persistencia de reminders**: ya tenemos el driver modernc configurado y probado. Un reminder es un dato simple (ID, mensaje, cron, estado). No justifica una dependencia nueva.
2. **Parsing de tiempo natural — enfoque progresivo**: empezar con formatos simples (ISO datetime `2026-04-10 15:00`, hora del dia `15:00`, expresiones cron). Anadir expresiones relativas (`en 30 minutos`, `manana a las 9`) como mejora incremental. No intentar NLP completo — el LLM ya interpreta la intencion, la tool solo necesita parsear el formato final.
3. **One-shot auto-delete**: los reminders que se disparan una vez se marcan como `fired` en el store (para auditoria) y se remueven del scheduler. Evita acumulacion de entries fantasma en el cron runner.
4. **`agent_call` usa el bus existente**: no se necesita protocolo nuevo. `SendAndWait` ya implementa el patron request-reply con timeout. El scheduler actua como el "from" agent, el target procesa via su pipeline LLM normal.
5. **Tools en subpackage `tools/reminder/`**: sigue el patron de `tools/file/`, `tools/ssh/`, etc. Cada tool recibe sus dependencias (store, scheduler) via constructor.
6. **Reminders scoped a room**: un reminder solo es visible y gestionable desde la room donde se creo. Esto evita que un usuario en room A borre reminders de room B.
## Prerequisitos
- Ninguno critico. Todo usa infraestructura existente:
- `shell/cron/scheduler.go` — scheduler a extender
- `shell/bus/` — bus inter-agente para `agent_call`
- `internal/config/schema.go` — config a extender
- SQLite via modernc (ya disponible)
- Pattern de tools en `tools/` (ya establecido)
## Riesgos
| Riesgo | Mitigacion |
|--------|------------|
| Parsing de tiempo natural es complejo | Empezar simple (ISO, hora, cron). El LLM normaliza la entrada antes de llamar la tool. Anadir formatos relativos iterativamente. |
| Timezone handling | Usar timezone del servidor inicialmente. Documentar la limitacion. Anadir soporte per-user TZ en un issue futuro si hay demanda. |
| Bus no disponible para `agent_call` | Si el bus es nil (agente standalone), loguear warning y saltar la ejecucion. Nunca crashear. |
| Tabla de reminders crece sin limite | One-shot se marcan fired (no se borran fisicamente para auditoria). Anadir retention policy (borrar fired > 30 dias) como cleanup task. |
| Scheduler concurrency con AddSchedule | `robfig/cron` es thread-safe para `AddFunc`/`Remove`. Proteger el mapa interno de IDs con mutex propio. |
| Reminder con cron invalido | Validar la expresion cron en el tool `create_reminder` antes de persistir. Retornar error claro al LLM si la expresion es invalida. |
+222
View File
@@ -0,0 +1,222 @@
# 0040 — Soporte para mensajes de voz (audio → STT → procesamiento)
**Estado:** pendiente
## Objetivo
Permitir que los agentes reciban y procesen mensajes de voz (`m.audio`) desde Matrix. El audio se descarga, se transcribe via Speech-to-Text (Whisper API), y el texto resultante entra al pipeline normal de procesamiento. Los usuarios pueden hablar con sus agentes enviando notas de voz desde Element.
## Contexto
- El listener en `shell/matrix/listener.go` actualmente solo maneja mensajes de texto: extrae `body` de `event.EventMessage` y lo pasa como `Content` al `MessageContext`.
- Los agentes procesan texto puro a traves de reglas → LLM. No hay soporte para ningun tipo de media.
- El proyecto ya tiene `github.com/sashabaranov/go-openai` como dependencia, que incluye el endpoint `CreateTranscription` para Whisper API.
- Element envia notas de voz como eventos `m.room.message` con `msgtype: m.audio` y contenido en formato OGG/Opus via URI `mxc://`.
- La Whisper API de OpenAI acepta OGG/Opus directamente — no se necesita conversion de formato.
- Limite de la Whisper API: 25 MB por archivo de audio.
## Arquitectura
### Patron pure core / impure shell
```
pkg/stt/types.go PURO — interfaz Transcriber + tipos de resultado (solo datos)
shell/stt/whisper.go IMPURO — implementacion OpenAI Whisper API
shell/stt/local.go IMPURO — implementacion opcional whisper.cpp (subproceso)
shell/matrix/listener.go IMPURO — deteccion de m.audio, descarga, orquestacion
shell/matrix/client.go IMPURO — metodo DownloadMedia para URIs mxc://
pkg/decision/types.go PURO — campos IsVoice, AudioDuration en MessageContext
internal/config/schema.go PURO — seccion STTCfg en el schema de configuracion
devagents/runtime.go COMPOSICION — inicializar transcriber, conectar con listener
```
### Flujo de datos
```
Matrix event (m.audio)
→ listener detecta msgtype "m.audio"
→ extrae mxc:// URI, mimetype, duration
→ client.DownloadMedia(mxcURL) → []byte
→ transcriber.Transcribe(ctx, audioData, "ogg") → texto
→ MessageContext{Content: texto, IsVoice: true, AudioDuration: 15.0}
→ reglas normales → LLM → respuesta de texto
```
## Archivos afectados
| Archivo | Accion | Descripcion |
|---------|--------|-------------|
| `pkg/stt/types.go` | NEW | Interfaz `Transcriber` y tipo `TranscriptionResult` |
| `shell/stt/whisper.go` | NEW | Implementacion OpenAI Whisper API |
| `shell/stt/local.go` | NEW | Implementacion opcional whisper.cpp via subproceso |
| `shell/matrix/listener.go` | MOD | Detectar `m.audio`, descargar audio, orquestar transcripcion |
| `shell/matrix/client.go` | MOD | Añadir `DownloadMedia(ctx, mxcURL) ([]byte, string, error)` |
| `pkg/decision/types.go` | MOD | Añadir `IsVoice bool`, `AudioDuration float64` a `MessageContext` |
| `internal/config/schema.go` | MOD | Añadir `STTCfg` al schema de configuracion |
| `devagents/runtime.go` | MOD | Inicializar `Transcriber` cuando STT esta habilitado, pasar al listener |
## Tareas
### Fase 1 — Deteccion y descarga de audio
- [ ] **1.1** Modificar `listener.go` en el handler `OnEventType(event.EventMessage)` para inspeccionar el campo `msgtype` del evento. Si es `m.audio`, extraer: `url` (URI `mxc://`), `info.mimetype`, `info.duration` (milisegundos).
- [ ] **1.2** Implementar `DownloadMedia(ctx context.Context, mxcURL string) ([]byte, string, error)` en `shell/matrix/client.go`. Usa `mautrix.Client.Download()` para obtener el contenido binario desde la URI `mxc://`. Retorna los bytes, el mimetype detectado y error.
- [ ] **1.3** Validar tamaño del audio antes de transcribir: rechazar archivos > 25 MB (limite de la Whisper API). Responder al usuario con mensaje explicativo si el audio es demasiado grande.
- [ ] **1.4** Validar duracion del audio: rechazar si excede `stt.max_duration` del config (default 120 segundos). Responder al usuario con mensaje explicativo.
- [ ] **1.5** Tests: mock de evento `m.audio` con campos esperados, verificar extraccion correcta de URI y metadata. Test de rechazo por tamaño y duracion.
### Fase 2 — Speech-to-Text
- [ ] **2.1** Definir tipos puros en `pkg/stt/types.go`:
```go
// Transcriber converts audio data to text. Pure interface, no I/O.
type Transcriber interface {
Transcribe(ctx context.Context, audio []byte, format string) (TranscriptionResult, error)
}
// TranscriptionResult holds the output of a transcription.
type TranscriptionResult struct {
Text string
Language string
Duration float64
Confidence float64
}
```
- [ ] **2.2** Implementar `shell/stt/whisper.go` — OpenAI Whisper API:
- Usar `github.com/sashabaranov/go-openai` `CreateTranscription`
- Modelo: `whisper-1`
- Language hint desde config del agente (mejora la precision)
- Escribir audio a archivo temporal (el SDK requiere filepath), limpiar despues
- Manejar errores de la API con contexto descriptivo
- [ ] **2.3** Implementar `shell/stt/local.go` — whisper.cpp via subproceso (opcional):
- Ejecutar: `whisper --model base --language es --output-format txt <tmpfile>`
- Parsear stdout como texto transcrito
- Verificar que el binario existe al inicializar; si no, retornar error descriptivo
- Util para desarrollo local y ahorro de costos
- [ ] **2.4** Añadir `STTCfg` a `internal/config/schema.go`:
```go
type STTCfg struct {
Enabled bool `yaml:"enabled"`
Provider string `yaml:"provider"` // "openai" | "local"
Model string `yaml:"model"` // e.g. "whisper-1"
Language string `yaml:"language"` // ISO 639-1, e.g. "es"
MaxDuration int `yaml:"max_duration"` // seconds, default 120
APIKeyEnv string `yaml:"api_key_env"` // e.g. "OPENAI_API_KEY"
}
```
Añadir campo `STT STTCfg \`yaml:"stt"\`` a `AgentConfig`.
- [ ] **2.5** Factory function en `shell/stt/`:
```go
func NewTranscriber(cfg config.STTCfg) (stt.Transcriber, error)
```
Selecciona implementacion segun `cfg.Provider`. Resuelve API key desde env var.
- [ ] **2.6** Tests: transcriber con mock de API responses. Test del factory con providers validos e invalidos. Test de manejo de errores (API timeout, audio corrupto).
### Fase 3 — Integracion en el pipeline
- [ ] **3.1** Añadir a `decision.MessageContext` en `pkg/decision/types.go`:
```go
IsVoice bool // true if the message originated from a voice note
AudioDuration float64 // duration in seconds of the original audio
```
- [ ] **3.2** En `listener.go`: para eventos `m.audio`, ejecutar el flujo completo:
1. Descargar audio via `DownloadMedia`
2. Validar tamaño y duracion
3. Transcribir via `Transcriber`
4. Crear `MessageContext` con `Content = texto transcrito`, `IsVoice = true`, `AudioDuration = duracion`
5. Pasar al handler normal (reglas → LLM)
- [ ] **3.3** Opcional: enviar typing indicator "Transcribiendo..." mientras se procesa el audio. Mejora la UX para audios largos donde la transcripcion tarda 2-5 segundos.
- [ ] **3.4** Inicializar `Transcriber` en `devagents/runtime.go` cuando `cfg.STT.Enabled == true`. Pasar la instancia al listener para que pueda usarla al recibir eventos de audio.
- [ ] **3.5** Si STT no esta habilitado y llega un `m.audio`, responder con mensaje informativo: "No tengo habilitada la transcripcion de audio. Enviame un mensaje de texto."
### Fase 4 — Tests y cleanup
- [ ] **4.1** Tests unitarios de `pkg/stt/types.go`: verificar que el tipo cumple la interfaz (compile-time check).
- [ ] **4.2** Test de integracion: mock transcriber → listener recibe evento m.audio → produce `MessageContext` correcto con `IsVoice=true` y texto transcrito.
- [ ] **4.3** Test de regresion: verificar que mensajes de texto (`m.text`) siguen funcionando identicamente tras los cambios en el listener.
- [ ] **4.4** Documentar la configuracion STT en un ejemplo dentro del config template (`agents/_template/config.yaml`) con la seccion comentada.
## Ejemplo de uso
### Config del agente
```yaml
# agents/asistente-2/config.yaml
stt:
enabled: true
provider: openai
model: whisper-1
language: es
max_duration: 120
api_key_env: OPENAI_API_KEY
```
### Flujo en Matrix
```
Usuario: [envia nota de voz de 15 segundos desde Element]
"¿Cuál es el estado de los servidores?"
→ Agente descarga OGG desde mxc://matrix-af2f3d.organic-machine.com/audio123
→ Whisper API transcribe: "¿Cuál es el estado de los servidores?"
→ Pipeline normal: reglas match → LLM responde con estado de servidores
Bot: Los servidores están todos operativos. El último check de salud
fue hace 5 minutos y todos los servicios reportan status OK.
```
### Sin STT habilitado
```
Usuario: [envia nota de voz]
Bot: No tengo habilitada la transcripción de audio.
Enviame un mensaje de texto por favor.
```
## Decisiones de diseño
1. **OpenAI Whisper como provider primario**: ya tenemos el SDK (`go-openai`) como dependencia. Whisper-1 tiene excelente calidad para español y acepta OGG/Opus directamente sin conversion. Costo accesible (~$0.006/minuto).
2. **whisper.cpp como alternativa local**: para desarrollo, testing y escenarios donde se prefiere no enviar audio a APIs externas. Es opcional — si el binario no esta instalado, el provider `local` falla al inicializar con error claro.
3. **Texto transcrito entra al pipeline existente**: no se crea un flujo paralelo para audio. La transcripcion produce texto que pasa por las mismas reglas y LLM que un mensaje escrito. Esto maximiza la reutilizacion y minimiza la complejidad.
4. **`IsVoice` flag en MessageContext**: permite que las reglas o el LLM ajusten su comportamiento para mensajes de voz (por ejemplo, respuestas mas concisas, o confirmar lo que se escucho). No es obligatorio usarlo — el agente puede ignorarlo.
5. **Audio no se persiste**: los bytes del audio se mantienen en memoria solo durante la transcripcion y se descartan inmediatamente despues. No se guardan en disco ni en la base de datos. Esto simplifica el manejo y evita problemas de almacenamiento.
6. **Interfaz `Transcriber` en `pkg/stt/` (puro)**: la interfaz y los tipos de resultado son datos puros sin I/O, coherente con el patron del proyecto. Las implementaciones impuras viven en `shell/stt/`.
## Prerequisitos
- Ninguna dependencia nueva de Go — `go-openai` ya esta en `go.mod` y tiene `CreateTranscription`.
- `mautrix` ya soporta descarga de contenido media via `mxc://` URIs.
- Para el provider `local`: `whisper.cpp` debe estar compilado e instalado en el PATH del servidor (solo si se usa ese provider).
## Riesgos
| Riesgo | Mitigacion |
|--------|------------|
| OGG/Opus no soportado por Whisper API | Whisper API acepta OGG nativamente. Si cambiara, añadir conversion con `ffmpeg` como paso intermedio |
| Latencia de transcripcion (2-5s para 30s de audio) | Typing indicator mientras se procesa. El usuario ya espera latencia del LLM, la transcripcion añade poco overhead relativo |
| Precision de Whisper varia con ruido de fondo | El agente recibe el mejor texto posible y responde normalmente. Language hint en config mejora resultados para español |
| Audio muy largo satura memoria | Limite de 25 MB (hard, API) + `max_duration` configurable (soft, UX). Audio tipico de Element: <1 MB para 30 segundos |
| Costo de API ($0.006/minuto) | Configurable — el admin puede desactivar STT o usar provider `local` gratuito. Limite de duracion previene abusos |
| Archivo temporal para el SDK | Se escribe a `os.TempDir()`, se elimina con `defer os.Remove()`. Sin riesgo de leak si se maneja correctamente |
+282
View File
@@ -0,0 +1,282 @@
# 0041 — Videollamadas con agentes via LiveKit (Element Call)
**Estado:** pendiente
## Objetivo
Permitir que los agentes se unan a llamadas de voz y video iniciadas desde Element, participando como interlocutores conversacionales en tiempo real. El agente captura el audio de la llamada, lo transcribe en tiempo real (streaming STT), genera respuestas via LLM, y habla de vuelta usando TTS — creando una experiencia de IA interactiva por voz dentro de las llamadas de Element.
## Contexto
- Element usa LiveKit como backend para llamadas de voz/video via MatrixRTC (Element Call)
- El proyecto ya usa `github.com/sashabaranov/go-openai` que incluye soporte para Whisper (STT) y TTS APIs
- Existe un issue planificado (0040) para soporte de mensajes de voz con STT — este issue reutiliza la interfaz `Transcriber` definida alli
- LiveKit tiene un SDK oficial para Go: `github.com/livekit/server-sdk-go` para interaccion server-side con rooms y tracks de audio/video
- No existe actualmente ninguna forma de que los agentes participen en llamadas — solo responden a mensajes de texto
- El flujo MatrixRTC funciona asi: Element crea un estado MatrixRTC en la room Matrix (events tipo `m.call.member`), lo que genera una sesion en el servidor LiveKit donde los participantes se conectan via WebRTC
- Esta feature es compleja y multi-faceted — se recomienda implementar en sub-issues independientes
## Arquitectura
### Flujo principal
```
Usuario inicia llamada en Element (1:1 o grupo)
→ Element crea estado MatrixRTC en la room
→ Event m.call.member llega al listener del agente
→ Agent detecta llamada activa → obtiene credenciales LiveKit
→ shell/livekit/ conecta al LiveKit room como participante
→ Audio pipeline:
Audio track entrante → Buffer/VAD → STT (Transcriber)
→ Texto transcrito → LLM del agente → Respuesta texto
→ TTS (Synthesizer) → Audio track saliente → LiveKit room
→ Usuario escucha la respuesta del agente
→ Ciclo continua hasta que se cuelga la llamada
```
### Pure core / impure shell
```
pkg/tts/types.go → PURO: interfaz Synthesizer, tipos de audio
shell/livekit/client.go → IMPURO: conexion LiveKit, join/leave rooms
shell/livekit/audio.go → IMPURO: captura y publicacion de audio tracks
shell/livekit/pipeline.go → IMPURO: orquestacion STT → LLM → TTS
shell/tts/openai.go → IMPURO: cliente OpenAI TTS API
shell/matrix/listener.go → IMPURO (MOD): deteccion de eventos de llamada
internal/config/schema.go → PURO (MOD): tipos LiveKitCfg, TTSCfg
devagents/runtime.go → COMPOSICION (MOD): inicializar LiveKit, wiring
```
La logica pura se limita a tipos e interfaces. Todo el I/O real (LiveKit, STT, TTS, LLM) vive en `shell/`. Las reglas del agente no cambian — la decision de unirse a una llamada es un comportamiento del runtime, no de las reglas de decision.
### Archivos afectados
```
shell/livekit/ NEW — paquete LiveKit
shell/livekit/client.go NEW — cliente LiveKit: connect, join room, leave, lifecycle
shell/livekit/audio.go NEW — captura audio track entrante, publish audio track saliente
shell/livekit/pipeline.go NEW — orquestacion del pipeline STT → LLM → TTS
shell/tts/ NEW — paquete TTS
shell/tts/openai.go NEW — implementacion OpenAI TTS API (tts-1, voces)
pkg/tts/ NEW — tipos puros de TTS
pkg/tts/types.go NEW — interfaz Synthesizer, AudioFormat, VoiceConfig
shell/matrix/listener.go MOD — detectar eventos m.call.member / MatrixRTC
internal/config/schema.go MOD — anadir LiveKitCfg, TTSCfg al schema de config
devagents/runtime.go MOD — inicializar cliente LiveKit, conectar call handling
```
## Tareas
**Nota**: este es un feature multi-issue. Cada fase deberia convertirse en un sub-issue independiente (ver seccion "Desglose multi-issue" mas abajo).
### Fase 1 — Cliente LiveKit + deteccion de llamadas
- [ ] **1.1** Anadir dependencia `github.com/livekit/server-sdk-go` al modulo Go
- [ ] **1.2** Crear `shell/livekit/client.go`: conexion al servidor LiveKit, join room como participante, leave room, manejo de reconexion
- [ ] **1.3** Anadir `LiveKitCfg` a `internal/config/schema.go`: `ServerURL`, `APIKeyEnv`, `APISecretEnv`, `Enabled`, `AutoJoinCalls`
- [ ] **1.4** Modificar `shell/matrix/listener.go` para detectar eventos MatrixRTC (`m.call.member` state events) y notificar al runtime
- [ ] **1.5** Implementar auto-join: cuando se detecta una llamada activa en una room donde el agente esta presente, obtener token LiveKit y unirse como participante de audio
- [ ] **1.6** Tests: conexion y join de room con servidor LiveKit mock o de prueba
### Fase 2 — Captura de audio + STT
- [ ] **2.1** Implementar captura de audio track desde el LiveKit room participant en `shell/livekit/audio.go`
- [ ] **2.2** Buffer de chunks de audio para procesamiento STT (formato Opus → PCM si es necesario)
- [ ] **2.3** Integrar con STT del issue 0040 — reutilizar interfaz `Transcriber` para transcribir audio capturado
- [ ] **2.4** Implementar Voice Activity Detection (VAD) para detectar cuando el usuario deja de hablar (silencio > umbral configurable)
- [ ] **2.5** Tests: pipeline de captura de audio con datos de prueba
### Fase 3 — TTS
- [ ] **3.1** Definir `pkg/tts/types.go`: interfaz `Synthesizer` con `Synthesize(ctx context.Context, text string) ([]byte, error)`, tipos `AudioFormat`, `VoiceConfig`
- [ ] **3.2** Implementar `shell/tts/openai.go`: cliente OpenAI TTS API (modelo `tts-1`, voces: alloy, echo, fable, onyx, nova, shimmer)
- [ ] **3.3** Anadir `TTSCfg` a `internal/config/schema.go`: `Enabled`, `Provider`, `Model`, `Voice`, `Speed`, `APIKeyEnv`
- [ ] **3.4** Convertir output de TTS al formato que LiveKit espera (PCM/Opus) si es necesario
- [ ] **3.5** Publicar audio track con la voz sintetizada al LiveKit room
- [ ] **3.6** Tests: TTS con mock del API de OpenAI
### Fase 4 — Pipeline completo
- [ ] **4.1** Orquestar pipeline en `shell/livekit/pipeline.go`: audio entrante → STT → LLM → TTS → audio saliente
- [ ] **4.2** Manejar flujo conversacional: usuario habla → pausa (VAD) → agente responde → vuelve a escuchar
- [ ] **4.3** Manejo de interrupciones: si el usuario habla mientras el agente esta hablando, detener TTS y escuchar
- [ ] **4.4** Optimizacion de latencia: iniciar TTS conforme los tokens del LLM van llegando (streaming TTS)
- [ ] **4.5** Conectar pipeline al runtime del agente en `devagents/runtime.go`
- [ ] **4.6** Tests: pipeline end-to-end con mocks de STT, LLM y TTS
### Fase 5 — Polish y opcionales
- [ ] **5.1** Gestion del ciclo de vida de llamadas: join, active, hangup, error recovery, timeout por inactividad
- [ ] **5.2** Opcional: publicar video track con avatar/estado del agente (estatico o animado)
- [ ] **5.3** Indicadores en la room Matrix durante la llamada (typing indicators, mensajes de estado)
- [ ] **5.4** Documentacion de config y ejemplos en config de agentes de referencia
- [ ] **5.5** Verificacion de permisos: solo aceptar llamadas de usuarios autorizados (ACL check via `security/`)
## Desglose multi-issue
Este issue es demasiado grande para completarse en una sola rama corta. Se recomienda desglosar en los siguientes sub-issues, cada uno autocontenido, compilable y testeable:
| Sub-issue | Rama | Alcance | Fases cubiertas | Estado |
|-----------|------|---------|-----------------|--------|
| 0041a — LiveKit client + deteccion de llamadas | `issue/0041a-livekit-client` | Paquete `shell/livekit/`, config `LiveKitCfg`, deteccion de eventos MatrixRTC en listener, auto-join basico | Fase 1 | pendiente |
| 0041b — TTS package + publicacion de audio | `issue/0041b-tts-audio-publish` | Paquete `pkg/tts/`, `shell/tts/`, config `TTSCfg`, publicar audio track al LiveKit room | Fase 3 | pendiente |
| 0041c — Pipeline completo STT → LLM → TTS | `issue/0041c-call-pipeline` | Orquestacion en `shell/livekit/pipeline.go`, captura audio, VAD, integracion STT (issue 0040), flujo conversacional, wiring en runtime | Fases 2 y 4 | pendiente |
| 0041d — Polish, video track y lifecycle | `issue/0041d-call-polish` | Lifecycle management, interrupciones, video track opcional, indicadores, ACL, docs | Fase 5 | pendiente |
### Nota sobre feature flags
Se recomienda usar un feature flag `livekit-calls` en `dev/feature_flags.json` (desactivado) para las sub-issues 0041a-0041c. La sub-issue 0041d activa el flag y cierra el feature. Esto permite mergear codigo completo y testeado a master sin activar el comportamiento hasta que todo el pipeline este listo.
```json
{
"flags": {
"livekit-calls": {
"enabled": false,
"issue": "0041",
"description": "Agentes pueden unirse a llamadas de voz/video via LiveKit + MatrixRTC",
"added": "2026-04-09"
}
}
}
```
### Progreso por tarea
**Fase 1: Cliente LiveKit + deteccion** — sub-issue 0041a
- [ ] 1.1 Dependencia `livekit/server-sdk-go`
- [ ] 1.2 `shell/livekit/client.go`
- [ ] 1.3 `LiveKitCfg` en config schema
- [ ] 1.4 Deteccion MatrixRTC en listener
- [ ] 1.5 Auto-join a llamadas
- [ ] 1.6 Tests
**Fase 2: Captura audio + STT** — sub-issue 0041c
- [ ] 2.1 Captura audio track
- [ ] 2.2 Buffer audio chunks
- [ ] 2.3 Integracion STT (issue 0040)
- [ ] 2.4 Voice Activity Detection
- [ ] 2.5 Tests
**Fase 3: TTS** — sub-issue 0041b
- [ ] 3.1 `pkg/tts/types.go`
- [ ] 3.2 `shell/tts/openai.go`
- [ ] 3.3 `TTSCfg` en config schema
- [ ] 3.4 Conversion formato audio
- [ ] 3.5 Publicar audio track
- [ ] 3.6 Tests
**Fase 4: Pipeline completo** — sub-issue 0041c
- [ ] 4.1 Orquestacion pipeline
- [ ] 4.2 Flujo conversacional
- [ ] 4.3 Manejo de interrupciones
- [ ] 4.4 Optimizacion latencia (streaming TTS)
- [ ] 4.5 Wiring en runtime
- [ ] 4.6 Tests E2E del pipeline
**Fase 5: Polish** — sub-issue 0041d
- [ ] 5.1 Lifecycle management
- [ ] 5.2 Video track opcional
- [ ] 5.3 Indicadores en Matrix
- [ ] 5.4 Documentacion
- [ ] 5.5 ACL check
## Ejemplo de uso
### Llamada 1:1 con agente
```
1. Usuario abre DM con el agente en Element
2. Usuario hace clic en el boton "Call" (icono de telefono)
3. Element crea sesion MatrixRTC → LiveKit room
4. El agente detecta el evento m.call.member en la room
5. El agente se une a la llamada como participante de audio
6. Conversacion:
Usuario (hablando): "Hola, como estan los servidores?"
[VAD detecta fin de habla → STT transcribe → LLM procesa → TTS genera audio]
Agente (hablando): "Hola! Todos los servidores estan operativos.
El uso de CPU promedio es del 23% y hay 4.2 GB
de memoria disponible."
Usuario: "Y el servicio de base de datos?"
[Pipeline se repite]
Agente: "PostgreSQL esta corriendo normalmente. La ultima replica
se sincronizo hace 3 minutos sin errores."
7. Usuario cuelga la llamada
8. El agente detecta el hangup y se desconecta del LiveKit room
```
### Config del agente
```yaml
# agents/<agent-id>/config.yaml
livekit:
enabled: true
server_url: "wss://livekit.myserver.com"
api_key_env: LIVEKIT_API_KEY
api_secret_env: LIVEKIT_API_SECRET
auto_join_calls: true # unirse automaticamente cuando se detecta llamada
tts:
enabled: true
provider: openai # openai | elevenlabs | local
model: tts-1 # tts-1 (rapido) | tts-1-hd (calidad)
voice: nova # alloy, echo, fable, onyx, nova, shimmer
speed: 1.0 # 0.25 - 4.0
api_key_env: OPENAI_API_KEY # reutiliza la misma key del LLM
```
### Env vars nuevas
```bash
# .env
LIVEKIT_API_KEY="APIxxxxxxxx"
LIVEKIT_API_SECRET="secretxxxxxxxx"
# OPENAI_API_KEY ya existe — se reutiliza para TTS
```
## Decisiones de diseno
1. **LiveKit server-sdk-go**: SDK oficial de LiveKit para Go, permite integracion nativa sin bridges ni proxies. El agente se conecta como participante server-side al LiveKit room.
2. **OpenAI TTS como provider primario**: consistente con la dependencia existente de `github.com/sashabaranov/go-openai`. El modelo `tts-1` ofrece buen balance entre calidad y latencia (~1s). Se puede extender a ElevenLabs o TTS local en el futuro.
3. **MVP solo audio, video opcional**: la interaccion por voz es el valor principal. El video track (avatar, estado) es un nice-to-have que se puede agregar despues sin cambiar la arquitectura.
4. **Reutilizar interfaz Transcriber del issue 0040**: evita duplicar logica de STT. El issue 0040 define la interfaz y la implementacion; este issue la consume para el pipeline de llamadas.
5. **Voice Activity Detection (VAD)**: critico para saber cuando el usuario termina de hablar. Sin VAD, el agente no sabe cuando empezar a procesar. Se puede empezar con un umbral simple de silencio (ej: 1.5s sin audio) y mejorar despues con VAD basado en WebRTC o silero-vad.
6. **Considerar OpenAI Realtime API como optimizacion futura**: la Realtime API de OpenAI permite audio-in → audio-out directamente, eliminando la necesidad de STT y TTS separados. Reduciria la latencia significativamente (~500ms vs ~4s). Sin embargo, introduce acoplamiento fuerte con OpenAI y no permite usar otros LLMs. Se deja como optimizacion futura.
7. **Feature flag para merge incremental**: dado que son 4+ sub-issues, cada uno mergea codigo funcional y testeado a master protegido por el flag `livekit-calls`. Esto sigue el patron TBD del proyecto y evita ramas largas.
## Prerequisitos
- **Issue 0040 (STT) completado**: este issue depende de la interfaz `Transcriber` y la implementacion de STT para transcribir el audio de la llamada
- **Servidor LiveKit desplegado**: se necesita un servidor LiveKit accesible (self-hosted via `livekit-server` o LiveKit Cloud), configurado para funcionar con el homeserver Matrix
- **Integracion MatrixRTC en el homeserver**: el homeserver Synapse necesita estar configurado para MatrixRTC/LiveKit (configuracion de SFU en `.well-known` o en el config de Synapse)
- **Element Web/Desktop con soporte de Element Call**: las versiones recientes de Element incluyen Element Call integrado
## Seguridad
- **Credenciales LiveKit via env vars**: `LIVEKIT_API_KEY` y `LIVEKIT_API_SECRET` nunca se hardcodean, se cargan desde `.env` via `api_key_env`/`api_secret_env`
- **Solo aceptar llamadas de usuarios autorizados**: verificar permisos del usuario que inicia la llamada contra las ACLs del agente (`security/permissions.yaml`) antes de unirse
- **Audio procesado en memoria, no persistido**: el audio de la llamada se procesa en streaming y no se guarda en disco. Los buffers se liberan despues de la transcripcion
- **Llamadas TTS/STT via HTTPS**: todas las llamadas a APIs externas (OpenAI Whisper, OpenAI TTS) usan HTTPS
- **Timeout por inactividad**: si no se detecta audio por un periodo configurable (ej: 5 minutos), el agente se desconecta automaticamente para liberar recursos
- **Rate limiting**: aplicar rate limiting a las llamadas por usuario/room para prevenir abuso de recursos (STT/TTS tienen costo por uso)
## Riesgos
| Riesgo | Probabilidad | Impacto | Mitigacion |
|--------|-------------|---------|------------|
| MatrixRTC spec en evolucion — la integracion LiveKit/Matrix puede cambiar entre versiones | Alta | Alto | Fijar versiones de Element y livekit-server; encapsular la deteccion de eventos en una capa de abstraccion que se pueda actualizar sin reescribir el pipeline |
| Latencia total del pipeline: STT (~1s) + LLM (~2s) + TTS (~1s) = ~4s minimo de respuesta | Alta | Medio | Aceptable para MVP; optimizar con streaming TTS (iniciar antes de completar la respuesta LLM); considerar OpenAI Realtime API como mejora futura |
| Codec Opus: conversion entre formato LiveKit (Opus/WebRTC) y APIs de STT/TTS (PCM/MP3) | Media | Medio | Usar librerias Go para decode Opus → PCM (`gopkg.in/hraban/opus.v2` o `pion/opus`); puede requerir CGO dependiendo de la libreria |
| Hosting y costo del servidor LiveKit | Media | Medio | LiveKit se puede self-host (binario unico); el costo de APIs de STT/TTS es proporcional al uso. Documentar estimaciones de costo |
| Compatibilidad Element Web vs Mobile vs Desktop | Media | Bajo | Element Call funciona diferente en cada plataforma. Priorizar Element Web/Desktop que usan MatrixRTC directamente; mobile puede tener limitaciones |
| CGO dependency para codec Opus | Media | Medio | El proyecto usa `CGO_ENABLED=0`. Si las librerias Opus requieren CGO, evaluar alternativas pure-Go o pre-compilar bindings. `pion/opus` ofrece decode pure-Go |
| LiveKit server-sdk-go compatibility con Go 1.23.5 | Baja | Bajo | Verificar compatibilidad antes de empezar; el SDK de LiveKit suele soportar versiones recientes de Go |
+6
View File
@@ -46,3 +46,9 @@ afectados y notas de implementacion.
| 33 | Comandos de robots sin prefijo ! | [0033-bot-commands-no-prefix.md](completed/0033-bot-commands-no-prefix.md) | completado |
| 34 | E2E: verificar skill /create-bot | [0034-e2e-create-bot-skill.md](completed/0034-e2e-create-bot-skill.md) | completado |
| 35 | Audit trail + comando !metrics | [0035-audit-trail-metrics.md](completed/0035-audit-trail-metrics.md) | completado |
| 36 | Claude Code streaming de progreso | [0036-claude-code-streaming.md](0036-claude-code-streaming.md) | pendiente |
| 37 | Agente que crea otros agentes via Matrix | [0037-agent-creator-bot.md](0037-agent-creator-bot.md) | pendiente |
| 38 | Webapps y dashboards embebidos en Element via widgets | [0038-element-widgets-dashboard.md](0038-element-widgets-dashboard.md) | pendiente |
| 39 | Recordatorios dinamicos y crons que invocan agentes | [0039-dynamic-reminders-cron.md](0039-dynamic-reminders-cron.md) | pendiente |
| 40 | Soporte para mensajes de voz (STT) | [0040-voice-messages-stt.md](0040-voice-messages-stt.md) | pendiente |
| 41 | Videollamadas con agentes via LiveKit | [0041-livekit-videocall.md](0041-livekit-videocall.md) | pendiente |
View File