"""Scheduled task models for Beaver cron. The scheduler borrows Hermes' durable JSON + explicit schedule parsing shape, but the execution target is Beaver Task mode: every trigger creates a normal Task run instead of a detached agent turn. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Literal from uuid import uuid4 CronScheduleKind = Literal["at", "every", "cron"] CronPayloadKind = Literal["agent_turn", "system_event"] CronPayloadMode = Literal["notification", "task"] @dataclass(slots=True) class CronSchedule: kind: CronScheduleKind at_ms: int | None = None every_ms: int | None = None expr: str | None = None tz: str | None = None display: str | None = None def to_dict(self) -> dict[str, Any]: return { "kind": self.kind, "at_ms": self.at_ms, "every_ms": self.every_ms, "expr": self.expr, "tz": self.tz, "display": self.display, } @classmethod def from_dict(cls, payload: dict[str, Any]) -> "CronSchedule": return cls( kind=str(payload.get("kind") or "every"), # type: ignore[arg-type] at_ms=_optional_int(payload.get("at_ms") or payload.get("atMs")), every_ms=_optional_int(payload.get("every_ms") or payload.get("everyMs")), expr=_optional_str(payload.get("expr")), tz=_optional_str(payload.get("tz")), display=_optional_str(payload.get("display")), ) @dataclass(slots=True) class CronPayload: kind: CronPayloadKind = "agent_turn" mode: CronPayloadMode = "notification" message: str = "" session_key: str | None = None requires_followup: bool = False deliver: bool = False channel: str | None = None to: str | None = None def to_dict(self) -> dict[str, Any]: return { "kind": self.kind, "mode": self.mode, "message": self.message, "session_key": self.session_key, "requires_followup": self.requires_followup, "deliver": self.deliver, "channel": self.channel, "to": self.to, } @classmethod def from_dict(cls, payload: dict[str, Any]) -> "CronPayload": return cls( kind=str(payload.get("kind") or "agent_turn"), # type: ignore[arg-type] mode=_payload_mode(payload.get("mode"), default="task"), message=str(payload.get("message") or ""), session_key=_optional_str(payload.get("session_key") or payload.get("sessionKey")), requires_followup=bool(payload.get("requires_followup") or payload.get("requiresFollowup") or False), deliver=bool(payload.get("deliver", False)), channel=_optional_str(payload.get("channel")), to=_optional_str(payload.get("to")), ) @dataclass(slots=True) class CronRunRecord: started_at_ms: int scheduled_run_id: str = field(default_factory=lambda: uuid4().hex) finished_at_ms: int | None = None status: Literal["running", "ok", "error", "skipped"] = "running" mode: CronPayloadMode = "notification" notification_session_id: str | None = None output: str | None = None task_id: str | None = None run_id: str | None = None error: str | None = None engaged: bool = False engaged_at_ms: int | None = None engage_intent: str | None = None def to_dict(self) -> dict[str, Any]: return { "scheduled_run_id": self.scheduled_run_id, "started_at_ms": self.started_at_ms, "finished_at_ms": self.finished_at_ms, "status": self.status, "mode": self.mode, "notification_session_id": self.notification_session_id, "output": self.output, "task_id": self.task_id, "run_id": self.run_id, "error": self.error, "engaged": self.engaged, "engaged_at_ms": self.engaged_at_ms, "engage_intent": self.engage_intent, } @classmethod def from_dict(cls, payload: dict[str, Any]) -> "CronRunRecord": return cls( scheduled_run_id=str(payload.get("scheduled_run_id") or payload.get("scheduledRunId") or uuid4().hex), started_at_ms=int(payload.get("started_at_ms") or payload.get("startedAtMs") or 0), finished_at_ms=_optional_int(payload.get("finished_at_ms") or payload.get("finishedAtMs")), status=str(payload.get("status") or "running"), # type: ignore[arg-type] mode=_payload_mode(payload.get("mode"), default="notification"), notification_session_id=_optional_str(payload.get("notification_session_id") or payload.get("notificationSessionId")), output=_optional_str(payload.get("output")), task_id=_optional_str(payload.get("task_id") or payload.get("taskId")), run_id=_optional_str(payload.get("run_id") or payload.get("runId")), error=_optional_str(payload.get("error")), engaged=bool(payload.get("engaged", False)), engaged_at_ms=_optional_int(payload.get("engaged_at_ms") or payload.get("engagedAtMs")), engage_intent=_optional_str(payload.get("engage_intent") or payload.get("engageIntent")), ) @dataclass(slots=True) class CronJob: id: str name: str enabled: bool schedule: CronSchedule payload: CronPayload created_at_ms: int updated_at_ms: int next_run_at_ms: int | None = None last_run_at_ms: int | None = None last_status: Literal["ok", "error", "skipped"] | None = None last_error: str | None = None delete_after_run: bool = False history: list[CronRunRecord] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return { "id": self.id, "name": self.name, "enabled": self.enabled, "schedule": self.schedule.to_dict(), "payload": self.payload.to_dict(), "created_at_ms": self.created_at_ms, "updated_at_ms": self.updated_at_ms, "next_run_at_ms": self.next_run_at_ms, "last_run_at_ms": self.last_run_at_ms, "last_status": self.last_status, "last_error": self.last_error, "delete_after_run": self.delete_after_run, "history": [item.to_dict() for item in self.history], } def to_api_dict(self) -> dict[str, Any]: latest = self.history[-1] if self.history else None return { "id": self.id, "name": self.name, "enabled": self.enabled, "schedule_kind": self.schedule.kind, "schedule_display": self.schedule.display or _schedule_display(self.schedule), "schedule_expr": self.schedule.expr, "schedule_every_ms": self.schedule.every_ms, "message": self.payload.message, "mode": self.payload.mode, "requires_followup": self.payload.requires_followup, "deliver": self.payload.deliver, "channel": self.payload.channel, "to": self.payload.to, "session_key": self.payload.session_key, "next_run_at_ms": self.next_run_at_ms, "last_run_at_ms": self.last_run_at_ms, "last_status": self.last_status, "last_error": self.last_error, "last_scheduled_run_id": latest.scheduled_run_id if latest else None, "last_task_id": latest.task_id if latest else None, "last_run_id": latest.run_id if latest else None, "history": [item.to_dict() for item in self.history], "created_at_ms": self.created_at_ms, "updated_at_ms": self.updated_at_ms, } @classmethod def from_dict(cls, payload: dict[str, Any]) -> "CronJob": schedule_payload = payload.get("schedule") if isinstance(payload.get("schedule"), dict) else {} payload_payload = payload.get("payload") if isinstance(payload.get("payload"), dict) else {} return cls( id=str(payload["id"]), name=str(payload.get("name") or payload["id"]), enabled=bool(payload.get("enabled", True)), schedule=CronSchedule.from_dict(schedule_payload), payload=CronPayload.from_dict(payload_payload), created_at_ms=int(payload.get("created_at_ms") or payload.get("createdAtMs") or 0), updated_at_ms=int(payload.get("updated_at_ms") or payload.get("updatedAtMs") or 0), next_run_at_ms=_optional_int(payload.get("next_run_at_ms") or payload.get("nextRunAtMs")), last_run_at_ms=_optional_int(payload.get("last_run_at_ms") or payload.get("lastRunAtMs")), last_status=_optional_str(payload.get("last_status") or payload.get("lastStatus")), # type: ignore[arg-type] last_error=_optional_str(payload.get("last_error") or payload.get("lastError")), delete_after_run=bool(payload.get("delete_after_run") or payload.get("deleteAfterRun") or False), history=[ CronRunRecord.from_dict(item) for item in payload.get("history") or [] if isinstance(item, dict) ], ) @dataclass(slots=True) class CronExecutionResult: response: str | None = None task_id: str | None = None run_id: str | None = None notification_session_id: str | None = None mode: CronPayloadMode = "notification" def _schedule_display(schedule: CronSchedule) -> str: if schedule.kind == "every": seconds = int((schedule.every_ms or 0) / 1000) return f"every {seconds}s" if schedule.kind == "cron": return schedule.expr or "cron" return "one-time" def _optional_str(value: Any) -> str | None: if value in (None, ""): return None return str(value) def _optional_int(value: Any) -> int | None: if value in (None, ""): return None def _payload_mode(value: Any, *, default: CronPayloadMode = "notification") -> CronPayloadMode: if value in (None, ""): return default cleaned = str(value or "").strip().lower() if cleaned == "task": return "task" return "notification" try: return int(value) except (TypeError, ValueError): return None