atman-runtime 1.7.0

atman flow execution runtime: evaluator, tool dispatch, provider dispatch, executor, memory stores
Documentation
use crate::error::RuntimeError;
use crate::storage;
use crate::tool::{BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
use crate::value::Value;

pub struct FlowCheck;

impl Tool for FlowCheck {
    fn name(&self) -> &str {
        "flow.check"
    }

    fn tier(&self) -> Tier {
        Tier::Zero
    }

    fn description(&self) -> Option<&str> {
        Some(
            "Validate and lint a .at flow file. Checks all flows in the file \
             for undefined variables, undefined tools, and type mismatches \
             (errors), plus unused params and too many positional args \
             (warnings). Pass `flow` as a filename (e.g. 'subagent.at') or \
             path. Use after writing or editing a flow to catch mistakes \
             before spawning.",
        )
    }

    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "flow": {
                    "type": "string",
                    "description": "Flow file name or path (e.g. 'subagent.at' or '/path/to/my.at')"
                }
            },
            "required": ["flow"]
        })
    }

    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let flow_ref = match args.named("flow").or_else(|| args.positional.first()) {
                Some(Value::Str(s)) if !s.trim().is_empty() => s.clone(),
                Some(other) => {
                    return Err(RuntimeError::TypeMismatch {
                        expected: "non-empty flow string".into(),
                        actual: other.kind_name().into(),
                    });
                }
                None => {
                    return Err(RuntimeError::MissingArg("flow.check.flow".into()));
                }
            };

            let (path, src) = read_flow_source(&flow_ref).await?;
            let file = atman_dsl::parse::parse_file(&src).map_err(|e| {
                RuntimeError::ToolFailed(format!("flow.check: parse {}: {e}", path.display()))
            })?;

            let registry = ctx
                .registry
                .as_ref()
                .ok_or_else(|| RuntimeError::ToolFailed("flow.check: no tool registry".into()))?;

            let mut errors: Vec<Value> = Vec::new();
            for flow in &file.flows {
                if let Err(errs) = crate::validate::validate(flow, registry) {
                    for e in errs {
                        errors.push(Value::Struct(vec![
                            ("kind".into(), Value::Str("validate".into())),
                            ("flow".into(), Value::Str(flow.name.name.clone())),
                            ("message".into(), Value::Str(format!("{e:?}"))),
                        ]));
                    }
                }
            }

            let mut warnings: Vec<Value> = Vec::new();
            for hit in crate::flow_lint::lint_file(&file) {
                warnings.push(Value::Struct(vec![
                    ("kind".into(), Value::Str(hit.rule.slug().into())),
                    ("flow".into(), Value::Str(hit.flow.clone())),
                    ("message".into(), Value::Str(hit.message.clone())),
                ]));
            }

            let valid = errors.is_empty();

            Ok(Value::Struct(vec![
                ("valid".into(), Value::Bool(valid)),
                ("errors".into(), Value::List(errors)),
                ("warnings".into(), Value::List(warnings)),
            ]))
        })
    }
}

async fn read_flow_source(flow_ref: &str) -> Result<(std::path::PathBuf, String), RuntimeError> {
    for path in flow_candidates(flow_ref) {
        match tokio::fs::read_to_string(&path).await {
            Ok(src) => return Ok((path, src)),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => {
                return Err(RuntimeError::ToolFailed(format!(
                    "flow.check: read {}: {e}",
                    path.display()
                )));
            }
        }
    }
    Err(RuntimeError::ToolFailed(format!(
        "flow.check: flow `{flow_ref}` not found"
    )))
}

fn flow_candidates(flow_ref: &str) -> Vec<std::path::PathBuf> {
    let path = std::path::PathBuf::from(flow_ref);
    if path.is_absolute() {
        return vec![path];
    }
    let file_name = if flow_ref.ends_with(".at") {
        flow_ref.to_string()
    } else {
        format!("{flow_ref}.at")
    };
    let mut out = Vec::new();
    if let Ok(config_dir) = storage::config_dir() {
        out.push(config_dir.join("commands").join(&file_name));
    }
    out.push(std::path::PathBuf::from(file_name));
    out
}