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