1use crate::runner::AgentId;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10use tokio::sync::{Notify, watch};
11use tokio_util::sync::CancellationToken;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RunStatus {
15 Running,
16 Paused,
17 Completed,
18 Failed,
19 Stopped,
20}
21
22impl RunStatus {
23 pub fn is_finished(self) -> bool {
24 matches!(
25 self,
26 RunStatus::Completed | RunStatus::Failed | RunStatus::Stopped
27 )
28 }
29
30 pub fn label(self) -> &'static str {
31 match self {
32 RunStatus::Running => "running",
33 RunStatus::Paused => "paused",
34 RunStatus::Completed => "completed",
35 RunStatus::Failed => "failed",
36 RunStatus::Stopped => "stopped",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum AgentStatus {
43 Queued,
44 Running,
45 Completed,
46 Failed,
47 Stopped,
48 Restarting,
50}
51
52impl AgentStatus {
53 pub fn is_finished(self) -> bool {
54 !matches!(
55 self,
56 AgentStatus::Queued | AgentStatus::Running | AgentStatus::Restarting
57 )
58 }
59
60 pub fn label(self) -> &'static str {
61 match self {
62 AgentStatus::Queued => "queued",
63 AgentStatus::Running => "running",
64 AgentStatus::Completed => "completed",
65 AgentStatus::Failed => "failed",
66 AgentStatus::Stopped => "stopped",
67 AgentStatus::Restarting => "restarting",
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct AgentSnapshot {
75 pub id: AgentId,
76 pub label: String,
77 pub status: AgentStatus,
78 pub tokens: u64,
79 pub elapsed: Duration,
80 pub prompt: String,
81 pub result: Option<String>,
82 pub error: Option<String>,
83}
84
85#[derive(Debug, Clone)]
87pub struct PhaseSnapshot {
88 pub title: String,
89 pub agents: Vec<AgentSnapshot>,
90 pub tokens: u64,
91}
92
93impl PhaseSnapshot {
94 pub fn finished_agents(&self) -> usize {
95 self.agents
96 .iter()
97 .filter(|agent| agent.status.is_finished())
98 .count()
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct RunSnapshot {
105 pub status: RunStatus,
106 pub elapsed: Duration,
107 pub log: Vec<String>,
108 pub phases: Vec<PhaseSnapshot>,
109 pub tokens: u64,
110 pub error: Option<String>,
111}
112
113impl RunSnapshot {
114 pub fn total_agents(&self) -> usize {
115 self.phases.iter().map(|phase| phase.agents.len()).sum()
116 }
117
118 pub fn finished_agents(&self) -> usize {
119 self.phases.iter().map(PhaseSnapshot::finished_agents).sum()
120 }
121
122 pub fn active_phase(&self) -> Option<&PhaseSnapshot> {
124 self.phases
125 .iter()
126 .find(|phase| phase.agents.iter().any(|agent| !agent.status.is_finished()))
127 .or_else(|| {
128 self.phases
129 .iter()
130 .rev()
131 .find(|phase| !phase.agents.is_empty())
132 })
133 }
134}
135
136struct AgentRecord {
137 id: AgentId,
138 label: String,
139 status: AgentStatus,
140 tokens: u64,
141 started: Option<Instant>,
142 elapsed: Duration,
143 prompt: String,
144 result: Option<String>,
145 error: Option<String>,
146 cancel: CancellationToken,
147}
148
149struct PhaseRecord {
150 title: String,
151 agents: Vec<AgentRecord>,
152}
153
154struct Inner {
155 status: RunStatus,
156 started: Instant,
157 finished: Option<Instant>,
158 log: Vec<String>,
159 phases: Vec<PhaseRecord>,
160 current_phase: usize,
161 error: Option<String>,
162}
163
164pub(crate) struct RunState {
166 inner: Mutex<Inner>,
167 version: watch::Sender<u64>,
168 cached: Mutex<Option<(u64, Arc<RunSnapshot>)>>,
170 resumed: Notify,
171 stop: CancellationToken,
172}
173
174pub(crate) const DEFAULT_PHASE: &str = "Workflow";
176
177impl RunState {
178 pub(crate) fn new(declared_phases: &[String]) -> Arc<RunState> {
179 let mut phases: Vec<PhaseRecord> = declared_phases
180 .iter()
181 .map(|title| PhaseRecord {
182 title: title.clone(),
183 agents: Vec::new(),
184 })
185 .collect();
186 if phases.is_empty() {
187 phases.push(PhaseRecord {
188 title: DEFAULT_PHASE.to_string(),
189 agents: Vec::new(),
190 });
191 }
192 let (version, _) = watch::channel(0);
193 Arc::new(RunState {
194 inner: Mutex::new(Inner {
195 status: RunStatus::Running,
196 started: Instant::now(),
197 finished: None,
198 log: Vec::new(),
199 phases,
200 current_phase: 0,
201 error: None,
202 }),
203 version,
204 cached: Mutex::new(None),
205 resumed: Notify::new(),
206 stop: CancellationToken::new(),
207 })
208 }
209
210 pub(crate) fn stop_token(&self) -> CancellationToken {
211 self.stop.clone()
212 }
213
214 pub fn subscribe(&self) -> watch::Receiver<u64> {
215 self.version.subscribe()
216 }
217
218 fn changed(&self) {
219 if let Ok(mut cached) = self.cached.lock() {
220 *cached = None;
221 }
222 self.version
223 .send_modify(|version| *version = version.wrapping_add(1));
224 }
225
226 pub(crate) fn set_phase(&self, title: &str) {
228 let Ok(mut inner) = self.inner.lock() else {
229 return;
230 };
231 match inner.phases.iter().position(|phase| phase.title == title) {
232 Some(index) => inner.current_phase = index,
233 None => {
234 inner.phases.push(PhaseRecord {
235 title: title.to_string(),
236 agents: Vec::new(),
237 });
238 inner.current_phase = inner.phases.len() - 1;
239 }
240 }
241 drop(inner);
242 self.changed();
243 }
244
245 pub(crate) fn current_phase_title(&self) -> String {
246 let Ok(inner) = self.inner.lock() else {
247 return DEFAULT_PHASE.to_string();
248 };
249 inner
250 .phases
251 .get(inner.current_phase)
252 .map(|phase| phase.title.clone())
253 .unwrap_or_else(|| DEFAULT_PHASE.to_string())
254 }
255
256 pub(crate) fn log(&self, message: String) {
257 if let Ok(mut inner) = self.inner.lock() {
258 inner.log.push(message);
259 }
260 self.changed();
261 }
262
263 pub(crate) fn register_agent(
265 &self,
266 id: AgentId,
267 label: String,
268 prompt: String,
269 ) -> CancellationToken {
270 let cancel = self.stop.child_token();
271 if let Ok(mut inner) = self.inner.lock() {
272 let phase = inner.current_phase;
273 if let Some(phase) = inner.phases.get_mut(phase) {
274 phase.agents.push(AgentRecord {
275 id,
276 label,
277 status: AgentStatus::Queued,
278 tokens: 0,
279 started: None,
280 elapsed: Duration::ZERO,
281 prompt,
282 result: None,
283 error: None,
284 cancel: cancel.clone(),
285 });
286 }
287 }
288 self.changed();
289 cancel
290 }
291
292 pub(crate) fn agent_started(&self, id: AgentId) {
293 self.with_agent(id, |agent| {
294 if agent.status != AgentStatus::Restarting {
296 agent.status = AgentStatus::Running;
297 }
298 agent.started = Some(Instant::now());
299 });
300 }
301
302 pub(crate) fn agent_finished(
303 &self,
304 id: AgentId,
305 status: AgentStatus,
306 result: Option<String>,
307 error: Option<String>,
308 tokens: u64,
309 ) {
310 self.with_agent(id, |agent| {
311 agent.status = status;
312 agent.result = result;
313 agent.error = error;
314 agent.tokens = tokens;
315 if let Some(started) = agent.started {
316 agent.elapsed = started.elapsed();
317 }
318 });
319 }
320
321 fn with_agent(&self, id: AgentId, apply: impl FnOnce(&mut AgentRecord)) {
322 if let Ok(mut inner) = self.inner.lock()
323 && let Some(agent) = inner
324 .phases
325 .iter_mut()
326 .flat_map(|phase| phase.agents.iter_mut())
327 .find(|agent| agent.id == id)
328 {
329 apply(agent);
330 }
331 self.changed();
332 }
333
334 pub(crate) fn finish(&self, status: RunStatus, error: Option<String>) {
335 if let Ok(mut inner) = self.inner.lock() {
336 inner.status = status;
337 inner.error = error;
338 inner.finished = Some(Instant::now());
339 }
340 self.changed();
341 }
342
343 pub fn pause(&self) {
346 if let Ok(mut inner) = self.inner.lock()
347 && inner.status == RunStatus::Running
348 {
349 inner.status = RunStatus::Paused;
350 drop(inner);
351 self.changed();
352 }
353 }
354
355 pub fn resume(&self) {
356 if let Ok(mut inner) = self.inner.lock()
357 && inner.status == RunStatus::Paused
358 {
359 inner.status = RunStatus::Running;
360 drop(inner);
361 self.resumed.notify_waiters();
362 self.changed();
363 }
364 }
365
366 pub fn is_paused(&self) -> bool {
367 self.inner
368 .lock()
369 .is_ok_and(|inner| inner.status == RunStatus::Paused)
370 }
371
372 pub(crate) async fn wait_while_paused(&self) -> bool {
375 while self.is_paused() {
376 if self.stop.is_cancelled() {
377 return false;
378 }
379 let resumed = self.resumed.notified();
380 if !self.is_paused() {
383 break;
384 }
385 tokio::select! {
386 _ = resumed => {}
387 _ = self.stop.cancelled() => return false,
388 }
389 }
390 !self.stop.is_cancelled()
391 }
392
393 pub fn stop(&self) {
394 self.stop.cancel();
395 self.resume();
397 self.changed();
398 }
399
400 pub fn stop_agent(&self, id: AgentId) {
401 if let Ok(inner) = self.inner.lock()
402 && let Some(agent) = inner
403 .phases
404 .iter()
405 .flat_map(|phase| phase.agents.iter())
406 .find(|agent| agent.id == id)
407 {
408 agent.cancel.cancel();
409 }
410 self.changed();
411 }
412
413 pub fn restart_agent(&self, id: AgentId) {
419 if let Ok(mut inner) = self.inner.lock()
420 && let Some(agent) = inner
421 .phases
422 .iter_mut()
423 .flat_map(|phase| phase.agents.iter_mut())
424 .find(|agent| agent.id == id)
425 && matches!(agent.status, AgentStatus::Queued | AgentStatus::Running)
426 {
427 agent.status = AgentStatus::Restarting;
428 agent.cancel.cancel();
429 }
430 self.changed();
431 }
432
433 pub(crate) fn take_restart(&self, id: AgentId) -> Option<CancellationToken> {
435 if self.stop.is_cancelled() {
436 return None;
437 }
438 let mut inner = self.inner.lock().ok()?;
439 let agent = inner
440 .phases
441 .iter_mut()
442 .flat_map(|phase| phase.agents.iter_mut())
443 .find(|agent| agent.id == id && agent.status == AgentStatus::Restarting)?;
444 agent.status = AgentStatus::Queued;
445 agent.started = None;
446 agent.elapsed = Duration::ZERO;
447 agent.result = None;
448 agent.error = None;
449 agent.cancel = self.stop.child_token();
450 let cancel = agent.cancel.clone();
451 drop(inner);
452 self.changed();
453 Some(cancel)
454 }
455
456 pub fn snapshot(&self) -> Arc<RunSnapshot> {
459 let version = *self.version.borrow();
460 if let Ok(cached) = self.cached.lock()
461 && let Some((cached_version, snapshot)) = cached.as_ref()
462 && *cached_version == version
463 {
464 return snapshot.clone();
465 }
466 let snapshot = Arc::new(self.build_snapshot());
467 if let Ok(mut cached) = self.cached.lock() {
468 *cached = Some((version, snapshot.clone()));
469 }
470 snapshot
471 }
472
473 fn build_snapshot(&self) -> RunSnapshot {
474 let Ok(inner) = self.inner.lock() else {
475 return RunSnapshot {
476 status: RunStatus::Failed,
477 elapsed: Duration::ZERO,
478 log: Vec::new(),
479 phases: Vec::new(),
480 tokens: 0,
481 error: Some("the run state was poisoned".into()),
482 };
483 };
484 let mut total_tokens = 0;
485 let phases = inner
486 .phases
487 .iter()
488 .map(|phase| {
489 let mut phase_tokens = 0;
490 let agents = phase
491 .agents
492 .iter()
493 .map(|agent| {
494 phase_tokens += agent.tokens;
495 AgentSnapshot {
496 id: agent.id,
497 label: agent.label.clone(),
498 status: agent.status,
499 tokens: agent.tokens,
500 elapsed: match (agent.status.is_finished(), agent.started) {
501 (false, Some(started)) => started.elapsed(),
502 _ => agent.elapsed,
503 },
504 prompt: agent.prompt.clone(),
505 result: agent.result.clone(),
506 error: agent.error.clone(),
507 }
508 })
509 .collect();
510 total_tokens += phase_tokens;
511 PhaseSnapshot {
512 title: phase.title.clone(),
513 agents,
514 tokens: phase_tokens,
515 }
516 })
517 .collect();
518 RunSnapshot {
519 status: inner.status,
520 elapsed: inner
521 .finished
522 .unwrap_or_else(Instant::now)
523 .saturating_duration_since(inner.started),
524 log: inner.log.clone(),
525 phases,
526 tokens: total_tokens,
527 error: inner.error.clone(),
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn declared_phases_appear_before_any_agent_runs() {
538 let state = RunState::new(&["Discover".into(), "Audit".into()]);
539 let snapshot = state.snapshot();
540 assert_eq!(snapshot.phases.len(), 2);
541 assert_eq!(snapshot.phases[0].title, "Discover");
542 assert_eq!(snapshot.total_agents(), 0);
543 }
544
545 #[test]
546 fn a_script_without_phases_gets_one_default_phase() {
547 let state = RunState::new(&[]);
548 assert_eq!(state.snapshot().phases[0].title, DEFAULT_PHASE);
549 }
550
551 #[test]
552 fn an_undeclared_phase_is_added_when_it_is_first_used() {
553 let state = RunState::new(&["Discover".into()]);
554 state.set_phase("Verify");
555 state.register_agent(0, "check".into(), "prompt".into());
556 let snapshot = state.snapshot();
557 assert_eq!(snapshot.phases.len(), 2);
558 assert_eq!(snapshot.phases[1].title, "Verify");
559 assert_eq!(snapshot.phases[1].agents.len(), 1);
560 }
561
562 #[test]
563 fn snapshots_are_shared_until_the_state_changes() {
564 let state = RunState::new(&[]);
565 let first = state.snapshot();
566 let second = state.snapshot();
567 assert!(Arc::ptr_eq(&first, &second));
568
569 state.register_agent(0, "one".into(), "prompt".into());
570 let third = state.snapshot();
571 assert!(!Arc::ptr_eq(&first, &third));
572 }
573
574 #[test]
575 fn the_version_moves_on_every_change_so_viewers_never_poll() {
576 let state = RunState::new(&[]);
577 let mut versions = state.subscribe();
578 assert_eq!(*versions.borrow_and_update(), 0);
579 state.log("started".into());
580 assert!(versions.has_changed().unwrap_or(false));
581 }
582
583 #[test]
584 fn finished_counts_ignore_agents_still_working() {
585 let state = RunState::new(&[]);
586 state.register_agent(0, "one".into(), "p".into());
587 state.register_agent(1, "two".into(), "p".into());
588 state.agent_started(0);
589 state.agent_finished(0, AgentStatus::Completed, Some("ok".into()), None, 12);
590 let snapshot = state.snapshot();
591 assert_eq!(snapshot.total_agents(), 2);
592 assert_eq!(snapshot.finished_agents(), 1);
593 assert_eq!(snapshot.tokens, 12);
594 }
595
596 #[tokio::test]
597 async fn a_paused_run_continues_after_resume() {
598 let state = RunState::new(&[]);
599 state.pause();
600 assert!(state.is_paused());
601 assert_eq!(state.snapshot().status, RunStatus::Paused);
602
603 let waiter = state.clone();
604 let handle = tokio::spawn(async move { waiter.wait_while_paused().await });
605 tokio::task::yield_now().await;
606 state.resume();
607 assert!(handle.await.unwrap());
608 assert_eq!(state.snapshot().status, RunStatus::Running);
609 }
610
611 #[test]
612 fn a_restart_requested_before_the_call_begins_still_restarts_it() {
613 let state = RunState::new(&[]);
614 state.register_agent(0, "agent".into(), "prompt".into());
615 state.restart_agent(0);
616 state.agent_started(0);
617 assert!(state.take_restart(0).is_some());
618 assert_eq!(
619 state.snapshot().phases[0].agents[0].status,
620 AgentStatus::Queued
621 );
622 }
623
624 #[test]
625 fn pausing_a_finished_run_changes_nothing() {
626 let state = RunState::new(&[]);
627 state.finish(RunStatus::Completed, None);
628 state.pause();
629 assert!(!state.is_paused());
630 assert_eq!(state.snapshot().status, RunStatus::Completed);
631 }
632
633 #[tokio::test]
634 async fn stopping_a_paused_run_releases_it() {
635 let state = RunState::new(&[]);
636 state.pause();
637 let waiter = state.clone();
638 let handle = tokio::spawn(async move { waiter.wait_while_paused().await });
639 tokio::task::yield_now().await;
640 state.stop();
641 assert!(!handle.await.unwrap());
643 }
644}