Skip to main content

atman_runtime/tools/
flow_check.rs

1use crate::error::RuntimeError;
2use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
3use crate::value::Value;
4
5pub struct FlowCheck;
6
7impl Tool for FlowCheck {
8    fn name(&self) -> &str {
9        "flow.check"
10    }
11
12    fn tier(&self) -> Tier {
13        Tier::Zero
14    }
15
16    fn description(&self) -> Option<&str> {
17        Some(
18            "Validate and lint a .at flow file. Checks all flows in the file \
19             for undefined variables, undefined tools, and type mismatches \
20             (errors), plus unused params and too many positional args \
21             (warnings). Pass `flow` as a filename (e.g. 'subagent.at') or \
22             path. Use after writing or editing a flow to catch mistakes \
23             before spawning.",
24        )
25    }
26
27    fn input_schema(&self) -> serde_json::Value {
28        serde_json::json!({
29            "type": "object",
30            "properties": {
31                "flow": {
32                    "type": "string",
33                    "description": "Flow file name or path (e.g. 'subagent.at' or '/path/to/my.at')"
34                }
35            },
36            "required": ["flow"]
37        })
38    }
39
40    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
41        Box::pin(async move {
42            let flow_ref = match args.named("flow").or_else(|| args.positional.first()) {
43                Some(Value::Str(s)) if !s.trim().is_empty() => s.clone(),
44                Some(other) => {
45                    return Err(RuntimeError::TypeMismatch {
46                        expected: "non-empty flow string".into(),
47                        actual: other.kind_name().into(),
48                    });
49                }
50                None => {
51                    return Err(RuntimeError::MissingArg("flow.check.flow".into()));
52                }
53            };
54
55            let (path, src) = read_flow_source(&flow_ref).await?;
56            let file = atman_dsl::parse::parse_file(&src).map_err(|e| {
57                RuntimeError::ToolFailed(format!("flow.check: parse {}: {e}", path.display()))
58            })?;
59
60            let registry = ctx
61                .registry
62                .as_ref()
63                .ok_or_else(|| RuntimeError::ToolFailed("flow.check: no tool registry".into()))?;
64
65            let mut errors: Vec<Value> = Vec::new();
66            for flow in &file.flows {
67                if let Err(errs) = crate::validate::validate(flow, registry) {
68                    for e in errs {
69                        errors.push(Value::Struct(vec![
70                            ("kind".into(), Value::Str("validate".into())),
71                            ("flow".into(), Value::Str(flow.name.name.clone())),
72                            ("message".into(), Value::Str(format!("{e:?}"))),
73                        ]));
74                    }
75                }
76            }
77
78            let mut warnings: Vec<Value> = Vec::new();
79            for hit in crate::flow_lint::lint_file(&file) {
80                warnings.push(Value::Struct(vec![
81                    ("kind".into(), Value::Str(hit.rule.slug().into())),
82                    ("flow".into(), Value::Str(hit.flow.clone())),
83                    ("message".into(), Value::Str(hit.message.clone())),
84                ]));
85            }
86
87            let valid = errors.is_empty();
88
89            Ok(Value::Struct(vec![
90                ("valid".into(), Value::Bool(valid)),
91                ("errors".into(), Value::List(errors)),
92                ("warnings".into(), Value::List(warnings)),
93            ]))
94        })
95    }
96}
97
98async fn read_flow_source(flow_ref: &str) -> Result<(std::path::PathBuf, String), RuntimeError> {
99    for path in super::flow_source::candidates(flow_ref) {
100        match tokio::fs::read_to_string(&path).await {
101            Ok(src) => return Ok((path, src)),
102            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
103            Err(e) => {
104                return Err(RuntimeError::ToolFailed(format!(
105                    "flow.check: read {}: {e}",
106                    path.display()
107                )));
108            }
109        }
110    }
111    Err(RuntimeError::ToolFailed(format!(
112        "flow.check: flow `{flow_ref}` not found"
113    )))
114}