// Package cron provides a scheduler for autonomous bot activity. // It is part of the impure shell: it reads files, calls LLMs, and sends Matrix messages. package cron import ( "context" "log/slog" "github.com/robfig/cron/v3" "github.com/enmanuel/agents/internal/config" coretypes "github.com/enmanuel/agents/pkg/llm" ) // MatrixSender is the subset of matrix.Client needed by the scheduler. type MatrixSender interface { SendMarkdown(ctx context.Context, roomID, markdown string) error } // Scheduler fires configured schedules and executes send_message or llm_prompt actions. type Scheduler struct { cfg []config.ScheduleCfg matrix MatrixSender llm coretypes.CompleteFunc // nil when agent has no LLM model string logger *slog.Logger cron *cron.Cron } // New creates a Scheduler. llm and model are optional (nil/empty for agents without LLM). func New( cfg []config.ScheduleCfg, matrix MatrixSender, llm coretypes.CompleteFunc, model string, logger *slog.Logger, ) *Scheduler { return &Scheduler{ cfg: cfg, matrix: matrix, llm: llm, model: model, logger: logger.With("component", "cron"), cron: cron.New(), } } // Fire immediately executes the action for the given schedule, bypassing the cron timer. // Useful for tests and manual triggering from CLI. func (s *Scheduler) Fire(ctx context.Context, sc config.ScheduleCfg) { room := sc.OutputRoom if room == "" { s.logger.Warn("Fire: schedule has no output_room, skipping", "name", sc.Name) return } handler := s.buildHandler(sc) if handler == nil { s.logger.Warn("Fire: unsupported action kind", "name", sc.Name, "kind", sc.Action.Kind) return } handler(ctx, room) } // Start registers all schedules and starts the cron loop. // It returns when ctx is cancelled, stopping the cron runner. func (s *Scheduler) Start(ctx context.Context) { for _, sc := range s.cfg { sc := sc // capture range var if sc.Cron == "" || sc.Action.Kind == "" { s.logger.Warn("skipping invalid schedule", "name", sc.Name, "cron", sc.Cron, "kind", sc.Action.Kind) continue } room := sc.OutputRoom if room == "" { s.logger.Warn("schedule has no output_room, skipping", "name", sc.Name) continue } handler := s.buildHandler(sc) if handler == nil { s.logger.Warn("unsupported action kind, skipping", "name", sc.Name, "kind", sc.Action.Kind) continue } _, err := s.cron.AddFunc(sc.Cron, func() { handler(ctx, room) }) if err != nil { s.logger.Error("failed to register schedule", "name", sc.Name, "cron", sc.Cron, "err", err, ) continue } s.logger.Info("schedule registered", "name", sc.Name, "cron", sc.Cron, "kind", sc.Action.Kind, "room", room) } s.cron.Start() s.logger.Info("cron scheduler started", "schedules", len(s.cfg)) <-ctx.Done() s.logger.Info("cron scheduler stopping") cronCtx := s.cron.Stop() <-cronCtx.Done() s.logger.Info("cron scheduler stopped") }