thal 0.0.1

Reactive semantic runtime — molecules, reactions, and effect actors for building LLM-backed applications as dataflow programs.
Documentation
use std::sync::Arc;
use std::time::Duration;
use thal::actors::terminal_prompt::ScriptedPromptIo;
use thal::actors::ActorRegistry;
use thal::value::Value;
use thal::Reactor;

// Inline program — same shape as `examples/chat.thal` but pinned to the
// mock provider so the test exercises the loop without any LLM provider
// configuration. The `examples/chat.thal` file uses real Codex and is for
// manual demos.
const PROGRAM: &str = r#"
reaction OnUserInput {
    when:  TerminalPrompt(p)
    where: p.status == "Done"
    emit:  LlmCall { provider: "mock", prompt: p.answer }
}

reaction OnLlmResponse {
    when:  LlmCall(c)
    where: c.status == "Done"
    emit:  TerminalWrite { content: c.text }
}

reaction OnPrintDone {
    when:  TerminalWrite(w)
    where: w.status == "Done"
    emit:  TerminalPrompt { question: "you: " }
}

reaction Init {
    when: Boot(b)
    emit: TerminalPrompt { question: "you: " }
}
"#;

#[tokio::test(flavor = "multi_thread")]
async fn chat_demo_completes_a_round_trip() {
    let program = thal::load_str(PROGRAM).expect("load");

    let scripted = Arc::new(ScriptedPromptIo::new(vec![
        "hello, thal".into(),
        "second turn".into(),
    ]));
    let registry = ActorRegistry::with_prompt_io(scripted);

    let reactor = Reactor::with_actors(program, registry);
    let store = reactor.store();
    let task = tokio::spawn(reactor.run());

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

    let llm_calls = store.scan_by_name("LlmCall");
    let any_done_echo = llm_calls.iter().any(|c| {
        let status_done = matches!(c.fields.get("status"), Some(Value::String(s)) if s == "Done");
        let text_echo =
            matches!(c.fields.get("text"), Some(Value::String(s)) if s.starts_with("echo: "));
        status_done && text_echo
    });
    assert!(
        any_done_echo,
        "expected at least one LlmCall to reach Done with `echo: ` text; got {llm_calls:#?}"
    );

    let writes = store.scan_by_name("TerminalWrite");
    let any_echo_write = writes.iter().any(|w| {
        matches!(w.fields.get("content"), Some(Value::String(s)) if s.starts_with("echo: "))
    });
    assert!(
        any_echo_write,
        "expected at least one TerminalWrite to carry the LLM response"
    );
}