atman-runtime 1.2.0

atman flow execution runtime: evaluator, tool dispatch, provider dispatch, executor, memory stores
Documentation
use atman_runtime::tool::{ToolArgs, ToolCtx};
use atman_runtime::tools::register_tier_zero;
use atman_runtime::value::Value;
use std::sync::{Arc, Mutex};
use tempfile::TempDir;

#[tokio::test]
async fn fs_edit_end_to_end_read_then_edit_flow() {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("script.py");
    tokio::fs::write(
        &path,
        b"def greet():\n    print(\"hi\")\n\ndef main():\n    greet()\n",
    )
    .await
    .unwrap();

    let mut registry = atman_runtime::tool::ToolRegistry::new();
    register_tier_zero(&mut registry);

    let tracker = Arc::new(Mutex::new(std::collections::HashSet::new()));
    let ctx = ToolCtx::new().with_read_files(tracker.clone());

    let read_tool = registry.get("fs.read").expect("fs.read registered");
    let edit_tool = registry.get("fs.edit").expect("fs.edit registered");

    let edit_before_read = edit_tool
        .call(
            ToolArgs {
                positional: vec![],
                named: vec![
                    ("path".into(), Value::Path(path.clone())),
                    ("old_string".into(), Value::Str("print(\"hi\")".into())),
                    ("new_string".into(), Value::Str("print(\"hello\")".into())),
                ],
            },
            &ctx,
        )
        .await;
    assert!(
        matches!(&edit_before_read, Err(e) if format!("{e}").contains("has not been read")),
        "expected read-before-edit gate, got {edit_before_read:?}"
    );

    read_tool
        .call(
            ToolArgs {
                positional: vec![Value::Path(path.clone())],
                named: vec![],
            },
            &ctx,
        )
        .await
        .expect("fs.read succeeds");

    let edit_result = edit_tool
        .call(
            ToolArgs {
                positional: vec![],
                named: vec![
                    ("path".into(), Value::Path(path.clone())),
                    ("old_string".into(), Value::Str("print(\"hi\")".into())),
                    (
                        "new_string".into(),
                        Value::Str("print(\"hello, world\")".into()),
                    ),
                ],
            },
            &ctx,
        )
        .await
        .expect("edit after read succeeds");
    assert!(matches!(edit_result, Value::Struct(_)));

    let after = tokio::fs::read_to_string(&path).await.unwrap();
    assert!(after.contains("print(\"hello, world\")"));
    assert!(!after.contains("print(\"hi\")"));
    assert!(after.contains("def greet"));
    assert!(after.contains("def main"));
}

#[tokio::test]
async fn fs_edit_ambiguous_match_returns_actionable_error_through_registry() {
    let dir = TempDir::new().unwrap();
    let path = dir.path().join("todos.txt");
    tokio::fs::write(&path, b"TODO first\nnormal\nTODO second\n")
        .await
        .unwrap();

    let mut registry = atman_runtime::tool::ToolRegistry::new();
    register_tier_zero(&mut registry);

    let tracker = Arc::new(Mutex::new(std::collections::HashSet::new()));
    let ctx = ToolCtx::new().with_read_files(tracker);
    registry
        .get("fs.read")
        .unwrap()
        .call(
            ToolArgs {
                positional: vec![Value::Path(path.clone())],
                named: vec![],
            },
            &ctx,
        )
        .await
        .unwrap();

    let err = registry
        .get("fs.edit")
        .unwrap()
        .call(
            ToolArgs {
                positional: vec![],
                named: vec![
                    ("path".into(), Value::Path(path)),
                    ("old_string".into(), Value::Str("TODO".into())),
                    ("new_string".into(), Value::Str("DONE".into())),
                ],
            },
            &ctx,
        )
        .await
        .unwrap_err();
    let msg = format!("{err}");
    assert!(msg.contains("matches 2 times"), "msg: {msg}");
    assert!(msg.contains("replace_all=true"));
}