Skip to main content

atman_runtime/tools/
flow_list.rs

1use 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}