supercode-interchange 0.4.15

Canonical, provider-neutral session interchange primitives for Supercode
Documentation
//! Scheduled jobs (ยง2.6; Hermes `cron/jobs.json`).

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::ontology::Residue;

/// When a job fires.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Schedule {
    /// Once, at an instant.
    Once {
        /// RFC3339.
        run_at: String,
    },
    /// Every N minutes.
    Interval {
        /// Minutes between fires (> 0).
        minutes: f64,
    },
    /// A five-field cron expression in a timezone.
    Cron {
        /// The expression.
        expr: String,
        /// IANA timezone; `UTC` by default.
        #[serde(default = "utc")]
        tz: String,
    },
}

fn utc() -> String {
    "UTC".to_string()
}

impl Eq for Schedule {}

impl Schedule {
    /// Hermes's word for the kind.
    pub fn kind(&self) -> &'static str {
        match self {
            Self::Once { .. } => "once",
            Self::Interval { .. } => "interval",
            Self::Cron { .. } => "cron",
        }
    }
}

/// Where a result goes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Target {
    /// The surface the job was created from.
    Origin,
    /// The profile's `/sethome` surface.
    Home,
    /// A file under the profile dir (the fire's own output file).
    Local,
    /// A named surface, as `hermes send --to` spells one.
    Explicit {
        /// The platform.
        platform: String,
        /// The chat.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        chat_id: Option<String>,
        /// The thread.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        thread_id: Option<String>,
    },
}

impl Target {
    /// Hermes's `deliver` word: `origin | home | local | <platform>[:<chat_id>[:<thread_id>]]`.
    pub fn render(&self) -> String {
        match self {
            Self::Origin => "origin".into(),
            Self::Home => "home".into(),
            Self::Local => "local".into(),
            Self::Explicit {
                platform,
                chat_id,
                thread_id,
            } => {
                let mut s = platform.clone();
                if let Some(c) = chat_id {
                    s.push(':');
                    s.push_str(c);
                    if let Some(t) = thread_id {
                        s.push(':');
                        s.push_str(t);
                    }
                }
                s
            }
        }
    }
}

/// Hermes 0.21.0's `repeat`: `{times: N | null, completed: M}`; `null` times = forever.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Repeat {
    /// Remaining fires, or `None` for forever.
    #[serde(default)]
    pub times: Option<u32>,
    /// Fires so far.
    #[serde(default)]
    pub completed: u32,
}

/// The conversation a job was created from (Hermes `origin`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct JobOrigin {
    /// The platform.
    pub platform: String,
    /// The chat type, when known.
    #[serde(default)]
    pub chat_type: Option<String>,
    /// The chat.
    #[serde(default)]
    pub chat_id: Option<String>,
    /// The thread.
    #[serde(default)]
    pub thread_id: Option<String>,
}

/// One scheduled job.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Job {
    /// The id (a file-name-safe token).
    pub id: String,
    /// When it fires.
    pub schedule: Schedule,
    /// The prompt.
    #[serde(default)]
    pub prompt: Option<String>,
    /// Working directory: relative to the profile dir, or absolute.
    #[serde(default)]
    pub workdir: Option<String>,
    /// Model override.
    #[serde(default)]
    pub model: Option<String>,
    /// Skills to load.
    #[serde(default)]
    pub skills: Vec<String>,
    /// Job ids (or `self`) whose newest output is prepended at fire time.
    #[serde(default)]
    pub context_from: Option<Vec<String>>,
    /// Where the result goes.
    pub deliver: Target,
    /// Where a failure goes; `deliver` with a prefix when absent.
    #[serde(default)]
    pub failure_deliver: Option<Target>,
    /// The creating conversation.
    #[serde(default)]
    pub origin: Option<JobOrigin>,
    /// Hermes: mirror the fire's output into the target conversation's transcript.
    #[serde(default)]
    pub attach_to_session: Option<bool>,
    /// Remaining-runs counter for `once` jobs.
    #[serde(default)]
    pub repeat: Option<Repeat>,
    /// Whether it fires at all.
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// Next fire, RFC3339.
    #[serde(default)]
    pub next_run_at: Option<String>,
    /// Last fire, RFC3339.
    #[serde(default)]
    pub last_run_at: Option<String>,
    /// Last fire's status word.
    #[serde(default)]
    pub last_status: Option<String>,
    /// Creation, RFC3339.
    #[serde(default)]
    pub created_at: Option<String>,
    /// Source fields the record does not model, verbatim.
    #[serde(default)]
    pub residue: Residue,
}

fn default_true() -> bool {
    true
}