use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
#[schemars(transparent)]
pub struct Expr(pub String);
impl Serialize for Expr {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl<'de> Deserialize<'de> for Expr {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
match Value::deserialize(d)? {
Value::String(s) => Ok(Self(s)),
v @ (Value::Number(_) | Value::Bool(_) | Value::Null) => Ok(Self(v.to_string())),
other => Err(D::Error::custom(format!(
"an expression is a string or a literal, not {other}"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct Action {
pub name: String,
pub args: Value,
}
impl Serialize for Action {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut m = Map::new();
m.insert(self.name.clone(), self.args.clone());
Value::Object(m).serialize(s)
}
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
match Value::deserialize(d)? {
Value::String(name) => Ok(Self {
name,
args: Value::Null,
}),
Value::Object(m) if m.len() == 1 => {
let (name, args) = m.into_iter().next().unwrap();
Ok(Self { name, args })
}
other => Err(D::Error::custom(format!(
"an action is a name or a one-key map, not {other}"
))),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Transition {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub from: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub by: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guard: Option<Expr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub validate: Vec<Validation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<Action>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Validation {
pub expr: Expr,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Delayed {
pub after: Expr,
#[serde(flatten)]
pub transition: Transition,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Run {
pub role: String,
pub session: String,
#[serde(default)]
pub prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_file: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub limits: BTreeMap<String, Expr>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Status {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stored_as: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run: Option<Run>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_as: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry: Vec<Action>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exit: Vec<Action>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub on: BTreeMap<String, Vec<Transition>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub always: Vec<Transition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub after: Vec<Delayed>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Limit {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub group: Expr,
pub counts: Expr,
pub max: Expr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reserve: Option<Reserve>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Reserve {
#[serde(rename = "for")]
pub for_cards: Expr,
pub slots: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Dispatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub serve: Option<Expr>,
pub tick: Expr,
pub start: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eligible: Option<Expr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub order: Vec<Expr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub limits: Vec<Limit>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RunMachine {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recurring_role: Option<String>,
#[serde(default)]
pub limits: BTreeMap<String, Expr>,
#[serde(default)]
pub roles: BTreeMap<String, Run>,
#[serde(default)]
pub states: BTreeMap<String, Status>,
#[serde(default)]
pub initial: String,
#[serde(default)]
pub on: BTreeMap<String, Vec<Transition>>,
#[serde(default)]
pub outcomes: BTreeMap<String, Vec<Transition>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct StorageRule {
pub guard: Expr,
pub status: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Workflow {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run: Option<RunMachine>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub verbs: BTreeMap<String, Vec<Transition>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub derived: BTreeMap<String, Expr>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub relations: BTreeMap<String, Value>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub flags: BTreeMap<String, Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stored_as: Vec<StorageRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub losses: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Map::is_empty")]
pub params: Map<String, Value>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub roles: BTreeMap<String, Expr>,
pub initial: Vec<Transition>,
pub statuses: BTreeMap<String, Status>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub on: BTreeMap<String, Vec<Transition>>,
pub dispatch: Dispatch,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub notify: BTreeMap<String, BTreeMap<String, String>>,
}
pub fn hermes_instance() -> Workflow {
let mut workflow: Workflow =
serde_yaml::from_str(HERMES_WORKFLOW).expect("the built-in Hermes workflow parses");
if let Some(run) = &mut workflow.run {
for (role, prompt) in [
("triager", include_str!("prompts/triager.md")),
("implementer", include_str!("prompts/implementer.md")),
("reviewer", include_str!("prompts/reviewer.md")),
] {
if let Some(spec) = run.roles.get_mut(role) {
spec.prompt = prompt.to_owned();
}
}
}
workflow
}
pub const HERMES_WORKFLOW: &str = include_str!("hermes.workflow.yaml");
impl Workflow {
pub fn check(&self) -> Result<(), String> {
let known = |t: &Option<String>| match t {
Some(name) if !name.starts_with("{{") && !self.statuses.contains_key(name) => {
Err(format!("no status {name}"))
}
_ => Ok(()),
};
let all = |ts: &[Transition]| ts.iter().try_for_each(|t| known(&t.target));
all(&self.initial)?;
for ts in self.verbs.values() {
all(ts)?;
for t in ts {
for source in &t.from {
if !self.statuses.contains_key(source) {
return Err(format!("no source status {source}"));
}
}
}
}
if let Some(run) = &self.run {
if !run.states.contains_key(&run.initial) {
return Err(format!("no initial run state {}", run.initial));
}
for ts in run.outcomes.values() {
all(ts)?;
}
for ts in run
.on
.values()
.chain(run.states.values().flat_map(|s| s.on.values()))
{
for t in ts {
if let Some(target) = &t.target {
if !target.starts_with("{{") && !run.states.contains_key(target) {
return Err(format!("no run state {target}"));
}
}
}
}
for (name, spec) in &run.roles {
if !self.roles.contains_key(&spec.role) {
return Err(format!("run {name}: no role {}", spec.role));
}
}
}
for ts in self.on.values() {
all(ts)?;
}
for (name, s) in &self.statuses {
if let Some(role) = &s.run_role {
if !self
.run
.as_ref()
.is_some_and(|r| r.roles.contains_key(role))
{
return Err(format!("{name}: no run specification {role}"));
}
}
for ts in s.on.values() {
all(ts).map_err(|e| format!("{name}: {e}"))?;
}
all(&s.always).map_err(|e| format!("{name}: {e}"))?;
s.after
.iter()
.try_for_each(|d| known(&d.transition.target))
.map_err(|e| format!("{name}: {e}"))?;
if let Some(run) = &s.run {
if !self.roles.contains_key(&run.role) {
return Err(format!("{name}: its run names no role {}", run.role));
}
}
}
Ok(())
}
}