1use crate::subagent::protocol::{AgentMsg, ControlMsg, SpawnPayload};
9use crate::supervisor::kill::{Ladder, LadderAction, kill_group, term_group};
10use crate::supervisor::liveness::{Health, Liveness, LivenessConfig};
11use crate::supervisor::reap::Reaped;
12use crate::supervisor::spawn::{Subagent, spawn};
13use crate::supervisor::tree::NodeId;
14use serde_json::{Value, json};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::mpsc::Sender;
18use std::time::{Duration, Instant};
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum ChildKind {
23 RootTurn {
25 ctx: String,
26 event: Option<String>,
27 reservation: Option<u64>,
28 },
29 StepTurn {
31 run: String,
32 step: String,
33 reservation: Option<u64>,
34 },
35 Think {
39 purpose: String,
40 ctx: Option<String>,
41 reply_to: Option<(NodeId, u64)>,
42 extra: Value,
43 reservation: Option<u64>,
44 },
45 Subagent { handle: String },
47}
48
49pub struct Child {
51 pub sub: Subagent,
52 pub kind: ChildKind,
53 pub started: Instant,
54 pub liveness: Liveness,
55 pub cancelled: bool,
56 pub tokens: u64,
57 pub settled: bool,
63}
64
65pub struct Children {
67 exe: PathBuf,
68 events: Sender<(NodeId, AgentMsg)>,
69 reap_tx: Sender<Reaped>,
70 map: HashMap<NodeId, Child>,
71 pid_to_node: HashMap<i32, NodeId>,
72 next: u64,
73 liveness_cfg: LivenessConfig,
74 ladder: Option<Ladder>,
75 last_ping: Instant,
76 ping_seq: u64,
77 last_reaped: Option<(NodeId, ChildKind, bool)>,
84}
85
86impl Children {
87 pub fn new(
88 exe: PathBuf,
89 events: Sender<(NodeId, AgentMsg)>,
90 reap_tx: Sender<Reaped>,
91 ) -> Children {
92 Children {
93 exe,
94 events,
95 reap_tx,
96 map: HashMap::new(),
97 pid_to_node: HashMap::new(),
98 next: 1,
99 liveness_cfg: LivenessConfig::from_env(),
100 ladder: None,
101 last_ping: Instant::now(),
102 ping_seq: 0,
103 last_reaped: None,
104 }
105 }
106
107 pub fn len(&self) -> usize {
108 self.map.len()
109 }
110 pub fn is_empty(&self) -> bool {
111 self.map.is_empty()
112 }
113 pub fn get(&self, node: NodeId) -> Option<&Child> {
114 self.map.get(&node)
115 }
116 pub fn get_mut(&mut self, node: NodeId) -> Option<&mut Child> {
117 self.map.get_mut(&node)
118 }
119 pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &Child)> {
120 self.map.iter()
121 }
122 pub fn count_kind(&self, f: impl Fn(&ChildKind) -> bool) -> usize {
123 self.map.values().filter(|c| f(&c.kind)).count()
124 }
125
126 pub fn spawn(
129 &mut self,
130 payload: &SpawnPayload,
131 kind: ChildKind,
132 deadline: Duration,
133 ) -> std::io::Result<NodeId> {
134 let node = NodeId(self.next);
135 self.next += 1;
136 let exe = self.exe.clone();
137 let events = self.events.clone();
138 let sub = crate::supervisor::reaper::spawn_tracked(&self.reap_tx, || {
139 spawn(&exe, payload, node, events)
140 })?;
141 let now = Instant::now();
142 let child = Child {
143 liveness: Liveness::new(
144 now,
145 now + deadline + Duration::from_secs(60),
146 self.liveness_cfg,
147 ),
148 sub,
149 kind,
150 started: now,
151 cancelled: false,
152 tokens: 0,
153 settled: false,
154 };
155 self.pid_to_node.insert(child.sub.pid(), node);
156 self.map.insert(node, child);
157 crate::obs::metrics::record_subagent_spawned();
158 Ok(node)
159 }
160
161 pub fn on_frame(&mut self, node: NodeId, msg: &AgentMsg) -> bool {
163 let Some(c) = self.map.get_mut(&node) else {
164 return false;
165 };
166 let now = Instant::now();
167 match msg {
168 AgentMsg::Pong { .. } => c.liveness.on_pong(now),
169 AgentMsg::Usage(u) => {
170 c.tokens += u.total();
171 c.liveness.on_event(now);
172 }
173 _ => c.liveness.on_event(now),
174 }
175 true
176 }
177
178 pub fn mark_settled(&mut self, node: NodeId) {
182 if let Some(c) = self.map.get_mut(&node) {
183 c.settled = true;
184 }
185 if let Some((n, _, settled)) = self.last_reaped.as_mut()
186 && *n == node
187 {
188 *settled = true;
189 }
190 }
191
192 pub fn is_settled(&self, node: NodeId) -> bool {
196 if let Some(c) = self.map.get(&node) {
197 return c.settled;
198 }
199 match &self.last_reaped {
200 Some((n, _, settled)) if *n == node => *settled,
201 _ => true,
202 }
203 }
204
205 pub fn reaped_kind(&self, node: NodeId) -> Option<ChildKind> {
208 match &self.last_reaped {
209 Some((n, kind, _)) if *n == node => Some(kind.clone()),
210 _ => None,
211 }
212 }
213
214 pub fn on_reaped(&mut self, r: &Reaped) -> Option<(NodeId, Child)> {
216 let node = self.pid_to_node.remove(&r.pid)?;
217 let mut c = self.map.remove(&node)?;
218 c.sub.mark_reaped();
219 c.liveness.on_eof();
220 self.last_reaped = Some((node, c.kind.clone(), c.settled));
221 crate::obs::metrics::record_subagent_exited(match r.outcome {
222 crate::supervisor::reap::WaitOutcome::Exited(0) => "completed",
223 crate::supervisor::reap::WaitOutcome::Exited(_) => "crashed",
224 crate::supervisor::reap::WaitOutcome::Signaled(_) => "cancelled",
225 });
226 Some((node, c))
227 }
228
229 pub fn send(&mut self, node: NodeId, msg: &ControlMsg) -> bool {
231 self.map
232 .get_mut(&node)
233 .is_some_and(|c| c.sub.send(msg).is_ok())
234 }
235
236 pub fn cancel(&mut self, node: NodeId, reason: &str) -> bool {
238 let Some(c) = self.map.get_mut(&node) else {
239 return false;
240 };
241 c.cancelled = true;
242 c.sub
243 .send(&ControlMsg::Cancel {
244 reason: reason.to_string(),
245 })
246 .is_ok()
247 }
248
249 pub fn kill(&mut self, node: NodeId) {
251 if let Some(c) = self.map.get_mut(&node) {
252 c.sub.kill();
253 }
254 }
255
256 pub fn tick(&mut self) -> Vec<(NodeId, Health)> {
259 let now = Instant::now();
260 if now.duration_since(self.last_ping) >= self.liveness_cfg.ping_interval {
261 self.last_ping = now;
262 self.ping_seq += 1;
263 let seq = self.ping_seq;
264 for c in self.map.values_mut() {
265 let _ = c.sub.send(&ControlMsg::Ping { seq });
266 }
267 }
268 self.map
269 .iter()
270 .filter_map(|(n, c)| {
271 let h = c.liveness.classify(now);
272 h.needs_teardown().then_some((*n, h))
273 })
274 .collect()
275 }
276
277 pub fn begin_drain(&mut self, reason: &str) {
279 for c in self.map.values_mut() {
280 c.cancelled = true;
281 let _ = c.sub.send(&ControlMsg::Cancel {
282 reason: reason.to_string(),
283 });
284 }
285 if self.ladder.is_none() {
286 self.ladder = Some(Ladder::with_defaults(Instant::now()));
287 }
288 }
289
290 pub fn drive_drain(&mut self, force: bool) -> bool {
292 let all_exited = self.map.is_empty();
293 let Some(ladder) = self.ladder.as_mut() else {
294 return all_exited;
295 };
296 match ladder.poll(Instant::now(), all_exited, force) {
297 LadderAction::Wait => false,
298 LadderAction::Term => {
299 for c in self.map.values() {
300 term_group(c.sub.pgid());
301 }
302 false
303 }
304 LadderAction::Kill => {
305 for c in self.map.values() {
306 kill_group(c.sub.pgid());
307 }
308 false
309 }
310 LadderAction::Done => true,
311 }
312 }
313
314 pub fn abandon(&mut self) {
316 for (_, mut c) in self.map.drain() {
317 c.sub.kill();
318 }
319 self.pid_to_node.clear();
320 }
321
322 pub fn status(&self) -> Value {
324 json!(
325 self.map
326 .iter()
327 .map(|(n, c)| json!({"node": n.0, "pid": c.sub.pid(), "kind": kind_label(&c.kind), "age_ms": c.started.elapsed().as_millis() as u64, "tokens": c.tokens, "cancelled": c.cancelled}))
328 .collect::<Vec<_>>()
329 )
330 }
331}
332
333pub fn kind_label(k: &ChildKind) -> String {
334 match k {
335 ChildKind::RootTurn { ctx, .. } => format!("turn:{ctx}"),
336 ChildKind::StepTurn { run, step, .. } => format!("step:{run}/{step}"),
337 ChildKind::Think { purpose, .. } => format!("think:{purpose}"),
338 ChildKind::Subagent { handle } => format!("subagent:{handle}"),
339 }
340}