# 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. |