atman_runtime/tools/
flow_list.rs1use crate::error::RuntimeError;
2use crate::storage;
3use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
4use crate::value::Value;
5use std::path::Path;
6
7pub struct FlowList;
8
9impl Tool for FlowList {
10 fn name(&self) -> &str {
11 "flow.list"
12 }
13
14 fn tier(&self) -> Tier {
15 Tier::Zero
16 }
17
18 fn description(&self) -> Option<&str> {
19 Some(
20 "List all .at flow files in ~/.config/atman/commands/ and every flow \
21 inside each file. Returns a list of file entries; each entry contains \
22 the file name, a file-level description (from the describe() flow if \
23 present), and a list of all flows with their names, ref strings, and \
24 parameters.\n\n\
25 Each flow's `ref` field gives the exact string to pass to flow.spawn's \
26 `flow` parameter (e.g. \"subagent.at@research_loop\"). Use this to \
27 discover flows and their parameter signatures before calling flow.spawn.",
28 )
29 }
30
31 fn input_schema(&self) -> serde_json::Value {
32 serde_json::json!({
33 "type": "object",
34 "properties": {}
35 })
36 }
37
38 fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
39 Box::pin(async move {
40 let config_dir = storage::config_dir()
41 .map_err(|e| RuntimeError::ToolFailed(format!("flow.list: config_dir: {e}")))?;
42 let commands_dir = config_dir.join("commands");
43 let mut entries: Vec<Value> = Vec::new();
44
45 if commands_dir.is_dir() {
46 let read = std::fs::read_dir(&commands_dir)
47 .map_err(|e| RuntimeError::ToolFailed(format!("flow.list: read dir: {e}")))?;
48 for entry in read.flatten() {
49 let path = entry.path();
50 if path.extension().and_then(|s| s.to_str()) != Some("at") {
51 continue;
52 }
53 if let Ok(flow_info) = scan_flow_file(&path) {
54 entries.push(flow_info);
55 }
56 }
57 }
58
59 entries.sort_by(|a, b| {
60 let file_a = match a {
61 Value::Struct(fields) => fields
62 .iter()
63 .find(|(k, _)| k == "file")
64 .and_then(|(_, v)| {
65 if let Value::Str(s) = v {
66 Some(s.clone())
67 } else {
68 None
69 }
70 })
71 .unwrap_or_default(),
72 _ => String::new(),
73 };
74 let file_b = match b {
75 Value::Struct(fields) => fields
76 .iter()
77 .find(|(k, _)| k == "file")
78 .and_then(|(_, v)| {
79 if let Value::Str(s) = v {
80 Some(s.clone())
81 } else {
82 None
83 }
84 })
85 .unwrap_or_default(),
86 _ => String::new(),
87 };
88 file_a.cmp(&file_b)
89 });
90
91 Ok(Value::List(entries))
92 })
93 }
94}
95
96fn scan_flow_file(path: &Path) -> Result<Value, RuntimeError> {
97 let src = std::fs::read_to_string(path).map_err(|e| {
98 RuntimeError::ToolFailed(format!("flow.list: read {}: {e}", path.display()))
99 })?;
100 let file = atman_dsl::parse::parse_file(&src).map_err(|e| {
101 RuntimeError::ToolFailed(format!("flow.list: parse {}: {e}", path.display()))
102 })?;
103
104 let file_name = path
105 .file_name()
106 .and_then(|s| s.to_str())
107 .unwrap_or_default()
108 .to_string();
109
110 let describe_flow = file.flows.iter().find(|f| f.name.name == "describe");
111 let description = describe_flow
112 .and_then(extract_return_string_literal)
113 .unwrap_or_default();
114
115 let flows: Vec<Value> = file
116 .flows
117 .iter()
118 .map(|f| {
119 let params: Vec<Value> = f
120 .params
121 .iter()
122 .map(|p| {
123 Value::Struct(vec![
124 ("name".into(), Value::Str(p.name.name.clone())),
125 ("ty".into(), Value::Str(format!("{:?}", p.ty))),
126 ("default".into(), expr_to_value(&p.default)),
127 ])
128 })
129 .collect();
130 Value::Struct(vec![
131 ("name".into(), Value::Str(f.name.name.clone())),
132 (
133 "ref".into(),
134 Value::Str(format!("{file_name}@{}", f.name.name)),
135 ),
136 ("params".into(), Value::List(params)),
137 ])
138 })
139 .collect();
140
141 Ok(Value::Struct(vec![
142 ("file".into(), Value::Str(file_name)),
143 ("description".into(), Value::Str(description)),
144 ("flows".into(), Value::List(flows)),
145 ]))
146}
147
148fn extract_return_string_literal(flow: &atman_dsl::ast::FlowDecl) -> Option<String> {
149 use atman_dsl::ast::{Expr, Literal, Stmt};
150 for stmt in &flow.body {
151 if let Stmt::Return { value } = stmt
152 && let Expr::Literal(Literal::Str(s)) = value
153 {
154 return Some(s.clone());
155 }
156 }
157 None
158}
159
160fn expr_to_value(expr: &Option<atman_dsl::ast::Expr>) -> Value {
161 use atman_dsl::ast::{Expr, Literal};
162 match expr {
163 Some(Expr::Literal(Literal::Int(n))) => Value::Int(*n),
164 Some(Expr::Literal(Literal::Float(n))) => Value::Float(*n),
165 Some(Expr::Literal(Literal::Bool(b))) => Value::Bool(*b),
166 Some(Expr::Literal(Literal::Str(s))) => Value::Str(s.clone()),
167 _ => Value::Unit,
168 }
169}