1use crate::dag::WorkflowDag;
8use crate::state_graph::{EdgeCondition, NodeAction, StateGraph};
9
10pub fn render_dag_ascii(dag: &WorkflowDag) -> String {
24 let mut lines = Vec::new();
25
26 for level_idx in 0..dag.levels.len() {
27 let level_ids = &dag.levels[level_idx];
28 lines.push(format!("Level {}:", level_idx));
29 for step_id in level_ids {
30 let node = dag.get_node(step_id);
31 if let Some(node) = node {
32 let tool_info = if let Some(tool) = &node.step.tool {
33 format!("→ {}", tool)
34 } else if node.step.prompt.is_some() {
35 "→ LLM".to_string()
36 } else {
37 "→ (empty)".to_string()
38 };
39 let deps = if node.depends_on.is_empty() {
40 String::new()
41 } else {
42 format!(" (depends: {})", node.depends_on.join(", "))
43 };
44 lines.push(format!(" ├── {}{}{}", step_id, tool_info, deps));
45 }
46 }
47 }
48
49 if lines.is_empty() {
50 lines.push("(empty DAG)".to_string());
51 }
52
53 lines.join("\n")
54}
55
56pub fn render_state_graph_ascii(graph: &StateGraph) -> String {
73 let mut lines = Vec::new();
74 lines.push(format!("Entry: {} →", graph.entry_point));
75
76 let mut node_ids: Vec<String> = graph.nodes.keys().cloned().collect();
78 node_ids.sort();
79
80 for node_id in &node_ids {
81 let node = graph.nodes.get(node_id).unwrap();
82 let action_str = match &node.action {
83 NodeAction::LlmInfer {
84 system_prompt_override,
85 include_tool_definitions,
86 ..
87 } => {
88 let tools_str = if *include_tool_definitions {
89 " +tools"
90 } else {
91 ""
92 };
93 if system_prompt_override.is_some() {
94 format!("🧠 LLM (custom prompt){}", tools_str)
95 } else {
96 format!("🧠 LLM{}", tools_str)
97 }
98 }
99 NodeAction::ToolCall { tool_name, .. } => format!("🔧 {}", tool_name),
100 NodeAction::Delegate { agent_kind, .. } => format!("🤖 →{}", agent_kind),
101 NodeAction::HumanApproval { description } => format!("✋ {}", description),
102 NodeAction::ConditionCheck { condition } => format!("🔀 {}", condition),
103 NodeAction::SwitchParadigm { paradigm } => format!("🔄 →{}", paradigm),
104 };
105 let interrupt_str = if node.interrupt { " ⏸" } else { "" };
106 lines.push(format!(" {} [{}]{}", node_id, action_str, interrupt_str));
107
108 if let Some(edges) = graph.edges.get(node_id) {
110 for edge in edges {
111 let cond_str: String = match &edge.condition {
112 Some(EdgeCondition::HasToolCalls) => " [HasToolCalls]".to_string(),
113 Some(EdgeCondition::IsFinalAnswer) => " [IsFinalAnswer]".to_string(),
114 Some(EdgeCondition::RequestsDelegation) => " [RequestsDelegation]".to_string(),
115 Some(EdgeCondition::ErrorOccurred) => " [ErrorOccurred]".to_string(),
116 Some(EdgeCondition::StateEquals { variable, value }) => {
117 format!(" [{}={}]", variable, value)
118 }
119 Some(EdgeCondition::Always) => String::new(),
120 Some(EdgeCondition::Custom { name, .. }) => format!(" [Custom:{}]", name),
121 Some(EdgeCondition::ParadigmEquals { paradigm }) => {
122 format!(" [Paradigm={}]", paradigm)
123 }
124 Some(EdgeCondition::IterationExceeds { count }) => format!(" [Iter>{}]", count),
125 None => String::new(),
126 };
127 lines.push(format!(" → {}{}", edge.to, cond_str));
128 }
129 }
130 }
131
132 lines.push(format!("Terminal: {}", graph.terminal_nodes.join(", ")));
133
134 if graph.has_cycles() {
136 lines.push("⚠ Contains cycles (expected for ReAct loops)".to_string());
137 }
138
139 lines.join("\n")
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::config::StepConfig;
146 use crate::dag::DagNode;
147 use std::collections::HashMap;
148
149 fn make_step(id: &str, depends_on: Vec<&str>, tool: Option<&str>) -> StepConfig {
150 StepConfig {
151 id: id.to_string(),
152 description: format!("Step {}", id),
153 depends_on: depends_on.iter().map(|s| s.to_string()).collect(),
154 tool: tool.map(|t| t.to_string()),
155 tool_args: None,
156 prompt: None,
157 requires_approval: false,
158 timeout_secs: None,
159 retry_policy: None,
160 metadata: HashMap::new(),
161 }
162 }
163
164 #[test]
165 fn test_render_dag_ascii() {
166 let mut dag = WorkflowDag::new("test");
167 dag.add_node(DagNode {
168 id: "read".to_string(),
169 step: make_step("read", vec![], Some("shell")),
170 depends_on: vec![],
171 children: Vec::new(),
172 level: 0,
173 });
174 dag.add_node(DagNode {
175 id: "analyze".to_string(),
176 step: StepConfig {
177 id: "analyze".to_string(),
178 description: "Analyze".to_string(),
179 depends_on: vec!["read".to_string()],
180 tool: None,
181 tool_args: None,
182 prompt: Some("Analyze code".to_string()),
183 requires_approval: false,
184 timeout_secs: None,
185 retry_policy: None,
186 metadata: HashMap::new(),
187 },
188 depends_on: vec!["read".to_string()],
189 children: Vec::new(),
190 level: 0,
191 });
192 dag.build();
193
194 let viz = render_dag_ascii(&dag);
195 assert!(viz.contains("Level 0:"));
196 assert!(viz.contains("→ shell"));
197 assert!(viz.contains("→ LLM"));
198 }
199
200 #[test]
201 fn test_render_state_graph_ascii() {
202 let mut graph = StateGraph::new("react-loop", "think");
203
204 graph.add_node(crate::state_graph::GraphNode {
205 id: "think".to_string(),
206 action: crate::state_graph::NodeAction::LlmInfer {
207 system_prompt_override: None,
208 use_streaming: true,
209 include_tool_definitions: true,
210 tool_filter_override: None,
211 thinking_budget: None,
212 temperature: None,
213 max_tokens: None,
214 },
215 interrupt: false,
216 metadata: HashMap::new(),
217 });
218
219 graph.add_node(crate::state_graph::GraphNode {
220 id: "end".to_string(),
221 action: crate::state_graph::NodeAction::LlmInfer {
222 system_prompt_override: Some("Final answer".to_string()),
223 use_streaming: false,
224 include_tool_definitions: false,
225 tool_filter_override: None,
226 thinking_budget: None,
227 temperature: None,
228 max_tokens: None,
229 },
230 interrupt: false,
231 metadata: HashMap::new(),
232 });
233
234 graph.add_edge(crate::state_graph::GraphEdge {
235 from: "think".to_string(),
236 to: "end".to_string(),
237 condition: Some(crate::state_graph::EdgeCondition::IsFinalAnswer),
238 metadata: HashMap::new(),
239 });
240
241 graph.add_terminal("end".to_string());
242
243 let viz = render_state_graph_ascii(&graph);
244 assert!(viz.contains("Entry: think"));
245 assert!(viz.contains("🧠 LLM"));
246 assert!(viz.contains("Terminal: end"));
247 assert!(viz.contains("[IsFinalAnswer]"));
248 }
249}