thal 0.0.1

Reactive semantic runtime — molecules, reactions, and effect actors for building LLM-backed applications as dataflow programs.
Documentation
use std::time::{Duration, Instant};
use thal::value::Value;
use thal::Reactor;

const PROGRAM: &str = r#"
reaction Echo {
    when:  Tick(t)
    where: t.sequence == 1
    emit:  Process { cmd: "echo", args: ["hello, thal"] }
}

reaction Init {
    when: Boot(b)
    emit: Timer { interval: 50ms }
}
"#;

#[tokio::test(flavor = "multi_thread")]
async fn process_actor_runs_echo() {
    let program = thal::load_str(PROGRAM).expect("load");
    let reactor = Reactor::new(program);
    let store = reactor.store();
    let task = tokio::spawn(reactor.run());

    // Poll until a Process molecule transitions to Done, with a deadline.
    let deadline = Instant::now() + Duration::from_millis(500);
    loop {
        if Instant::now() > deadline {
            break;
        }
        let processes = store.scan_by_name("Process");
        if processes
            .iter()
            .any(|p| matches!(p.fields.get("status"), Some(Value::String(s)) if s == "Done"))
        {
            break;
        }
        tokio::time::sleep(Duration::from_millis(10)).await;
    }
    task.abort();

    let processes = store.scan_by_name("Process");
    assert_eq!(
        processes.len(),
        1,
        "expected exactly one Process molecule, got {}",
        processes.len()
    );

    let p = &processes[0];
    assert_eq!(p.fields["cmd"], Value::String("echo".into()));
    assert_eq!(
        p.fields["status"],
        Value::String("Done".into()),
        "actor should have driven Pending -> Done"
    );
    assert_eq!(p.fields["exit_code"], Value::Int(0));

    let stdout = p.fields["stdout"]
        .as_string()
        .expect("stdout is a String");
    assert!(
        stdout.contains("hello, thal"),
        "expected echo output to contain the argument, got {stdout:?}"
    );

    // List literal round-trip: args should come back as a list of one string.
    match &p.fields["args"] {
        Value::List(items) => {
            assert_eq!(items.len(), 1);
            assert_eq!(items[0], Value::String("hello, thal".into()));
        }
        other => panic!("expected args to be a List, got {}", other.type_name()),
    }
}