1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! 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; absent, the home's own clock, as Hermes evaluates it (`HERMES_TIMEZONE`, then
/// the home's `timezone`, then the host's zone).
#[serde(default, skip_serializing_if = "Option::is_none")]
tz: Option<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>,
/// The fire is a turn in the live session of the conversation it delivers to (steering a running turn where
/// the worker allows), instead of a session of its own; Hermes mirrors it into that 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
}