1use crate::subagent::protocol::{AgentMsg, ControlMsg, SpawnPayload};
10use crate::supervisor::kill::{Ladder, LadderAction, kill_group, term_group};
11use crate::supervisor::liveness::{Health, Liveness, LivenessConfig};
12use crate::supervisor::reap::Reaped;
13use crate::supervisor::spawn::{Subagent, spawn};
14use crate::supervisor::tree::NodeId;
15use serde_json::{Value, json};
16use std::collections::HashMap;
17use std::path::PathBuf;
18use std::sync::mpsc::Sender;
19use std::time::{Duration, Instant};
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum ChildKind {
24 RootTurn {
26 ctx: String,
27 event: Option<String>,
28 reservation: Option<u64>,
29 msg_depth: u32,
34 },
35 StepTurn {
37 run: String,
38 step: String,
39 reservation: Option<u64>,
40 },
41 Think {
45 purpose: String,
46 ctx: Option<String>,
47 reply_to: Option<(NodeId, u64)>,
48 extra: Value,
49 reservation: Option<u64>,
50 },
51 Subagent { handle: String },
54}
55
56pub struct Child {
58 pub sub: Subagent,
59 pub kind: ChildKind,
60 pub started: Instant,
61 pub liveness: Liveness,
62 pub cancelled: bool,
63 pub tokens: u64,
64 pub settled: bool,
70}
71
72pub struct Children {
74 exe: PathBuf,
75 events: crate::supervisor::spawn::FrameSink,
76 reap_tx: Sender<Reaped>,
77 map: HashMap<NodeId, Child>,
78 pid_to_node: HashMap<i32, NodeId>,
79 next: u64,
80 liveness_cfg: LivenessConfig,
81 ladder: Option<Ladder>,
82 last_ping: Instant,
83 ping_seq: u64,
84 last_reaped: Option<(NodeId, ChildKind, bool)>,
91}
92
93impl Children {
94 pub fn new(
95 exe: PathBuf,
96 events: crate::supervisor::spawn::FrameSink,
97 reap_tx: Sender<Reaped>,
98 ) -> Children {
99 Children {
100 exe,
101 events,
102 reap_tx,
103 map: HashMap::new(),
104 pid_to_node: HashMap::new(),
105 next: 1,
106 liveness_cfg: LivenessConfig::from_env(),
107 ladder: None,
108 last_ping: Instant::now(),
109 ping_seq: 0,
110 last_reaped: None,
111 }
112 }
113
114 pub fn len(&self) -> usize {
115 self.map.len()
116 }
117 pub fn is_empty(&self) -> bool {
118 self.map.is_empty()
119 }
120 pub fn get(&self, node: NodeId) -> Option<&Child> {
121 self.map.get(&node)
122 }
123 pub fn get_mut(&mut self, node: NodeId) -> Option<&mut Child> {
124 self.map.get_mut(&node)
125 }
126 pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &Child)> {
127 self.map.iter()
128 }
129 pub fn count_kind(&self, f: impl Fn(&ChildKind) -> bool) -> usize {
130 self.map.values().filter(|c| f(&c.kind)).count()
131 }
132 pub fn pid_of(&self, node: NodeId) -> Option<i32> {
134 self.map.get(&node).map(|c| c.sub.pid())
135 }
136 pub fn has_pid(&self, pid: i32) -> bool {
139 self.pid_to_node.contains_key(&pid)
140 }
141 pub fn reap_sender(&self) -> Sender<Reaped> {
145 self.reap_tx.clone()
146 }
147 pub fn join_reader_of(&mut self, pid: i32) {
149 if let Some(node) = self.pid_to_node.get(&pid)
150 && let Some(c) = self.map.get_mut(node)
151 {
152 c.sub.join_reader();
153 }
154 }
155
156 pub fn spawn(
159 &mut self,
160 payload: &SpawnPayload,
161 kind: ChildKind,
162 deadline: Duration,
163 ) -> std::io::Result<NodeId> {
164 let node = NodeId(self.next);
165 self.next += 1;
166 let exe = self.exe.clone();
167 let events = self.events.clone();
168 let sub = crate::supervisor::reaper::spawn_tracked(&self.reap_tx, || {
169 spawn(&exe, payload, node, events)
170 })?;
171 let now = Instant::now();
172 let child = Child {
173 liveness: Liveness::new(
174 now,
175 now + deadline + Duration::from_secs(60),
176 self.liveness_cfg,
177 ),
178 sub,
179 kind,
180 started: now,
181 cancelled: false,
182 tokens: 0,
183 settled: false,
184 };
185 self.pid_to_node.insert(child.sub.pid(), node);
186 self.map.insert(node, child);
187 crate::obs::metrics::record_subagent_spawned();
188 Ok(node)
189 }
190
191 pub fn on_frame(&mut self, node: NodeId, msg: &AgentMsg) -> bool {
193 let Some(c) = self.map.get_mut(&node) else {
194 return false;
195 };
196 let now = Instant::now();
197 match msg {
198 AgentMsg::Pong { .. } => c.liveness.on_pong(now),
199 AgentMsg::Usage(u) => {
200 c.tokens += u.total();
201 c.liveness.on_event(now);
202 }
203 _ => c.liveness.on_event(now),
204 }
205 true
206 }
207
208 pub fn mark_settled(&mut self, node: NodeId) {
212 if let Some(c) = self.map.get_mut(&node) {
213 c.settled = true;
214 }
215 if let Some((n, _, settled)) = self.last_reaped.as_mut()
216 && *n == node
217 {
218 *settled = true;
219 }
220 }
221
222 pub fn is_settled(&self, node: NodeId) -> bool {
226 if let Some(c) = self.map.get(&node) {
227 return c.settled;
228 }
229 match &self.last_reaped {
230 Some((n, _, settled)) if *n == node => *settled,
231 _ => true,
232 }
233 }
234
235 pub fn reaped_kind(&self, node: NodeId) -> Option<ChildKind> {
238 match &self.last_reaped {
239 Some((n, kind, _)) if *n == node => Some(kind.clone()),
240 _ => None,
241 }
242 }
243
244 pub fn on_reaped(&mut self, r: &Reaped) -> Option<(NodeId, Child)> {
246 let node = self.pid_to_node.remove(&r.pid)?;
247 let mut c = self.map.remove(&node)?;
248 c.sub.mark_reaped();
249 c.liveness.on_eof();
250 self.last_reaped = Some((node, c.kind.clone(), c.settled));
251 crate::obs::metrics::record_subagent_exited(match r.outcome {
252 crate::supervisor::reap::WaitOutcome::Exited(0) => "completed",
253 crate::supervisor::reap::WaitOutcome::Exited(_) => "crashed",
254 crate::supervisor::reap::WaitOutcome::Signaled(_) => "cancelled",
255 });
256 Some((node, c))
257 }
258
259 pub fn send(&mut self, node: NodeId, msg: &ControlMsg) -> bool {
261 self.map
262 .get_mut(&node)
263 .is_some_and(|c| c.sub.send(msg).is_ok())
264 }
265
266 pub fn cancel(&mut self, node: NodeId, reason: &str) -> bool {
268 let Some(c) = self.map.get_mut(&node) else {
269 return false;
270 };
271 c.cancelled = true;
272 c.sub
273 .send(&ControlMsg::Cancel {
274 reason: reason.to_string(),
275 })
276 .is_ok()
277 }
278
279 pub fn kill(&mut self, node: NodeId) {
281 if let Some(c) = self.map.get_mut(&node) {
282 c.sub.kill();
283 }
284 }
285
286 pub fn tick(&mut self) -> Vec<(NodeId, Health)> {
289 let now = Instant::now();
290 if now.duration_since(self.last_ping) >= self.liveness_cfg.ping_interval {
291 self.last_ping = now;
292 self.ping_seq += 1;
293 let seq = self.ping_seq;
294 for c in self.map.values_mut() {
295 let _ = c.sub.send(&ControlMsg::Ping { seq });
296 }
297 }
298 self.map
299 .iter()
300 .filter_map(|(n, c)| {
301 let h = c.liveness.classify(now);
302 h.needs_teardown().then_some((*n, h))
303 })
304 .collect()
305 }
306
307 pub fn begin_drain(&mut self, reason: &str) {
309 for c in self.map.values_mut() {
310 c.cancelled = true;
311 let _ = c.sub.send(&ControlMsg::Cancel {
312 reason: reason.to_string(),
313 });
314 }
315 if self.ladder.is_none() {
316 self.ladder = Some(Ladder::with_defaults(Instant::now()));
317 }
318 }
319
320 pub fn drive_drain(&mut self, force: bool) -> bool {
322 let all_exited = self.map.is_empty();
323 let Some(ladder) = self.ladder.as_mut() else {
324 return all_exited;
325 };
326 match ladder.poll(Instant::now(), all_exited, force) {
327 LadderAction::Wait => false,
328 LadderAction::Term => {
329 for c in self.map.values() {
330 term_group(c.sub.pgid());
331 }
332 false
333 }
334 LadderAction::Kill => {
335 for c in self.map.values() {
336 kill_group(c.sub.pgid());
337 }
338 false
339 }
340 LadderAction::Done => true,
341 }
342 }
343
344 pub fn abandon(&mut self) {
346 for (_, mut c) in self.map.drain() {
347 c.sub.kill();
348 }
349 self.pid_to_node.clear();
350 }
351
352 pub fn status(&self) -> Value {
354 json!(
355 self.map
356 .iter()
357 .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}))
358 .collect::<Vec<_>>()
359 )
360 }
361}
362
363pub fn kind_label(k: &ChildKind) -> String {
364 match k {
365 ChildKind::RootTurn { ctx, .. } => format!("turn:{ctx}"),
366 ChildKind::StepTurn { run, step, .. } => format!("step:{run}/{step}"),
367 ChildKind::Think { purpose, .. } => format!("think:{purpose}"),
368 ChildKind::Subagent { handle } => format!("subagent:{handle}"),
369 }
370}