| 1 | """Cron types.""" |
| 2 | |
| 3 | from dataclasses import dataclass, field |
| 4 | from typing import Literal |
| 5 | |
| 6 | |
| 7 | @dataclass |
| 8 | class CronSchedule: |
| 9 | """Schedule definition for a cron job.""" |
| 10 | kind: Literal["at", "every", "cron"] |
| 11 | # For "at": timestamp in ms |
| 12 | at_ms: int | None = None |
| 13 | # For "every": interval in ms |
| 14 | every_ms: int | None = None |
| 15 | # For "cron": cron expression (e.g. "0 9 * * *") |
| 16 | expr: str | None = None |
| 17 | # Timezone for cron expressions |
| 18 | tz: str | None = None |
| 19 | |
| 20 | |
| 21 | @dataclass |
| 22 | class CronPayload: |
| 23 | """What to do when the job runs.""" |
| 24 | kind: Literal["system_event", "agent_turn"] = "agent_turn" |
| 25 | message: str = "" |
| 26 | # Deliver response to channel |
| 27 | deliver: bool = False |
| 28 | channel: str | None = None # e.g. "whatsapp" |
| 29 | to: str | None = None # e.g. phone number |
| 30 | |
| 31 | |
| 32 | @dataclass |
| 33 | class CronRunRecord: |
| 34 | """A single execution record for a cron job.""" |
| 35 | run_at_ms: int |
| 36 | status: Literal["ok", "error", "skipped"] |
| 37 | duration_ms: int = 0 |
| 38 | error: str | None = None |
| 39 | |
| 40 | |
| 41 | @dataclass |
| 42 | class CronJobState: |
| 43 | """Runtime state of a job.""" |
| 44 | next_run_at_ms: int | None = None |
| 45 | last_run_at_ms: int | None = None |
| 46 | last_status: Literal["ok", "error", "skipped"] | None = None |
| 47 | last_error: str | None = None |
| 48 | run_history: list[CronRunRecord] = field(default_factory=list) |
| 49 | |
| 50 | |
| 51 | @dataclass |
| 52 | class CronJob: |
| 53 | """A scheduled job.""" |
| 54 | id: str |
| 55 | name: str |
| 56 | enabled: bool = True |
| 57 | schedule: CronSchedule = field(default_factory=lambda: CronSchedule(kind="every")) |
| 58 | payload: CronPayload = field(default_factory=CronPayload) |
| 59 | state: CronJobState = field(default_factory=CronJobState) |
| 60 | created_at_ms: int = 0 |
| 61 | updated_at_ms: int = 0 |
| 62 | delete_after_run: bool = False |
| 63 | |
| 64 | @classmethod |
| 65 | def from_dict(cls, kwargs: dict): |
| 66 | state_kwargs = dict(kwargs.get("state", {})) |
| 67 | state_kwargs["run_history"] = [ |
| 68 | record if isinstance(record, CronRunRecord) else CronRunRecord(**record) |
| 69 | for record in state_kwargs.get("run_history", []) |
| 70 | ] |
| 71 | kwargs["schedule"] = CronSchedule(**kwargs.get("schedule", {"kind": "every"})) |
| 72 | kwargs["payload"] = CronPayload(**kwargs.get("payload", {})) |
| 73 | kwargs["state"] = CronJobState(**state_kwargs) |
| 74 | return cls(**kwargs) |
| 75 | |
| 76 | |
| 77 | @dataclass |
| 78 | class CronStore: |
| 79 | """Persistent store for cron jobs.""" |
| 80 | version: int = 1 |
| 81 | jobs: list[CronJob] = field(default_factory=list) |
| 82 |