1use std::path::Path;
11
12use serde::Serialize;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Restart {
17 Permanent,
19 Transient,
21 Temporary,
23}
24
25impl Restart {
26 pub fn parse(raw: &str) -> Option<Self> {
27 match raw {
28 "permanent" => Some(Self::Permanent),
29 "transient" => Some(Self::Transient),
30 "temporary" => Some(Self::Temporary),
31 _ => None,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ExitKind {
39 Clean,
40 Crash,
41}
42
43impl ExitKind {
44 pub fn parse(raw: &str) -> Option<Self> {
45 match raw {
46 "clean" => Some(Self::Clean),
47 "crash" => Some(Self::Crash),
48 _ => None,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "kebab-case")]
56pub enum Action {
57 Restart,
58 Stop,
59 GiveUp,
61}
62
63#[derive(Debug, Clone)]
65pub struct Child {
66 pub name: String,
67 pub harness: String,
68 pub restart: Restart,
69 pub max_restarts: u32,
70 pub window_secs: u64,
71}
72
73#[derive(Debug, Clone)]
75pub struct Supervisor {
76 pub child: Child,
77 pub failures: Vec<u64>,
78}
79
80impl Supervisor {
81 pub fn on_exit(&mut self, kind: ExitKind, now: u64) -> Action {
84 match (self.child.restart, kind) {
85 (Restart::Temporary, _) | (Restart::Transient, ExitKind::Clean) => Action::Stop,
86 (Restart::Permanent, _) | (Restart::Transient, ExitKind::Crash) => {
87 let window = self.child.window_secs;
88 self.failures.retain(|t| now.saturating_sub(*t) < window);
89 if self.failures.len() as u32 >= self.child.max_restarts {
90 Action::GiveUp
91 } else {
92 self.failures.push(now);
93 Action::Restart
94 }
95 }
96 }
97 }
98}
99
100#[must_use]
105pub fn herdr_argv(name: &str, home: &Path, script: &Path) -> Vec<String> {
106 vec![
107 "herdr".into(),
108 "agent".into(),
109 "start".into(),
110 format!("persona-{name}"),
111 "--no-focus".into(),
112 "--cwd".into(),
113 home.display().to_string(),
114 "--".into(),
115 "sh".into(),
116 script.display().to_string(),
117 ]
118}
119
120#[derive(Debug, Serialize)]
122pub struct Decision {
123 pub action: Action,
124 pub harness: String,
125 pub herdr: Vec<String>,
126 pub failures: Vec<u64>,
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::path::Path;
133
134 fn child(restart: Restart) -> Supervisor {
135 Supervisor {
136 child: Child {
137 name: "grok-1".into(),
138 harness: "grok".into(),
139 restart,
140 max_restarts: 3,
141 window_secs: 60,
142 },
143 failures: Vec::new(),
144 }
145 }
146
147 #[test]
148 fn a_permanent_child_restarts_and_then_gives_up() {
149 let mut sup = child(Restart::Permanent);
150 assert_eq!(sup.on_exit(ExitKind::Crash, 10), Action::Restart);
151 assert_eq!(sup.on_exit(ExitKind::Clean, 20), Action::Restart);
152 assert_eq!(sup.on_exit(ExitKind::Crash, 30), Action::Restart);
153 assert_eq!(sup.on_exit(ExitKind::Crash, 40), Action::GiveUp);
154 assert_eq!(sup.failures, vec![10, 20, 30]);
155 }
156
157 #[test]
158 fn an_old_failure_leaves_the_window() {
159 let mut sup = child(Restart::Permanent);
160 sup.failures = vec![10, 20, 30];
161 assert_eq!(sup.on_exit(ExitKind::Crash, 100), Action::Restart);
162 assert_eq!(sup.failures, vec![100]);
163 }
164
165 #[test]
166 fn transient_stops_on_a_clean_exit_and_temporary_never_restarts() {
167 let mut sup = child(Restart::Transient);
168 assert_eq!(sup.on_exit(ExitKind::Clean, 5), Action::Stop);
169 assert!(sup.failures.is_empty());
170 assert_eq!(sup.on_exit(ExitKind::Crash, 6), Action::Restart);
171 let mut once = child(Restart::Temporary);
172 assert_eq!(once.on_exit(ExitKind::Crash, 1), Action::Stop);
173 }
174
175 #[test]
176 fn herdr_starts_a_grok_child_and_a_claude_child_the_same_way() {
177 let grok = herdr_argv(
178 "grok-1",
179 Path::new("/s/personas/grok-1"),
180 Path::new("/s/run.sh"),
181 );
182 let claude = herdr_argv(
183 "claude-1",
184 Path::new("/s/personas/claude-1"),
185 Path::new("/s/run.sh"),
186 );
187 assert_eq!(
188 grok,
189 [
190 "herdr",
191 "agent",
192 "start",
193 "persona-grok-1",
194 "--no-focus",
195 "--cwd",
196 "/s/personas/grok-1",
197 "--",
198 "sh",
199 "/s/run.sh",
200 ]
201 );
202 assert_eq!(claude[3], "persona-claude-1");
203 assert_eq!(grok[1..3], claude[1..3]);
204 }
205}