Skip to main content

supercode_interchange/world/
job.rs

1//! Scheduled jobs (ยง2.6; Hermes `cron/jobs.json`).
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::ontology::Residue;
7
8/// When a job fires.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum Schedule {
12    /// Once, at an instant.
13    Once {
14        /// RFC3339.
15        run_at: String,
16    },
17    /// Every N minutes.
18    Interval {
19        /// Minutes between fires (> 0).
20        minutes: f64,
21    },
22    /// A five-field cron expression in a timezone.
23    Cron {
24        /// The expression.
25        expr: String,
26        /// IANA timezone; `UTC` by default.
27        #[serde(default = "utc")]
28        tz: String,
29    },
30}
31
32fn utc() -> String {
33    "UTC".to_string()
34}
35
36impl Eq for Schedule {}
37
38impl Schedule {
39    /// Hermes's word for the kind.
40    pub fn kind(&self) -> &'static str {
41        match self {
42            Self::Once { .. } => "once",
43            Self::Interval { .. } => "interval",
44            Self::Cron { .. } => "cron",
45        }
46    }
47}
48
49/// Where a result goes.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51#[serde(tag = "kind", rename_all = "snake_case")]
52pub enum Target {
53    /// The surface the job was created from.
54    Origin,
55    /// The profile's `/sethome` surface.
56    Home,
57    /// A file under the profile dir (the fire's own output file).
58    Local,
59    /// A named surface, as `hermes send --to` spells one.
60    Explicit {
61        /// The platform.
62        platform: String,
63        /// The chat.
64        #[serde(default, skip_serializing_if = "Option::is_none")]
65        chat_id: Option<String>,
66        /// The thread.
67        #[serde(default, skip_serializing_if = "Option::is_none")]
68        thread_id: Option<String>,
69    },
70}
71
72impl Target {
73    /// Hermes's `deliver` word: `origin | home | local | <platform>[:<chat_id>[:<thread_id>]]`.
74    pub fn render(&self) -> String {
75        match self {
76            Self::Origin => "origin".into(),
77            Self::Home => "home".into(),
78            Self::Local => "local".into(),
79            Self::Explicit {
80                platform,
81                chat_id,
82                thread_id,
83            } => {
84                let mut s = platform.clone();
85                if let Some(c) = chat_id {
86                    s.push(':');
87                    s.push_str(c);
88                    if let Some(t) = thread_id {
89                        s.push(':');
90                        s.push_str(t);
91                    }
92                }
93                s
94            }
95        }
96    }
97}
98
99/// Hermes 0.21.0's `repeat`: `{times: N | null, completed: M}`; `null` times = forever.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
101pub struct Repeat {
102    /// Remaining fires, or `None` for forever.
103    #[serde(default)]
104    pub times: Option<u32>,
105    /// Fires so far.
106    #[serde(default)]
107    pub completed: u32,
108}
109
110/// The conversation a job was created from (Hermes `origin`).
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
112pub struct JobOrigin {
113    /// The platform.
114    pub platform: String,
115    /// The chat type, when known.
116    #[serde(default)]
117    pub chat_type: Option<String>,
118    /// The chat.
119    #[serde(default)]
120    pub chat_id: Option<String>,
121    /// The thread.
122    #[serde(default)]
123    pub thread_id: Option<String>,
124}
125
126/// One scheduled job.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128pub struct Job {
129    /// The id (a file-name-safe token).
130    pub id: String,
131    /// When it fires.
132    pub schedule: Schedule,
133    /// The prompt.
134    #[serde(default)]
135    pub prompt: Option<String>,
136    /// Working directory: relative to the profile dir, or absolute.
137    #[serde(default)]
138    pub workdir: Option<String>,
139    /// Model override.
140    #[serde(default)]
141    pub model: Option<String>,
142    /// Skills to load.
143    #[serde(default)]
144    pub skills: Vec<String>,
145    /// Job ids (or `self`) whose newest output is prepended at fire time.
146    #[serde(default)]
147    pub context_from: Option<Vec<String>>,
148    /// Where the result goes.
149    pub deliver: Target,
150    /// Where a failure goes; `deliver` with a prefix when absent.
151    #[serde(default)]
152    pub failure_deliver: Option<Target>,
153    /// The creating conversation.
154    #[serde(default)]
155    pub origin: Option<JobOrigin>,
156    /// Hermes: mirror the fire's output into the target conversation's transcript.
157    #[serde(default)]
158    pub attach_to_session: Option<bool>,
159    /// Remaining-runs counter for `once` jobs.
160    #[serde(default)]
161    pub repeat: Option<Repeat>,
162    /// Whether it fires at all.
163    #[serde(default = "default_true")]
164    pub enabled: bool,
165    /// Next fire, RFC3339.
166    #[serde(default)]
167    pub next_run_at: Option<String>,
168    /// Last fire, RFC3339.
169    #[serde(default)]
170    pub last_run_at: Option<String>,
171    /// Last fire's status word.
172    #[serde(default)]
173    pub last_status: Option<String>,
174    /// Creation, RFC3339.
175    #[serde(default)]
176    pub created_at: Option<String>,
177    /// Source fields the record does not model, verbatim.
178    #[serde(default)]
179    pub residue: Residue,
180}
181
182fn default_true() -> bool {
183    true
184}