Skip to main content

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