atman-runtime 1.4.0

atman flow execution runtime: evaluator, tool dispatch, provider dispatch, executor, memory stores
Documentation
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

const HASH_PREFIX_LEN: usize = 12;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FlowMeta {
    pub version: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
    #[serde(default)]
    pub author: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default, skip)]
    pub source: FlowMetaSource,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlowMetaSource {
    Sidecar,
    #[default]
    HashFallback,
}

impl FlowMeta {
    pub fn load(at_path: &Path) -> Result<Self> {
        let src = std::fs::read_to_string(at_path)
            .with_context(|| format!("read flow source {}", at_path.display()))?;
        Self::from_source(at_path, &src)
    }

    pub fn from_source(at_path: &Path, at_source: &str) -> Result<Self> {
        let sidecar = sidecar_path(at_path);
        if sidecar.exists() {
            return load_sidecar(&sidecar).map(|mut m| {
                m.source = FlowMetaSource::Sidecar;
                m
            });
        }
        Ok(hash_meta(at_source))
    }

    pub fn is_sidecar(&self) -> bool {
        matches!(self.source, FlowMetaSource::Sidecar)
    }

    pub fn short_hash(at_source: &str) -> String {
        let hash = blake3::hash(at_source.as_bytes()).to_hex().to_string();
        hash.chars().take(HASH_PREFIX_LEN).collect()
    }
}

pub fn sidecar_path(at_path: &Path) -> PathBuf {
    let name = at_path
        .file_name()
        .map(|s| s.to_string_lossy().to_string())
        .unwrap_or_default();
    let with_meta = format!("{name}.meta.toml");
    at_path.with_file_name(with_meta)
}

fn load_sidecar(path: &Path) -> Result<FlowMeta> {
    let text = std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
    let mut meta: FlowMeta =
        toml::from_str(&text).with_context(|| format!("parse {}", path.display()))?;
    if meta.version.trim().is_empty() {
        anyhow::bail!("{}: `version` must be non-empty", path.display());
    }
    meta.source = FlowMetaSource::Sidecar;
    Ok(meta)
}

fn hash_meta(at_source: &str) -> FlowMeta {
    FlowMeta {
        version: format!("hash:{}", FlowMeta::short_hash(at_source)),
        description: None,
        last_modified: None,
        author: None,
        tags: Vec::new(),
        source: FlowMetaSource::HashFallback,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hash_fallback_when_no_sidecar() {
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("greet.at");
        std::fs::write(&at, "flow greet() { return 1 }").unwrap();
        let meta = FlowMeta::load(&at).unwrap();
        assert!(!meta.is_sidecar());
        assert!(meta.version.starts_with("hash:"));
        assert_eq!(meta.version.len(), "hash:".len() + HASH_PREFIX_LEN);
    }

    #[test]
    fn hash_is_stable_and_content_addressed() {
        let a = FlowMeta::short_hash("flow x() { return 1 }");
        let b = FlowMeta::short_hash("flow x() { return 1 }");
        let c = FlowMeta::short_hash("flow x() { return 2 }");
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn sidecar_wins_over_hash() {
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("greet.at");
        std::fs::write(&at, "flow greet() { return 1 }").unwrap();
        std::fs::write(
            dir.path().join("greet.at.meta.toml"),
            r#"version = "0.3.1"
description = "greet the user"
author = "w-mai"
tags = ["hello", "demo"]
"#,
        )
        .unwrap();
        let meta = FlowMeta::load(&at).unwrap();
        assert!(meta.is_sidecar());
        assert_eq!(meta.version, "0.3.1");
        assert_eq!(meta.description.as_deref(), Some("greet the user"));
        assert_eq!(meta.author.as_deref(), Some("w-mai"));
        assert_eq!(meta.tags, vec!["hello", "demo"]);
    }

    #[test]
    fn sidecar_missing_version_rejected() {
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("foo.at");
        std::fs::write(&at, "flow foo() { return 1 }").unwrap();
        std::fs::write(
            dir.path().join("foo.at.meta.toml"),
            r#"version = ""
description = "bad"
"#,
        )
        .unwrap();
        let err = FlowMeta::load(&at).unwrap_err();
        assert!(err.to_string().contains("version"));
    }

    #[test]
    fn sidecar_path_beside_at_file() {
        let p = sidecar_path(Path::new("/tmp/agents/review.at"));
        assert_eq!(p, PathBuf::from("/tmp/agents/review.at.meta.toml"));
    }

    #[test]
    fn from_source_reads_sidecar_without_reading_at_file() {
        let dir = tempfile::tempdir().unwrap();
        let at = dir.path().join("v.at");
        std::fs::write(dir.path().join("v.at.meta.toml"), "version = \"1.2.3\"\n").unwrap();
        let meta = FlowMeta::from_source(&at, "flow v() { return 1 }").unwrap();
        assert_eq!(meta.version, "1.2.3");
        assert!(meta.is_sidecar());
    }
}