atman-runtime 1.9.1

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;
use std::path::Path;

pub struct FlowList;

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

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

    fn description(&self) -> Option<&str> {
        Some(
            "List all .at flow files in ~/.config/atman/commands/ and every flow \
             inside each file. Returns a list of file entries; each entry contains \
             the file name, a file-level description (from the describe() flow if \
             present), and a list of all flows with their names, ref strings, and \
             parameters.\n\n\
             Each flow's `ref` field gives the exact string to pass to flow.spawn's \
             `flow` parameter (e.g. \"subagent.at@research_loop\"). Use this to \
             discover flows and their parameter signatures before calling flow.spawn.",
        )
    }

    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {}
        })
    }

    fn call<'a>(&'a self, _args: ToolArgs, _ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let config_dir = storage::config_dir()
                .map_err(|e| RuntimeError::ToolFailed(format!("flow.list: config_dir: {e}")))?;
            let commands_dir = config_dir.join("commands");
            let mut entries: Vec<Value> = Vec::new();

            if commands_dir.is_dir() {
                let read = std::fs::read_dir(&commands_dir)
                    .map_err(|e| RuntimeError::ToolFailed(format!("flow.list: read dir: {e}")))?;
                for entry in read.flatten() {
                    let path = entry.path();
                    if path.extension().and_then(|s| s.to_str()) != Some("at") {
                        continue;
                    }
                    if let Ok(flow_info) = scan_flow_file(&path) {
                        entries.push(flow_info);
                    }
                }
            }

            entries.sort_by(|a, b| {
                let file_a = match a {
                    Value::Struct(fields) => fields
                        .iter()
                        .find(|(k, _)| k == "file")
                        .and_then(|(_, v)| {
                            if let Value::Str(s) = v {
                                Some(s.clone())
                            } else {
                                None
                            }
                        })
                        .unwrap_or_default(),
                    _ => String::new(),
                };
                let file_b = match b {
                    Value::Struct(fields) => fields
                        .iter()
                        .find(|(k, _)| k == "file")
                        .and_then(|(_, v)| {
                            if let Value::Str(s) = v {
                                Some(s.clone())
                            } else {
                                None
                            }
                        })
                        .unwrap_or_default(),
                    _ => String::new(),
                };
                file_a.cmp(&file_b)
            });

            Ok(Value::List(entries))
        })
    }
}

fn scan_flow_file(path: &Path) -> Result<Value, RuntimeError> {
    let src = std::fs::read_to_string(path).map_err(|e| {
        RuntimeError::ToolFailed(format!("flow.list: read {}: {e}", path.display()))
    })?;
    let file = atman_dsl::parse::parse_file(&src).map_err(|e| {
        RuntimeError::ToolFailed(format!("flow.list: parse {}: {e}", path.display()))
    })?;

    let file_name = path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_string();

    let describe_flow = file.flows.iter().find(|f| f.name.name == "describe");
    let description = describe_flow
        .and_then(extract_return_string_literal)
        .unwrap_or_default();

    let flows: Vec<Value> = file
        .flows
        .iter()
        .map(|f| {
            let params: Vec<Value> = f
                .params
                .iter()
                .map(|p| {
                    Value::Struct(vec![
                        ("name".into(), Value::Str(p.name.name.clone())),
                        ("ty".into(), Value::Str(format!("{:?}", p.ty))),
                        ("default".into(), expr_to_value(&p.default)),
                    ])
                })
                .collect();
            Value::Struct(vec![
                ("name".into(), Value::Str(f.name.name.clone())),
                (
                    "ref".into(),
                    Value::Str(format!("{file_name}@{}", f.name.name)),
                ),
                ("params".into(), Value::List(params)),
            ])
        })
        .collect();

    Ok(Value::Struct(vec![
        ("file".into(), Value::Str(file_name)),
        ("description".into(), Value::Str(description)),
        ("flows".into(), Value::List(flows)),
    ]))
}

fn extract_return_string_literal(flow: &atman_dsl::ast::FlowDecl) -> Option<String> {
    use atman_dsl::ast::{Expr, Literal, Stmt};
    for stmt in &flow.body {
        if let Stmt::Return { value } = stmt
            && let Expr::Literal(Literal::Str(s)) = value
        {
            return Some(s.clone());
        }
    }
    None
}

fn expr_to_value(expr: &Option<atman_dsl::ast::Expr>) -> Value {
    use atman_dsl::ast::{Expr, Literal};
    match expr {
        Some(Expr::Literal(Literal::Int(n))) => Value::Int(*n),
        Some(Expr::Literal(Literal::Float(n))) => Value::Float(*n),
        Some(Expr::Literal(Literal::Bool(b))) => Value::Bool(*b),
        Some(Expr::Literal(Literal::Str(s))) => Value::Str(s.clone()),
        _ => Value::Unit,
    }
}