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;
use thal::runtime::Molecule;
use thal::value::Value;
use thal::Reactor;

const PROGRAM: &str = r#"
molecule Source {
    id: Int
    primary_key: id
}

molecule TaskA {
    src_id: Int
    note:   String?
    primary_key: src_id
}

molecule TaskB {
    src_id: Int
    primary_key: src_id
}

molecule TaskC {
    src_id: Int
    primary_key: src_id
}

reaction Fanout {
    when: Source(s)
    emit:
        TaskA { src_id: s.id, note: null },
        TaskB { src_id: s.id },
        TaskC { src_id: s.id }
}
"#;

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

    handle
        .emit(
            Molecule::builder("Source")
                .field("id", Value::Int(1))
                .build(),
        )
        .await
        .expect("emit Source");

    tokio::time::sleep(Duration::from_millis(100)).await;
    task.abort();

    let task_a = store.scan_by_name("TaskA");
    let task_b = store.scan_by_name("TaskB");
    let task_c = store.scan_by_name("TaskC");

    assert_eq!(task_a.len(), 1, "expected one TaskA, got {}", task_a.len());
    assert_eq!(task_b.len(), 1, "expected one TaskB, got {}", task_b.len());
    assert_eq!(task_c.len(), 1, "expected one TaskC, got {}", task_c.len());

    assert_eq!(task_a[0].fields["src_id"], Value::Int(1));
    assert_eq!(
        task_a[0].fields["note"],
        Value::Null,
        "null literal should round-trip through the store as Value::Null"
    );

    assert_eq!(task_b[0].fields["src_id"], Value::Int(1));
    assert_eq!(task_c[0].fields["src_id"], Value::Int(1));
}