Files
agents_and_robots/dev/issues/0039-dynamic-reminders-cron.md
T
egutierrez 52d5632d89 docs: crear issues 0036-0041 — nuevas features del sistema
Issues planificados:
- 0036: Claude Code streaming de progreso en Matrix
- 0037: Agente que crea otros agentes/bots via Matrix
- 0038: Webapps y dashboards embebidos en Element via widgets
- 0039: Recordatorios dinámicos y crons que invocan agentes
- 0040: Soporte para mensajes de voz (audio → STT)
- 0041: Videollamadas con agentes via LiveKit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:19:09 +00:00

265 lines
13 KiB
Markdown

# 0039 — Recordatorios dinamicos y crons que invocan agentes
**Estado:** pendiente
## Objetivo
Extender el sistema cron para soportar (1) recordatorios dinamicos creados en runtime via tool calls del LLM ("recuerdame a las 3pm que...") y (2) un nuevo action kind `agent_call` que invoca a otro agente con un prompt, habilitando workflows automatizados agente-a-agente en horario.
## Contexto
- El scheduler actual (`shell/cron/scheduler.go`) soporta `send_message` y `llm_prompt` como action kinds, configurados estaticamente via YAML.
- No existe forma de crear schedules en runtime: si un usuario pide "recuerdame X", el agente no puede programar un disparo futuro sin editar config.
- El bus inter-agente (`shell/bus/`) ya permite comunicacion entre agentes via `SendAndWait`, pero no esta integrado con el cron.
- `ScheduledAction` en `internal/config/schema.go` define los campos para `send_message` y `llm_prompt` pero no tiene campos para invocacion de agentes.
- Las tools existentes siguen el patron subpackage en `tools/` (ej: `tools/file/`, `tools/ssh/`, `tools/clock/`).
- SQLite ya esta disponible via modernc (pure-Go, CGO_ENABLED=0) con el shim en `cmd/launcher/sqlite.go`.
## Arquitectura
### Fase 1 — Tipos puros y storage de reminders
```
pkg/reminder/types.go NEW — tipo Reminder puro (datos, sin I/O)
shell/reminder/store.go NEW — SQLite-backed store (Create, Delete, List, MarkFired, LoadActive)
shell/reminder/store_test.go NEW — tests CRUD del store
```
**Pure core / impure shell:**
- `pkg/reminder/` es 100% puro: solo define el struct `Reminder` y constantes. Sin imports de I/O.
- `shell/reminder/` es impuro: abre conexion SQLite, lee/escribe en disco.
### Fase 2 — Tools de recordatorios
```
tools/reminder/reminder.go NEW — create_reminder, delete_reminder, list_reminders
tools/reminder/reminder_test.go NEW — tests de validacion de params y parsing de tiempo
devagents/runtime.go MOD — registrar reminder tools cuando config lo habilita
```
### Fase 3 — Scheduler dinamico
```
shell/cron/scheduler.go MOD — AddSchedule, RemoveSchedule, soporte para IDs dinamicos
shell/cron/actions.go MOD — nuevo action kind "reminder" (mensaje personalizado a room/usuario)
shell/cron/scheduler_test.go MOD — tests de add/remove dinamico y one-shot auto-cleanup
```
### Fase 4 — Agent-to-agent cron calls
```
internal/config/schema.go MOD — campos AgentCall en ScheduledAction
shell/cron/actions.go MOD — nuevo action kind "agent_call" usando shell/bus/
shell/cron/scheduler.go MOD — inyeccion del bus como dependencia
```
## Tareas
### Fase 1 — Tipos puros y storage de reminders
- [ ] **1.1** Crear `pkg/reminder/types.go` con struct `Reminder`:
- Campos: `ID string`, `UserID string`, `RoomID string`, `Message string`, `CronExpr string`, `OneShot bool`, `CreatedAt time.Time`, `FiredAt *time.Time`
- Constantes para estados: `StatusActive`, `StatusFired`, `StatusDeleted`
- Sin imports de I/O, sin side effects
- [ ] **1.2** Crear `shell/reminder/store.go` con `Store` struct:
- Constructor `New(dbPath string) (*Store, error)` — abre SQLite, crea tabla si no existe
- `Create(ctx, Reminder) error` — inserta un reminder
- `Delete(ctx, id string) error` — borrado logico (marcar como deleted)
- `List(ctx, roomID string) ([]Reminder, error)` — listar activos de una room
- `MarkFired(ctx, id string) error` — marcar como disparado con timestamp
- `LoadActive(ctx) ([]Reminder, error)` — cargar todos los activos (para startup)
- Auto-crear tabla `reminders` en init (`CREATE TABLE IF NOT EXISTS`)
- [ ] **1.3** Tests del store en `shell/reminder/store_test.go`:
- Test CRUD completo: crear, listar, marcar fired, borrar
- Test que LoadActive no retorna reminders fired ni deleted
- Test de filtrado por roomID en List
- Usar tmpdir para base de datos de test
### Fase 2 — Tools de recordatorios
- [ ] **2.1** Crear `tools/reminder/reminder.go` con `NewCreateReminder(store, scheduler)`:
- Params: `message` (string, required), `time` (string, required — "15:00", "2026-04-10 15:00", "en 30 minutos"), `recurring` (bool, optional, default false), `cron` (string, optional — expresion cron para recurrentes)
- Parsear expresiones de tiempo naturales a cron expressions o timestamps absolutos
- Generar ID unico (UUID o nanoid)
- Persistir en store y registrar en scheduler
- [ ] **2.2** Crear `NewListReminders(store)`:
- Sin params requeridos (usa roomID del contexto del mensaje)
- Retorna lista formateada de reminders activos de la room
- [ ] **2.3** Crear `NewDeleteReminder(store, scheduler)`:
- Params: `id` (string, required)
- Borrar del store y remover del scheduler
- Validar que el reminder pertenece a la room del solicitante
- [ ] **2.4** Registrar tools en `devagents/runtime.go`:
- Condicion: nueva seccion `tools.reminders.enabled` en config
- Pasar referencia al store y al scheduler
- [ ] **2.5** Anadir `ReminderToolCfg` a `ToolsCfg` en `internal/config/schema.go`:
- Campos: `Enabled bool`, `MaxPerRoom int` (limite de reminders activos por room, default 50), `DBPath string` (default: `data/reminders.db`)
- [ ] **2.6** Tests en `tools/reminder/reminder_test.go`:
- Validacion de params requeridos
- Parsing de formatos de tiempo: "15:00", "2026-04-10 15:00", "en 30 minutos", "manana a las 9"
- Error si formato no reconocido
- Rate limit: error si se excede MaxPerRoom
### Fase 3 — Scheduler dinamico
- [ ] **3.1** Anadir `AddSchedule(id string, sc ScheduleCfg) error` al Scheduler:
- Registra un nuevo schedule en el cron runner en caliente
- Guardar referencia al `cron.EntryID` para poder remover despues
- Thread-safe (mutex sobre el mapa de entries)
- [ ] **3.2** Anadir `RemoveSchedule(id string) error` al Scheduler:
- Remover entry del cron runner por EntryID
- Limpiar del mapa interno
- [ ] **3.3** Implementar action kind `reminder` en `shell/cron/actions.go`:
- Envia mensaje personalizado al room: `"<prefix> @<user> Recordatorio: <mensaje>"`
- Nuevos campos en `ScheduledAction`: `UserID string` (para mention), `ReminderID string` (para tracking)
- [ ] **3.4** Logica one-shot:
- Despues de disparar un reminder one-shot, auto-remover del cron via `RemoveSchedule`
- Marcar como fired en el store via `MarkFired`
- Loguear: `"reminder_fired"`, `"reminder_auto_removed"`
- [ ] **3.5** On startup: cargar reminders persistidos en `devagents/runtime.go`:
- Despues de crear el Scheduler, llamar `store.LoadActive()`
- Registrar cada reminder activo via `scheduler.AddSchedule()`
- Descartar reminders one-shot cuya hora ya paso (marcar como fired)
- [ ] **3.6** Tests en `shell/cron/scheduler_test.go`:
- Test AddSchedule + RemoveSchedule (verificar que el cron entry existe/no existe)
- Test reminder action kind (mock MatrixSender, verificar mensaje con mention)
- Test one-shot auto-cleanup (verificar que despues de fire se remueve)
### Fase 4 — Agent-to-agent cron calls
- [ ] **4.1** Anadir campos a `ScheduledAction` en `internal/config/schema.go`:
- `TargetAgent string` — ID del agente destino
- `PromptTemplate string` — path al archivo .md con el prompt (reutilizar campo `Template`)
- [ ] **4.2** Inyectar `shell/bus.Bus` como dependencia del Scheduler:
- Nuevo campo `bus *bus.Bus` en Scheduler struct
- Parametro opcional en `New()` (nil si no hay bus disponible)
- [ ] **4.3** Implementar action kind `agent_call` en `shell/cron/actions.go`:
- Leer prompt desde `PromptTemplate` o inline `Prompt`
- Enviar via `bus.SendAndWait()` al agente destino con kind `"task"`
- El agente destino procesa el prompt via su LLM
- Enviar la respuesta al `OutputRoom` configurado
- Timeout configurable (default 2 minutos)
- [ ] **4.4** Documentar ejemplo de config en `crons/README.md` o similar
- [ ] **4.5** Tests en `shell/cron/scheduler_test.go`:
- Test `agent_call` con mock bus: verificar que envia mensaje correcto al agente destino
- Test timeout: verificar que si el agente no responde, se loguea error
- Test con bus nil: verificar que se loguea warning y se salta
### Fase 5 — Tests de integracion y cleanup
- [ ] **5.1** Test de integracion: crear reminder via tool → verificar que el scheduler lo tiene → fire → verificar store actualizado
- [ ] **5.2** Documentar nuevos action kinds en el system prompt de agentes que usen reminders
- [ ] **5.3** Actualizar `CLAUDE.md` con la nueva seccion de reminder tools si aplica
- [ ] **5.4** Verificar que `go build -tags goolm ./...` compila sin errores
- [ ] **5.5** Verificar que `go test -tags goolm ./...` pasa sin errores
## Ejemplo de uso
### Recordatorio one-shot via LLM
```
Usuario: "Recuerdame manana a las 9am que tengo reunion con el equipo"
Agente: [tool_call] create_reminder(message="Reunion con el equipo", time="2026-04-10 09:00", recurring=false)
Agente: "Listo, te recordare manana a las 9:00 AM."
→ 2026-04-10 09:00:
Agente envia: "⏰ @usuario Recordatorio: Reunion con el equipo"
→ Reminder auto-borrado del scheduler y marcado como fired en store.
```
### Recordatorio recurrente
```
Usuario: "Recuerdame todos los lunes a las 10am hacer el standup"
Agente: [tool_call] create_reminder(message="Hacer el standup", cron="0 10 * * 1", recurring=true)
Agente: "Configurado. Cada lunes a las 10:00 AM te recordare."
→ Cada lunes 10:00:
Agente envia: "⏰ @usuario Recordatorio: Hacer el standup"
```
### Listar y borrar reminders
```
Usuario: "Que recordatorios tengo?"
Agente: [tool_call] list_reminders()
Agente:
"Tienes 2 recordatorios activos:
1. [abc123] Reunion con el equipo — 2026-04-10 09:00 (one-shot)
2. [def456] Hacer el standup — lunes 10:00 (recurrente)"
Usuario: "Borra el del standup"
Agente: [tool_call] delete_reminder(id="def456")
Agente: "Recordatorio eliminado."
```
### Agent-to-agent cron call
Config en `agents/asistente-2/config.yaml`:
```yaml
schedules:
- name: daily-analysis
cron: "0 18 * * *"
action:
kind: agent_call
target_agent: "asistente-2"
prompt_template: "crons/daily-summary/prompts/prompt.md"
output_room: "!room:matrix-af2f3d.organic-machine.com"
```
Resultado: cada dia a las 18:00, el scheduler envia el prompt al agente `asistente-2` via bus. El agente procesa con su LLM y envia la respuesta al room configurado.
## Decisiones de diseno
1. **SQLite para persistencia de reminders**: ya tenemos el driver modernc configurado y probado. Un reminder es un dato simple (ID, mensaje, cron, estado). No justifica una dependencia nueva.
2. **Parsing de tiempo natural — enfoque progresivo**: empezar con formatos simples (ISO datetime `2026-04-10 15:00`, hora del dia `15:00`, expresiones cron). Anadir expresiones relativas (`en 30 minutos`, `manana a las 9`) como mejora incremental. No intentar NLP completo — el LLM ya interpreta la intencion, la tool solo necesita parsear el formato final.
3. **One-shot auto-delete**: los reminders que se disparan una vez se marcan como `fired` en el store (para auditoria) y se remueven del scheduler. Evita acumulacion de entries fantasma en el cron runner.
4. **`agent_call` usa el bus existente**: no se necesita protocolo nuevo. `SendAndWait` ya implementa el patron request-reply con timeout. El scheduler actua como el "from" agent, el target procesa via su pipeline LLM normal.
5. **Tools en subpackage `tools/reminder/`**: sigue el patron de `tools/file/`, `tools/ssh/`, etc. Cada tool recibe sus dependencias (store, scheduler) via constructor.
6. **Reminders scoped a room**: un reminder solo es visible y gestionable desde la room donde se creo. Esto evita que un usuario en room A borre reminders de room B.
## Prerequisitos
- Ninguno critico. Todo usa infraestructura existente:
- `shell/cron/scheduler.go` — scheduler a extender
- `shell/bus/` — bus inter-agente para `agent_call`
- `internal/config/schema.go` — config a extender
- SQLite via modernc (ya disponible)
- Pattern de tools en `tools/` (ya establecido)
## Riesgos
| Riesgo | Mitigacion |
|--------|------------|
| Parsing de tiempo natural es complejo | Empezar simple (ISO, hora, cron). El LLM normaliza la entrada antes de llamar la tool. Anadir formatos relativos iterativamente. |
| Timezone handling | Usar timezone del servidor inicialmente. Documentar la limitacion. Anadir soporte per-user TZ en un issue futuro si hay demanda. |
| Bus no disponible para `agent_call` | Si el bus es nil (agente standalone), loguear warning y saltar la ejecucion. Nunca crashear. |
| Tabla de reminders crece sin limite | One-shot se marcan fired (no se borran fisicamente para auditoria). Anadir retention policy (borrar fired > 30 dias) como cleanup task. |
| Scheduler concurrency con AddSchedule | `robfig/cron` es thread-safe para `AddFunc`/`Remove`. Proteger el mapa interno de IDs con mutex propio. |
| Reminder con cron invalido | Validar la expresion cron en el tool `create_reminder` antes de persistir. Retornar error claro al LLM si la expresion es invalida. |