1use std::collections::BTreeMap;
13
14use schemars::JsonSchema;
15use serde::de::Error as _;
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17use serde_json::{Map, Value};
18
19#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
23#[schemars(transparent)]
24pub struct Expr(pub String);
25
26impl Serialize for Expr {
27 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
28 self.0.serialize(s)
29 }
30}
31
32impl<'de> Deserialize<'de> for Expr {
33 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
34 match Value::deserialize(d)? {
35 Value::String(s) => Ok(Self(s)),
36 v @ (Value::Number(_) | Value::Bool(_) | Value::Null) => Ok(Self(v.to_string())),
37 other => Err(D::Error::custom(format!(
38 "an expression is a string or a literal, not {other}"
39 ))),
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
47pub struct Action {
48 pub name: String,
50 pub args: Value,
52}
53
54impl Serialize for Action {
55 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
56 let mut m = Map::new();
57 m.insert(self.name.clone(), self.args.clone());
58 Value::Object(m).serialize(s)
59 }
60}
61
62impl<'de> Deserialize<'de> for Action {
63 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
64 match Value::deserialize(d)? {
65 Value::String(name) => Ok(Self {
66 name,
67 args: Value::Null,
68 }),
69 Value::Object(m) if m.len() == 1 => {
70 let (name, args) = m.into_iter().next().unwrap();
71 Ok(Self { name, args })
72 }
73 other => Err(D::Error::custom(format!(
74 "an action is a name or a one-key map, not {other}"
75 ))),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82pub struct Transition {
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub from: Vec<String>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub by: Vec<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub guard: Option<Expr>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub validate: Vec<Validation>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub target: Option<String>,
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub actions: Vec<Action>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub description: Option<String>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
109pub struct Validation {
110 pub expr: Expr,
112 pub message: String,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
118pub struct Delayed {
119 pub after: Expr,
121 #[serde(flatten)]
123 pub transition: Transition,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128pub struct Run {
129 pub role: String,
131 pub session: String,
134 #[serde(default)]
137 pub prompt: String,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub prompt_file: Option<String>,
141 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 pub skills: Vec<String>,
144 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
147 pub limits: BTreeMap<String, Expr>,
148}
149
150#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152pub struct Status {
153 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub stored_as: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub description: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub run: Option<Run>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub run_role: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub resume_as: Option<String>,
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub entry: Vec<Action>,
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub exit: Vec<Action>,
175 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
177 pub on: BTreeMap<String, Vec<Transition>>,
178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub always: Vec<Transition>,
181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
183 pub after: Vec<Delayed>,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188pub struct Limit {
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub description: Option<String>,
192 pub group: Expr,
194 pub counts: Expr,
196 pub max: Expr,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub reserve: Option<Reserve>,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
205pub struct Reserve {
206 #[serde(rename = "for")]
208 pub for_cards: Expr,
209 pub slots: Expr,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
215pub struct Dispatch {
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub serve: Option<Expr>,
219 pub tick: Expr,
221 pub start: String,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub eligible: Option<Expr>,
226 #[serde(default, skip_serializing_if = "Vec::is_empty")]
228 pub order: Vec<Expr>,
229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
231 pub limits: Vec<Limit>,
232}
233
234#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
236pub struct RunMachine {
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub recurring_role: Option<String>,
240 #[serde(default)]
242 pub limits: BTreeMap<String, Expr>,
243 #[serde(default)]
245 pub roles: BTreeMap<String, Run>,
246 #[serde(default)]
248 pub states: BTreeMap<String, Status>,
249 #[serde(default)]
251 pub initial: String,
252 #[serde(default)]
254 pub on: BTreeMap<String, Vec<Transition>>,
255 #[serde(default)]
257 pub outcomes: BTreeMap<String, Vec<Transition>>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
262pub struct StorageRule {
263 pub guard: Expr,
264 pub status: String,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
269pub struct Workflow {
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub run: Option<RunMachine>,
273 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
275 pub verbs: BTreeMap<String, Vec<Transition>>,
276 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
278 pub derived: BTreeMap<String, Expr>,
279 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
281 pub relations: BTreeMap<String, Value>,
282 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
284 pub flags: BTreeMap<String, Value>,
285 #[serde(default, skip_serializing_if = "Vec::is_empty")]
287 pub stored_as: Vec<StorageRule>,
288 #[serde(default, skip_serializing_if = "Vec::is_empty")]
289 pub losses: Vec<String>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub description: Option<String>,
293 #[serde(default, skip_serializing_if = "Map::is_empty")]
296 pub params: Map<String, Value>,
297 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
299 pub roles: BTreeMap<String, Expr>,
300 pub initial: Vec<Transition>,
302 pub statuses: BTreeMap<String, Status>,
304 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
306 pub on: BTreeMap<String, Vec<Transition>>,
307 pub dispatch: Dispatch,
309 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
312 pub notify: BTreeMap<String, BTreeMap<String, String>>,
313}
314
315pub fn hermes_instance() -> Workflow {
318 let mut workflow: Workflow =
319 serde_yaml::from_str(HERMES_WORKFLOW).expect("the built-in Hermes workflow parses");
320 if let Some(run) = &mut workflow.run {
321 for (role, prompt) in [
322 ("triager", include_str!("prompts/triager.md")),
323 ("implementer", include_str!("prompts/implementer.md")),
324 ("reviewer", include_str!("prompts/reviewer.md")),
325 ] {
326 if let Some(spec) = run.roles.get_mut(role) {
327 spec.prompt = prompt.to_owned();
328 }
329 }
330 }
331 workflow
332}
333
334pub const HERMES_WORKFLOW: &str = include_str!("hermes.workflow.yaml");
336
337impl Workflow {
338 pub fn check(&self) -> Result<(), String> {
340 let known = |t: &Option<String>| match t {
341 Some(name) if !name.starts_with("{{") && !self.statuses.contains_key(name) => {
342 Err(format!("no status {name}"))
343 }
344 _ => Ok(()),
345 };
346 let all = |ts: &[Transition]| ts.iter().try_for_each(|t| known(&t.target));
347 all(&self.initial)?;
348 for ts in self.verbs.values() {
349 all(ts)?;
350 for t in ts {
351 for source in &t.from {
352 if !self.statuses.contains_key(source) {
353 return Err(format!("no source status {source}"));
354 }
355 }
356 }
357 }
358 if let Some(run) = &self.run {
359 if !run.states.contains_key(&run.initial) {
360 return Err(format!("no initial run state {}", run.initial));
361 }
362 for ts in run.outcomes.values() {
363 all(ts)?;
364 }
365 for ts in run
366 .on
367 .values()
368 .chain(run.states.values().flat_map(|s| s.on.values()))
369 {
370 for t in ts {
371 if let Some(target) = &t.target {
372 if !target.starts_with("{{") && !run.states.contains_key(target) {
373 return Err(format!("no run state {target}"));
374 }
375 }
376 }
377 }
378 for (name, spec) in &run.roles {
379 if !self.roles.contains_key(&spec.role) {
380 return Err(format!("run {name}: no role {}", spec.role));
381 }
382 }
383 }
384 for ts in self.on.values() {
385 all(ts)?;
386 }
387 for (name, s) in &self.statuses {
388 if let Some(role) = &s.run_role {
389 if !self
390 .run
391 .as_ref()
392 .is_some_and(|r| r.roles.contains_key(role))
393 {
394 return Err(format!("{name}: no run specification {role}"));
395 }
396 }
397 for ts in s.on.values() {
398 all(ts).map_err(|e| format!("{name}: {e}"))?;
399 }
400 all(&s.always).map_err(|e| format!("{name}: {e}"))?;
401 s.after
402 .iter()
403 .try_for_each(|d| known(&d.transition.target))
404 .map_err(|e| format!("{name}: {e}"))?;
405 if let Some(run) = &s.run {
406 if !self.roles.contains_key(&run.role) {
407 return Err(format!("{name}: its run names no role {}", run.role));
408 }
409 }
410 }
411 Ok(())
412 }
413}