From bbcab3bbc36d93b71fcdde867b6ad7582b12bae8 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 22 May 2026 12:56:04 -0600 Subject: [PATCH] feat: Removed indicatif spinners. The UX just won't stop clobbering for parallel graph nodes --- Cargo.lock | 106 ++++++++++++++-------------------------- Cargo.toml | 1 - src/graph/executor.rs | 85 +++++++++++++++++++++----------- src/graph/logging.rs | 111 ++++++++++++++++++++++++++++++++++++++++++ src/graph/map.rs | 16 ------ src/graph/mod.rs | 1 - src/graph/progress.rs | 71 --------------------------- 7 files changed, 204 insertions(+), 187 deletions(-) delete mode 100644 src/graph/progress.rs diff --git a/Cargo.lock b/Cargo.lock index 029450c..cd4c9ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,13 +183,13 @@ dependencies = [ [[package]] name = "argc" -version = "1.23.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed45b16ecde78101a90574aeeac8e61749203b7dba72a4abb70a870e92af0047" +checksum = "17060e608fbc0809d62a996a65cdee9e7c441a979f40f2d1d2fbdce9eef60dad" dependencies = [ "anyhow", "base64", - "convert_case 0.8.0", + "convert_case 0.11.0", "dirs", "either", "indexmap 2.14.0", @@ -203,6 +203,7 @@ dependencies = [ "shell-words", "textwrap", "threadpool", + "unicode-width", "which", ] @@ -271,9 +272,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-config" @@ -950,9 +951,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -1264,19 +1265,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - [[package]] name = "console" version = "0.16.3" @@ -1297,18 +1285,18 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "convert_case" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] [[package]] name = "convert_case" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] @@ -1671,7 +1659,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" dependencies = [ - "console 0.16.3", + "console", "shell-words", "tempfile", "zeroize", @@ -1823,9 +1811,9 @@ checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -2867,26 +2855,13 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console 0.15.11", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - [[package]] name = "indicatif" version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ - "console 0.16.3", + "console", "portable-atomic", "unicode-width", "unit-prefix", @@ -3085,9 +3060,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -3277,7 +3252,6 @@ dependencies = [ "html_to_markdown", "http 1.4.0", "indexmap 2.14.0", - "indicatif 0.17.11", "indoc", "inquire", "is-terminal", @@ -3605,12 +3579,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "objc2" version = "0.6.4" @@ -3993,9 +3961,9 @@ dependencies = [ [[package]] name = "pastey" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "path-absolutize" @@ -4744,9 +4712,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rpassword" @@ -5142,7 +5110,7 @@ dependencies = [ "either", "flate2", "http 1.4.0", - "indicatif 0.18.4", + "indicatif", "log", "quick-xml 0.38.4", "regex", @@ -5218,9 +5186,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap 2.14.0", "itoa", @@ -6545,9 +6513,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -6558,9 +6526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -6568,9 +6536,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6578,9 +6546,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -6591,9 +6559,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -6730,9 +6698,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 9f4535b..e7f8a5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,6 @@ unicode-width = "0.2.0" async-recursion = "1.1.1" http = "1.1.0" indexmap = { version = "2.2.6", features = ["serde"] } -indicatif = "0.17" hmac = "0.12.1" aws-smithy-eventstream = "0.60.4" urlencoding = "2.1.3" diff --git a/src/graph/executor.rs b/src/graph/executor.rs index 349e9d7..46398c6 100644 --- a/src/graph/executor.rs +++ b/src/graph/executor.rs @@ -1,8 +1,7 @@ use super::agent::AgentNodeExecutor; use super::llm::{LlmExecutionOutcome, LlmNodeExecutor}; -use super::logging::{GraphLogger, node_type_label}; +use super::logging::{GraphLogger, narrate_node_complete, narrate_node_failed}; use super::map::MapNodeExecutor; -use super::progress::{BranchProgressHandle, BranchProgressTracker}; use super::rag::RagNodeExecutor; use super::script::ScriptExecutor; use super::staging::BranchWrites; @@ -152,14 +151,15 @@ impl GraphExecutor { let semaphore = Arc::new(Semaphore::new(max_concurrency)); let frontier_size = frontier.len(); - let is_nested = ctx.current_depth > 0; - let has_progress_nodes = frontier.iter().any(|nid| { - graph.get_node(nid).is_some_and(|n| { - !matches!(n.node_type, NodeType::Approval(_) | NodeType::Input(_)) - }) - }); - let progress_tracker = - (has_progress_nodes && !is_nested).then(BranchProgressTracker::new); + let in_super_step = frontier_size > 1; + let silent = logger.silent(); + + if in_super_step { + let mut branches = sorted_frontier(&frontier); + branches.sort(); + logger.super_step_start(&branches); + } + let mut branch_tasks = Vec::with_capacity(frontier_size); for node_id in &frontier { let node = graph @@ -168,34 +168,31 @@ impl GraphExecutor { anyhow!("Node '{}' not found in graph '{}'", node_id, graph.name) })? .clone(); + logger.node_start(&node, in_super_step); let branch_state = state.fork_for_branch_state(); let mut branch_ctx = ctx.fork_for_branch(); - branch_ctx.render_mode = RenderMode::Silent; + if in_super_step { + branch_ctx.render_mode = RenderMode::Silent; + } let script_exec_clone = script_executor.clone(); let graph_clone = Arc::clone(&graph); let current = node_id.clone(); let sem_clone = semaphore.clone(); let abort_clone = abort_signal.clone(); - let progress_handle = match ( - matches!(node.node_type, NodeType::Approval(_) | NodeType::Input(_)), - &progress_tracker, - ) { - (false, Some(tracker)) => { - tracker.add_branch(&format!("{} ({})", node_id, node_type_label(&node))) - } - _ => BranchProgressHandle::disabled(), - }; let task = tokio::spawn(async move { - let mut progress_handle = Some(progress_handle); let _permit = sem_clone .acquire() .await .expect("semaphore should not be closed"); if abort_clone.aborted() { - if let Some(h) = progress_handle.take() { - h.fail("aborted"); - } + narrate_node_failed( + silent, + &node, + Duration::default(), + "aborted", + in_super_step, + ); return ( current.clone(), branch_state, @@ -214,10 +211,38 @@ impl GraphExecutor { }; let result = step(&node, &mut state, &mut ctx, &step_ctx, ¤t).await; let elapsed = node_start.elapsed(); - if let Some(h) = progress_handle.take() { - match &result { - Ok(_) => h.complete(), - Err(e) => h.fail(&e.to_string()), + match &result { + Ok(StepResult::Continue(targets)) => { + let route = if targets.is_empty() { + None + } else { + Some(targets.join(", ")) + }; + narrate_node_complete( + silent, + &node, + elapsed, + route.as_deref(), + in_super_step, + ); + } + Ok(StepResult::End(_)) => { + narrate_node_complete( + silent, + &node, + elapsed, + Some("END"), + in_super_step, + ); + } + Err(e) => { + narrate_node_failed( + silent, + &node, + elapsed, + &e.to_string(), + in_super_step, + ); } } (current, state, result, elapsed) @@ -226,7 +251,6 @@ impl GraphExecutor { } let joined = join_all(branch_tasks).await; - drop(progress_tracker); let mut branch_writes: Vec = Vec::new(); let mut next_frontier: HashSet = HashSet::new(); @@ -294,6 +318,9 @@ impl GraphExecutor { return Ok(output); } + if in_super_step { + logger.super_step_end(&sorted_frontier(&next_frontier)); + } frontier = next_frontier; } } diff --git a/src/graph/logging.rs b/src/graph/logging.rs index de7d9bd..782a761 100644 --- a/src/graph/logging.rs +++ b/src/graph/logging.rs @@ -1,10 +1,24 @@ use super::state::StateManager; use super::types::{Node, NodeType}; use crate::utils::dimmed_text; +use chrono::Local; use indexmap::IndexMap; use std::cmp::Reverse; use std::time::Duration; +fn ts() -> String { + Local::now().format("%H:%M:%S").to_string() +} + +fn fmt_secs(elapsed: Duration) -> String { + let secs = elapsed.as_secs_f64(); + if secs < 1.0 { + format!("{}ms", elapsed.as_millis()) + } else { + format!("{secs:.2}s") + } +} + #[derive(Debug, Clone, Default)] struct NodeTiming { count: usize, @@ -80,6 +94,43 @@ impl GraphLogger { ); } + pub fn silent(&self) -> bool { + self.silent + } + + pub fn node_start(&self, node: &Node, in_super_step: bool) { + narrate_node_start(self.silent, node, in_super_step); + } + + pub fn super_step_start(&self, branches: &[String]) { + if self.silent { + return; + } + eprintln!( + "{}", + dimmed_text(&format!( + "▸ {} super-step start: {}", + ts(), + branches.join(", ") + )) + ); + } + + pub fn super_step_end(&self, targets: &[String]) { + if self.silent { + return; + } + let route = if targets.is_empty() { + String::new() + } else { + format!(" -> {}", targets.join(", ")) + }; + eprintln!( + "{}", + dimmed_text(&format!("▸ {} super-step end{route}", ts())) + ); + } + pub fn record_timing(&mut self, node_id: &str, elapsed: Duration) { self.timings .entry(node_id.to_string()) @@ -144,6 +195,66 @@ impl GraphLogger { } } +pub fn narrate_node_start(silent: bool, node: &Node, in_super_step: bool) { + if silent { + return; + } + let indent = if in_super_step { " " } else { "" }; + let label = node_type_label(node); + eprintln!( + "{}", + dimmed_text(&format!("▸ {} {indent}{} ({label}) start", ts(), node.id)) + ); +} + +pub fn narrate_node_complete( + silent: bool, + node: &Node, + elapsed: Duration, + next_target: Option<&str>, + in_super_step: bool, +) { + if silent { + return; + } + let indent = if in_super_step { " " } else { "" }; + let label = node_type_label(node); + let dur = fmt_secs(elapsed); + let route = next_target.map(|t| format!(" -> {t}")).unwrap_or_default(); + eprintln!( + "{}", + dimmed_text(&format!( + "▸ {} {indent}{} ({label}) done in {dur}{route}", + ts(), + node.id + )) + ); +} + +pub fn narrate_node_failed( + silent: bool, + node: &Node, + elapsed: Duration, + err: &str, + in_super_step: bool, +) { + if silent { + return; + } + let indent = if in_super_step { " " } else { "" }; + let label = node_type_label(node); + let dur = fmt_secs(elapsed); + let excerpt: String = err.chars().take(120).collect(); + eprintln!( + "{}", + dimmed_text(&format!( + "▸ {} {indent}{} ({label}) FAILED in {dur} -- {excerpt}", + ts(), + node.id + )) + ); +} + pub(super) fn node_type_label(node: &Node) -> &'static str { match &node.node_type { NodeType::Agent(_) => "agent", diff --git a/src/graph/map.rs b/src/graph/map.rs index 5045abe..403011a 100644 --- a/src/graph/map.rs +++ b/src/graph/map.rs @@ -1,7 +1,6 @@ use super::agent::AgentNodeExecutor; use super::executor::StepContext; use super::llm::LlmNodeExecutor; -use super::progress::{BranchProgressHandle, BranchProgressTracker}; use super::rag::RagNodeExecutor; use super::state::StateManager; use super::types::{MapNode, NodeType}; @@ -54,7 +53,6 @@ impl MapNodeExecutor { .unwrap_or(step_ctx.max_concurrency) .max(1); let semaphore = Arc::new(Semaphore::new(max_conc)); - let progress_tracker = BranchProgressTracker::new(); let mut sub_tasks = Vec::with_capacity(items.len()); for (idx, item) in items.iter().enumerate() { @@ -68,21 +66,15 @@ impl MapNodeExecutor { let sub_branch_id = node.branch.clone(); let sem = semaphore.clone(); let abort = step_ctx.abort_signal.clone(); - let progress_handle: BranchProgressHandle = - progress_tracker.add_branch(&format!("{}[{idx}]", node.branch)); sub_state.state_mut().set(as_name, item); let task = tokio::spawn(async move { - let mut progress_handle = Some(progress_handle); let _permit = sem .acquire() .await .expect("map semaphore should not be closed"); if abort.aborted() { - if let Some(h) = progress_handle.take() { - h.fail("aborted"); - } return ( idx, sub_state, @@ -110,20 +102,12 @@ impl MapNodeExecutor { )), }; - if let Some(h) = progress_handle.take() { - match &exec_result { - Ok(_) => h.complete(), - Err(e) => h.fail(&e.to_string()), - } - } - (idx, state, exec_result) }); sub_tasks.push(task); } let joined = join_all(sub_tasks).await; - drop(progress_tracker); // Collect outputs keyed by input index so order is preserved regardless of finish order. let mut outputs: HashMap = HashMap::new(); diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 6bd1cd0..b5355d8 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -5,7 +5,6 @@ pub mod llm; pub mod logging; pub mod map; pub mod parser; -pub mod progress; pub mod rag; pub mod reducer; pub mod script; diff --git a/src/graph/progress.rs b/src/graph/progress.rs deleted file mode 100644 index c853240..0000000 --- a/src/graph/progress.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::utils::IS_STDOUT_TERMINAL; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use std::sync::LazyLock; -use std::time::Duration; - -const GREEN: &str = "\x1b[32m"; -const RED: &str = "\x1b[31m"; -const RESET: &str = "\x1b[0m"; - -static SPINNER_STYLE: LazyLock = LazyLock::new(|| { - ProgressStyle::with_template("{spinner} [{prefix}] {msg} ({elapsed})") - .expect("valid template") - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", ""]) -}); - -pub(super) struct BranchProgressTracker { - multi: Option, -} - -impl BranchProgressTracker { - pub fn new() -> Self { - if *IS_STDOUT_TERMINAL { - Self { - multi: Some(MultiProgress::new()), - } - } else { - Self { multi: None } - } - } - - pub fn add_branch(&self, label: &str) -> BranchProgressHandle { - let Some(multi) = &self.multi else { - return BranchProgressHandle::disabled(); - }; - let bar = multi.add(ProgressBar::new_spinner()); - bar.set_style(SPINNER_STYLE.clone()); - bar.set_prefix(label.to_string()); - bar.set_message("running…"); - bar.enable_steady_tick(Duration::from_millis(80)); - BranchProgressHandle { bar: Some(bar) } - } -} - -pub(super) struct BranchProgressHandle { - bar: Option, -} - -impl BranchProgressHandle { - pub fn disabled() -> Self { - Self { bar: None } - } - - pub fn complete(self) { - if let Some(bar) = self.bar { - bar.finish_with_message(format!("{GREEN}✓ done{RESET}")); - } - } - - pub fn fail(self, err: &str) { - if let Some(bar) = self.bar { - let truncated = if err.len() > 80 { - let mut s = err[..80].to_string(); - s.push('…'); - s - } else { - err.to_string() - }; - bar.finish_with_message(format!("{RED}✗ failed {RESET} — {truncated}")); - } - } -}