use uuid::Uuid;
use crate::brain::agent::{AgentError, AgentService};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct SubagentCounts {
pub(crate) working: usize,
pub(crate) awaiting: usize,
}
impl SubagentCounts {
pub(crate) fn total(self) -> usize {
self.working + self.awaiting
}
pub(crate) fn is_empty(self) -> bool {
self.total() == 0
}
}
pub(crate) fn subagent_waiting_phrase(agents: SubagentCounts) -> String {
let n = agents.total();
let noun = if n == 1 { "agent" } else { "agents" };
match (agents.working, agents.awaiting) {
(working, 0) => format!("{working} working {noun}"),
(0, awaiting) => format!("{awaiting} {noun} awaiting collection"),
(working, awaiting) => {
format!("{n} {noun} ({working} working, {awaiting} awaiting collection)")
}
}
}
pub(crate) fn waiting_verb(bg_count: Option<usize>, agents: SubagentCounts) -> Option<String> {
let bg = bg_count.unwrap_or(0);
if bg == 0 && agents.is_empty() {
return None;
}
let mut parts: Vec<String> = Vec::new();
if bg > 0 {
parts.push(if bg == 1 {
"1 background task".to_string()
} else {
format!("{bg} background tasks")
});
}
if !agents.is_empty() {
parts.push(subagent_waiting_phrase(agents));
}
Some(format!("Waiting for {}", parts.join(" + ")))
}
pub(crate) fn bg_indicator_for(
agent: &AgentService,
session_id: Uuid,
) -> (Option<String>, Option<usize>) {
let Some(bm) = agent.background_manager() else {
return (None, None);
};
let tasks = bm.running_tasks(session_id);
match tasks.len() {
0 => (None, Some(0)),
1 => (Some(format!("{} running", tasks[0].label)), Some(1)),
n => (Some(format!("{n} tasks running")), Some(n)),
}
}
pub(crate) fn subagent_counts_for(agent: &AgentService, session_id: Uuid) -> SubagentCounts {
let Some(mgr) = agent.subagent_manager() else {
return SubagentCounts::default();
};
let (working, awaiting) = mgr.alive_counts_for(session_id);
SubagentCounts { working, awaiting }
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum FlowOutcome {
Finished,
Failed,
TimedOut,
Cancelled,
}
impl FlowOutcome {
pub(crate) fn icon_verb(self) -> (&'static str, &'static str) {
match self {
FlowOutcome::Finished => ("✅", "Finished"),
FlowOutcome::Failed => ("❌", "Failed"),
FlowOutcome::TimedOut => ("⏱", "Timed out"),
FlowOutcome::Cancelled => ("❌", "Cancelled"),
}
}
}
pub(crate) fn outcome_for_error(err: &AgentError) -> FlowOutcome {
let text = err.to_string().to_lowercase();
if text.contains("timed out") || text.contains("timeout") || text.contains("deadline") {
FlowOutcome::TimedOut
} else {
FlowOutcome::Failed
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agents(working: usize, awaiting: usize) -> SubagentCounts {
SubagentCounts { working, awaiting }
}
#[test]
fn waiting_verb_is_none_when_nothing_is_alive() {
assert_eq!(waiting_verb(None, agents(0, 0)), None);
assert_eq!(waiting_verb(Some(0), agents(0, 0)), None);
}
#[test]
fn waiting_verb_counts_background_tasks_alone() {
assert_eq!(
waiting_verb(Some(1), agents(0, 0)).as_deref(),
Some("Waiting for 1 background task")
);
assert_eq!(
waiting_verb(Some(3), agents(0, 0)).as_deref(),
Some("Waiting for 3 background tasks")
);
}
#[test]
fn waiting_verb_counts_agents_alone() {
assert_eq!(
waiting_verb(Some(0), agents(2, 0)).as_deref(),
Some("Waiting for 2 working agents")
);
assert_eq!(
waiting_verb(None, agents(0, 1)).as_deref(),
Some("Waiting for 1 agent awaiting collection")
);
}
#[test]
fn waiting_verb_folds_both_registries_with_a_plus() {
assert_eq!(
waiting_verb(Some(1), agents(2, 0)).as_deref(),
Some("Waiting for 1 background task + 2 working agents")
);
assert_eq!(
waiting_verb(Some(2), agents(1, 1)).as_deref(),
Some("Waiting for 2 background tasks + 2 agents (1 working, 1 awaiting collection)")
);
}
#[test]
fn subagent_waiting_phrase_grammar_is_pinned() {
assert_eq!(subagent_waiting_phrase(agents(1, 0)), "1 working agent");
assert_eq!(
subagent_waiting_phrase(agents(0, 2)),
"2 agents awaiting collection"
);
assert_eq!(
subagent_waiting_phrase(agents(2, 1)),
"3 agents (2 working, 1 awaiting collection)"
);
}
}