diff --git a/dev/issues/0036-claude-code-streaming.md b/dev/issues/0036-claude-code-streaming.md new file mode 100644 index 0000000..c8ada39 --- /dev/null +++ b/dev/issues/0036-claude-code-streaming.md @@ -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: ` + - 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) | diff --git a/dev/issues/0037-agent-creator-bot.md b/dev/issues/0037-agent-creator-bot.md new file mode 100644 index 0000000..5d35f70 --- /dev/null +++ b/dev/issues/0037-agent-creator-bot.md @@ -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 "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 "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_` +- [ ] **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 | diff --git a/dev/issues/0038-element-widgets-dashboard.md b/dev/issues/0038-element-widgets-dashboard.md new file mode 100644 index 0000000..c977570 --- /dev/null +++ b/dev/issues/0038-element-widgets-dashboard.md @@ -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. | diff --git a/dev/issues/0039-dynamic-reminders-cron.md b/dev/issues/0039-dynamic-reminders-cron.md new file mode 100644 index 0000000..883e754 --- /dev/null +++ b/dev/issues/0039-dynamic-reminders-cron.md @@ -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: `" @ Recordatorio: "` + - 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. | diff --git a/dev/issues/0040-voice-messages-stt.md b/dev/issues/0040-voice-messages-stt.md new file mode 100644 index 0000000..5806da4 --- /dev/null +++ b/dev/issues/0040-voice-messages-stt.md @@ -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 ` + - 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 | diff --git a/dev/issues/0041-livekit-videocall.md b/dev/issues/0041-livekit-videocall.md new file mode 100644 index 0000000..09686aa --- /dev/null +++ b/dev/issues/0041-livekit-videocall.md @@ -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//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 | diff --git a/dev/issues/README.md b/dev/issues/README.md index 2398f58..7f57c3a 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -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 | diff --git a/agents/commands.go b/devagents/commands.go similarity index 100% rename from agents/commands.go rename to devagents/commands.go diff --git a/agents/commands_metrics_test.go b/devagents/commands_metrics_test.go similarity index 100% rename from agents/commands_metrics_test.go rename to devagents/commands_metrics_test.go diff --git a/agents/handler.go b/devagents/handler.go similarity index 100% rename from agents/handler.go rename to devagents/handler.go diff --git a/agents/lifecycle_test.go b/devagents/lifecycle_test.go similarity index 100% rename from agents/lifecycle_test.go rename to devagents/lifecycle_test.go diff --git a/agents/llm.go b/devagents/llm.go similarity index 100% rename from agents/llm.go rename to devagents/llm.go diff --git a/agents/memory.go b/devagents/memory.go similarity index 100% rename from agents/memory.go rename to devagents/memory.go diff --git a/agents/registry.go b/devagents/registry.go similarity index 100% rename from agents/registry.go rename to devagents/registry.go diff --git a/agents/registry_build.go b/devagents/registry_build.go similarity index 100% rename from agents/registry_build.go rename to devagents/registry_build.go diff --git a/agents/registry_build_test.go b/devagents/registry_build_test.go similarity index 100% rename from agents/registry_build_test.go rename to devagents/registry_build_test.go diff --git a/agents/registry_test.go b/devagents/registry_test.go similarity index 100% rename from agents/registry_test.go rename to devagents/registry_test.go diff --git a/agents/robot.go b/devagents/robot.go similarity index 100% rename from agents/robot.go rename to devagents/robot.go diff --git a/agents/robot_test.go b/devagents/robot_test.go similarity index 100% rename from agents/robot_test.go rename to devagents/robot_test.go diff --git a/agents/runtime.go b/devagents/runtime.go similarity index 100% rename from agents/runtime.go rename to devagents/runtime.go diff --git a/agents/runtime_test.go b/devagents/runtime_test.go similarity index 100% rename from agents/runtime_test.go rename to devagents/runtime_test.go diff --git a/agents/types.go b/devagents/types.go similarity index 100% rename from agents/types.go rename to devagents/types.go