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:
@@ -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) |
|
||||
@@ -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. |
|
||||
@@ -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. |
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user