52d5632d89
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>
265 lines
13 KiB
Markdown
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. |
|