From 52d5632d892b982c271c743ac75e55803f417ff4 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 21:19:09 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20crear=20issues=200036-0041=20?= =?UTF-8?q?=E2=80=94=20nuevas=20features=20del=20sistema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dev/issues/0036-claude-code-streaming.md | 248 +++++++++++++++ dev/issues/0037-agent-creator-bot.md | 239 ++++++++++++++ dev/issues/0038-element-widgets-dashboard.md | 296 ++++++++++++++++++ dev/issues/0039-dynamic-reminders-cron.md | 264 ++++++++++++++++ dev/issues/0040-voice-messages-stt.md | 222 +++++++++++++ dev/issues/0041-livekit-videocall.md | 282 +++++++++++++++++ dev/issues/README.md | 6 + {agents => devagents}/commands.go | 0 .../commands_metrics_test.go | 0 {agents => devagents}/handler.go | 0 {agents => devagents}/lifecycle_test.go | 0 {agents => devagents}/llm.go | 0 {agents => devagents}/memory.go | 0 {agents => devagents}/registry.go | 0 {agents => devagents}/registry_build.go | 0 {agents => devagents}/registry_build_test.go | 0 {agents => devagents}/registry_test.go | 0 {agents => devagents}/robot.go | 0 {agents => devagents}/robot_test.go | 0 {agents => devagents}/runtime.go | 0 {agents => devagents}/runtime_test.go | 0 {agents => devagents}/types.go | 0 22 files changed, 1557 insertions(+) create mode 100644 dev/issues/0036-claude-code-streaming.md create mode 100644 dev/issues/0037-agent-creator-bot.md create mode 100644 dev/issues/0038-element-widgets-dashboard.md create mode 100644 dev/issues/0039-dynamic-reminders-cron.md create mode 100644 dev/issues/0040-voice-messages-stt.md create mode 100644 dev/issues/0041-livekit-videocall.md rename {agents => devagents}/commands.go (100%) rename {agents => devagents}/commands_metrics_test.go (100%) rename {agents => devagents}/handler.go (100%) rename {agents => devagents}/lifecycle_test.go (100%) rename {agents => devagents}/llm.go (100%) rename {agents => devagents}/memory.go (100%) rename {agents => devagents}/registry.go (100%) rename {agents => devagents}/registry_build.go (100%) rename {agents => devagents}/registry_build_test.go (100%) rename {agents => devagents}/registry_test.go (100%) rename {agents => devagents}/robot.go (100%) rename {agents => devagents}/robot_test.go (100%) rename {agents => devagents}/runtime.go (100%) rename {agents => devagents}/runtime_test.go (100%) rename {agents => devagents}/types.go (100%) 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 From bd0c8c0dd3ea1ac31325a86e48e2008172b652a3 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Thu, 9 Apr 2026 21:19:25 +0000 Subject: [PATCH 2/2] refactor: mover runtime Go de agents/ a devagents/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agents/ ahora solo contiene carpetas de agentes (config, reglas, prompts). El runtime (Agent, Robot, Runner, registry, handler, commands, llm, memory) vive en devagents/ como package devagents. Cambios: - git mv agents/*.go → devagents/*.go - package agents → package devagents en todos los archivos movidos - Actualizar imports en agents/*/agent.go, cmd/launcher/, dev-scripts/ - Actualizar docs: CLAUDE.md, rules/, docs/e2ee.md, issues pendientes Build y tests pasan sin errores. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 15 ++++++++------- .claude/rules/create_agent.md | 12 ++++++------ .claude/rules/create_command.md | 6 +++--- .claude/rules/create_tool.md | 2 +- .claude/rules/index.md | 3 ++- agents/_template/agent.go | 4 ++-- agents/asistente-2/agent.go | 4 ++-- agents/assistant-bot/agent.go | 4 ++-- agents/meteorologo/agent.go | 4 ++-- agents/test-bot/agent.go | 4 ++-- agents/test-personality/agent.go | 4 ++-- cmd/launcher/main.go | 10 +++++----- cmd/launcher/registry.go | 10 +++++----- dev-scripts/agent/convert-to-robot.sh | 4 ++-- devagents/commands.go | 2 +- devagents/commands_metrics_test.go | 2 +- devagents/handler.go | 2 +- devagents/lifecycle_test.go | 2 +- devagents/llm.go | 2 +- devagents/memory.go | 2 +- devagents/registry.go | 2 +- devagents/registry_build.go | 2 +- devagents/registry_build_test.go | 2 +- devagents/registry_test.go | 2 +- devagents/robot.go | 2 +- devagents/robot_test.go | 2 +- devagents/runtime.go | 2 +- devagents/runtime_test.go | 2 +- devagents/types.go | 2 +- docs/e2ee.md | 4 ++-- 30 files changed, 61 insertions(+), 59 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ced47d8..855a51f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -9,10 +9,11 @@ Monorepo Go para bots Matrix autonomos. Modulo: `github.com/enmanuel/agents`. ### 1. Functional PRogramming: Pure core / Impure shell ``` -pkg/ → PURO: tipos, funciones puras, cero side effects -shell/ → IMPURO: todo I/O (Matrix, LLM, SSH, filesystem) -agents/ → composicion: reglas puras + ensamblado con shell -tools/ → Def (puro) + Exec (impuro) +pkg/ → PURO: tipos, funciones puras, cero side effects +shell/ → IMPURO: todo I/O (Matrix, LLM, SSH, filesystem) +devagents/ → runtime: Agent/Robot ensambla core + shell +agents/ → reglas puras por agente + config.yaml + prompts +tools/ → Def (puro) + Exec (impuro) ``` **Nunca** side effects en `pkg/`. El core produce `[]decision.Action` (datos puros), el shell los interpreta. @@ -56,9 +57,9 @@ shell/mcp/ cliente y servidor MCP (Model Context Protocol) shell/skills/ loader (filesystem) + executor (scripts) shell/effects/ Runner: []Action → side effects shell/bus/ comunicacion inter-agente -agents/types.go Runner interface (comun a Agent y Robot) -agents/runtime.go Agent{}: ensambla core + shell (runtime completo con LLM) -agents/robot.go Robot{}: runtime ligero command-only (sin LLM, reglas, memoria) +devagents/types.go Runner interface (comun a Agent y Robot) +devagents/runtime.go Agent{}: ensambla core + shell (runtime completo con LLM) +devagents/robot.go Robot{}: runtime ligero command-only (sin LLM, reglas, memoria) agents// agent.go (reglas puras) + config.yaml + prompts/system.md tools/ tool registry + tool implementations (subpackages) tools/mcptools/ bridge: convierte MCP tools → tools.Tool diff --git a/.claude/rules/create_agent.md b/.claude/rules/create_agent.md index 6c713e0..592c8b0 100644 --- a/.claude/rules/create_agent.md +++ b/.claude/rules/create_agent.md @@ -7,7 +7,7 @@ Guia ejecutable para Claude. Seguir paso a paso sin desviarse. | | Agent | Robot | |---|---|---| | **Cuando usar** | Necesita LLM, reglas, memoria, tools | Solo responde comandos (!xxx) | -| **Runtime** | `agents.New()` — completo | `agents.NewRobot()` — ligero | +| **Runtime** | `devagents.New()` — completo | `devagents.NewRobot()` — ligero | | **Config type** | `type: agent` (default) | `type: robot` | | **LLM** | Si | No | | **Reglas** | Si (`agent.go` con `Rules()`) | No (sin `agent.go`) | @@ -57,12 +57,12 @@ Template base (generado por el scaffold): package // sin guiones: "monitor-bot" → package monitor (strip hyphens, strip _bot) import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("", Rules) + devdevagents.Register("", Rules) } func Rules() []decision.Rule { @@ -84,7 +84,7 @@ func Rules() []decision.Rule { **Reglas estrictas:** - **PURO**: solo imports de `pkg/decision` y `agents` (para Register), cero I/O, cero side effects -- **Auto-registro**: cada agente se registra via `init()` con `agents.Register("", Rules)` +- **Auto-registro**: cada agente se registra via `init()` con `devagents.Register("", Rules)` - Package name = ID sin guiones ni `_bot` (e.g. `monitor-bot` → `package monitor`) - **No usar reglas para comandos** (`!help`, `!ping`, etc.) — los comandos se gestionan via `RegisterCommand` (ver policy `create_command.md`) - Las reglas solo aplican a mensajes normales (sin prefijo `!`) @@ -174,7 +174,7 @@ _ "github.com/enmanuel/agents/agents/" Las reglas se registran automáticamente via `init()` en el paquete del agente. No se necesita editar ningún map ni registry manualmente. -**El ID en `agents.Register()` DEBE coincidir exactamente con `agent.id` en config.yaml.** +**El ID en `devagents.Register()` DEBE coincidir exactamente con `agent.id` en config.yaml.** ## Convención de env vars — REGLA CRÍTICA @@ -228,7 +228,7 @@ tail -f run/launcher.log - **Nunca** side effects en `agent.go` - **Siempre** compilar con `-tags goolm` -- **Siempre** que `agent.id` coincida entre config.yaml, `agents.Register()` y directorio +- **Siempre** que `agent.id` coincida entre config.yaml, `devagents.Register()` y directorio - **No** crear `data/` manualmente — se auto-genera - **No** commitear tokens ni passwords - **No** compartir crypto stores entre agentes diff --git a/.claude/rules/create_command.md b/.claude/rules/create_command.md index 1d79d8c..1f22063 100644 --- a/.claude/rules/create_command.md +++ b/.claude/rules/create_command.md @@ -28,7 +28,7 @@ Todos los agentes los tienen automáticamente: `!help`, `!ping`, `!tools`, `!too ### Agent-specific (por agente) -Se registran con `agent.RegisterCommand(spec, handler)` en el launcher, después de `agents.New()` y antes de `agent.Run()`. +Se registran con `agent.RegisterCommand(spec, handler)` en el launcher, después de `devagents.New()` y antes de `agent.Run()`. ## Pasos para crear un comando de agente @@ -79,10 +79,10 @@ type CommandEntry struct { ### 2. Registrar en el launcher (`cmd/launcher/main.go`) -Después de `agents.New()` y antes de `wg.Add(1)`: +Después de `devagents.New()` y antes de `wg.Add(1)`: ```go -a, err := agents.New(cfg, rules, agentLogger) +a, err := devagents.New(cfg, rules, agentLogger) if err != nil { ... } // Register agent-specific commands diff --git a/.claude/rules/create_tool.md b/.claude/rules/create_tool.md index 0f32b4b..8bfe4af 100644 --- a/.claude/rules/create_tool.md +++ b/.claude/rules/create_tool.md @@ -41,7 +41,7 @@ func NewMiTool(/* deps */) Tool { } ``` -### 2. Registrar en `agents/runtime.go` → `buildToolRegistry()` +### 2. Registrar en `devagents/registry_build.go` → `buildToolRegistry()` ```go if /* condición basada en config */ { diff --git a/.claude/rules/index.md b/.claude/rules/index.md index 5ccb58f..40107bc 100644 --- a/.claude/rules/index.md +++ b/.claude/rules/index.md @@ -66,5 +66,6 @@ Filosofia completa documentada en `CLAUDE.md` seccion "Trunk-based development". Todas las reglas respetan el patron **pure core / impure shell**: - `pkg/` es puro — nunca añadir side effects - `shell/` es impuro — todo I/O va aqui -- `agents/` compone ambos — reglas puras + ensamblado con shell +- `devagents/` runtime: Agent/Robot ensambla core + shell +- `agents/` reglas puras por agente + config + prompts - `tools/` sigue el mismo patron: `Def` (datos puros) + `Exec` (funcion impura) diff --git a/agents/_template/agent.go b/agents/_template/agent.go index 1f6ee40..4de3261 100644 --- a/agents/_template/agent.go +++ b/agents/_template/agent.go @@ -4,12 +4,12 @@ package _template import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("AGENT_ID_PLACEHOLDER", Rules) + devagents.Register("AGENT_ID_PLACEHOLDER", Rules) } // Rules devuelve las reglas de este agente (vacio para el template). diff --git a/agents/asistente-2/agent.go b/agents/asistente-2/agent.go index 75547c8..6118fac 100644 --- a/agents/asistente-2/agent.go +++ b/agents/asistente-2/agent.go @@ -3,12 +3,12 @@ package asistente2 import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("asistente-2", Rules) + devagents.Register("asistente-2", Rules) } // Rules returns the decision rules for the asistente-2 bot. diff --git a/agents/assistant-bot/agent.go b/agents/assistant-bot/agent.go index aff28d7..4d9374f 100644 --- a/agents/assistant-bot/agent.go +++ b/agents/assistant-bot/agent.go @@ -3,12 +3,12 @@ package assistant import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("assistant-bot", Rules) + devagents.Register("assistant-bot", Rules) } // Rules returns the decision rules for the assistant bot. diff --git a/agents/meteorologo/agent.go b/agents/meteorologo/agent.go index d221ed6..8cf8dc9 100644 --- a/agents/meteorologo/agent.go +++ b/agents/meteorologo/agent.go @@ -3,12 +3,12 @@ package meteorologo import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("meteorologo", Rules) + devagents.Register("meteorologo", Rules) } // Rules returns the decision rules for the meteorologo bot. diff --git a/agents/test-bot/agent.go b/agents/test-bot/agent.go index eaa2b79..ed12619 100644 --- a/agents/test-bot/agent.go +++ b/agents/test-bot/agent.go @@ -4,12 +4,12 @@ package test import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("test-bot", Rules) + devagents.Register("test-bot", Rules) } // Rules devuelve las reglas de este agente (vacio para el template). diff --git a/agents/test-personality/agent.go b/agents/test-personality/agent.go index cf020cc..84b98eb 100644 --- a/agents/test-personality/agent.go +++ b/agents/test-personality/agent.go @@ -2,12 +2,12 @@ package test_personality import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("test-personality", Rules) + devagents.Register("test-personality", Rules) } // Rules routes all DMs and mentions to the LLM. diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index b7f55a3..7053954 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" @@ -173,10 +173,10 @@ func main() { } // Branch: robot (command-only, lightweight) vs agent (full runtime). - var runner agents.Runner + var runner devagents.Runner if cfg.Agent.Type == "robot" { - robot, rErr := agents.NewRobot(cfg, agentLogger) + robot, rErr := devagents.NewRobot(cfg, agentLogger) if rErr != nil { logger.Error("failed to create robot", "id", cfg.Agent.ID, "err", rErr) agentCleanup() @@ -201,7 +201,7 @@ func main() { "acl_empty", agentACL.Empty(), ) - a, cErr := agents.New(cfg, rules, agentACL, agentLogger, agents.WithLogDir(logDir)) + a, cErr := devagents.New(cfg, rules, agentACL, agentLogger, devagents.WithLogDir(logDir)) if cErr != nil { logger.Error("failed to create agent", "id", cfg.Agent.ID, "err", cErr) agentCleanup() @@ -312,7 +312,7 @@ func startOrchestrator(agentBus *bus.Bus, logger *slog.Logger) (*orchHandle, err // global registry (populated by init() in each agent package). // Returns nil if no rules are registered (command-only bot). func rulesFor(agentID string, logger *slog.Logger) []decision.Rule { - factory := agents.GetRules(agentID) + factory := devagents.GetRules(agentID) if factory == nil { logger.Warn("no rules registered for agent, using empty ruleset (command-only)", "id", agentID) return nil diff --git a/cmd/launcher/registry.go b/cmd/launcher/registry.go index ce23d14..dfbb3a5 100644 --- a/cmd/launcher/registry.go +++ b/cmd/launcher/registry.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/pkg/orchestration" @@ -19,7 +19,7 @@ import ( // runningAgent holds a live runner (Agent or Robot) and the metadata needed to recreate it. type runningAgent struct { - runner agents.Runner + runner devagents.Runner cfg *config.AgentConfig cfgPath string logger *slog.Logger @@ -139,10 +139,10 @@ func (r *agentRegistry) reload(id string, rulesFor func(string, *slog.Logger) [] } // 5. Create new runner (validates config before discarding the old one). - var newRunner agents.Runner + var newRunner devagents.Runner if cfg.Agent.Type == "robot" { - robot, rErr := agents.NewRobot(cfg, newLogger) + robot, rErr := devagents.NewRobot(cfg, newLogger) if rErr != nil { newLogger.Error("reload: failed to create robot", "id", id, "err", rErr) newCleanup() @@ -153,7 +153,7 @@ func (r *agentRegistry) reload(id string, rulesFor func(string, *slog.Logger) [] rules := rulesFor(cfg.Agent.ID, newLogger) agentACL := pksecurity.ResolveACL(cfg.Agent.ID, r.deps.secPolicy) newLogger.Debug("resolved acl for agent (reload)", "agent", cfg.Agent.ID, "acl_empty", agentACL.Empty()) - newAgent, aErr := agents.New(cfg, rules, agentACL, newLogger) + newAgent, aErr := devagents.New(cfg, rules, agentACL, newLogger) if aErr != nil { newLogger.Error("reload: failed to create agent", "id", id, "err", aErr) newCleanup() diff --git a/dev-scripts/agent/convert-to-robot.sh b/dev-scripts/agent/convert-to-robot.sh index 7a21ac2..f28bd10 100755 --- a/dev-scripts/agent/convert-to-robot.sh +++ b/dev-scripts/agent/convert-to-robot.sh @@ -106,12 +106,12 @@ cat > "$DIR/agent.go" << GO package ${PACKAGE} import ( - "github.com/enmanuel/agents/agents" + "github.com/enmanuel/agents/devagents" "github.com/enmanuel/agents/pkg/decision" ) func init() { - agents.Register("${ID}", Rules) + devagents.Register("${ID}", Rules) } // Rules returns nil — robots only respond to commands. diff --git a/devagents/commands.go b/devagents/commands.go index 12e0fb6..80a52f0 100644 --- a/devagents/commands.go +++ b/devagents/commands.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/commands_metrics_test.go b/devagents/commands_metrics_test.go index 16cce03..85aaf04 100644 --- a/devagents/commands_metrics_test.go +++ b/devagents/commands_metrics_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/handler.go b/devagents/handler.go index 3ed43be..8e4d638 100644 --- a/devagents/handler.go +++ b/devagents/handler.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/lifecycle_test.go b/devagents/lifecycle_test.go index 93c71b2..5b5fe66 100644 --- a/devagents/lifecycle_test.go +++ b/devagents/lifecycle_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/llm.go b/devagents/llm.go index 073261f..f1ebc27 100644 --- a/devagents/llm.go +++ b/devagents/llm.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/memory.go b/devagents/memory.go index b5cd058..a4004ea 100644 --- a/devagents/memory.go +++ b/devagents/memory.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/registry.go b/devagents/registry.go index c941a58..260fb60 100644 --- a/devagents/registry.go +++ b/devagents/registry.go @@ -3,7 +3,7 @@ // Each agent package self-registers via init() using Register. // The launcher retrieves rules via GetRules without importing agent // packages explicitly (only blank imports are needed). -package agents +package devagents import ( "sync" diff --git a/devagents/registry_build.go b/devagents/registry_build.go index 2fcb14c..6dc2c9c 100644 --- a/devagents/registry_build.go +++ b/devagents/registry_build.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/registry_build_test.go b/devagents/registry_build_test.go index 5a3c9a5..f186734 100644 --- a/devagents/registry_build_test.go +++ b/devagents/registry_build_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "log/slog" diff --git a/devagents/registry_test.go b/devagents/registry_test.go index 5e71f80..f16e8f2 100644 --- a/devagents/registry_test.go +++ b/devagents/registry_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "sort" diff --git a/devagents/robot.go b/devagents/robot.go index bcf8dcf..0e9c7da 100644 --- a/devagents/robot.go +++ b/devagents/robot.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/robot_test.go b/devagents/robot_test.go index 9e8236d..500364d 100644 --- a/devagents/robot_test.go +++ b/devagents/robot_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/runtime.go b/devagents/runtime.go index 94f844b..bc2ebbc 100644 --- a/devagents/runtime.go +++ b/devagents/runtime.go @@ -1,5 +1,5 @@ // Package agents defines the Agent runtime that ties core and shell together. -package agents +package devagents import ( "context" diff --git a/devagents/runtime_test.go b/devagents/runtime_test.go index 16d0487..e4d152f 100644 --- a/devagents/runtime_test.go +++ b/devagents/runtime_test.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/devagents/types.go b/devagents/types.go index 84dd4fd..c420fa7 100644 --- a/devagents/types.go +++ b/devagents/types.go @@ -1,4 +1,4 @@ -package agents +package devagents import ( "context" diff --git a/docs/e2ee.md b/docs/e2ee.md index 57a96c5..d01cdd6 100644 --- a/docs/e2ee.md +++ b/docs/e2ee.md @@ -10,7 +10,7 @@ La implementación usa Olm puro en Go (`-tags goolm`, sin CGO). ``` config.yaml (encryption section) ↓ -agents/runtime.go → Agent.New() llama a InitCrypto() +devagents/runtime.go → Agent.New() llama a InitCrypto() ↓ shell/matrix/client.go → InitCrypto() configura cryptohelper ↓ @@ -144,7 +144,7 @@ Los nuevos mensajes se descifrarán normalmente tras regenerar las claves. | Archivo | Propósito | |---------|-----------| -| `agents/runtime.go` | Inicializa E2EE por agente | +| `devagents/runtime.go` | Inicializa E2EE por agente | | `shell/matrix/client.go` | `InitCrypto()` — setup de cryptohelper | | `cmd/verify/main.go` | Herramienta de cross-signing | | `cmd/launcher/sqlite.go` | Registro driver SQLite |