openquiver-runtime 0.1.0

Event-sourced executor: exec tokens, journal, run resumption.
Documentation
//! Turning source text into something runnable, for tests.
//!
//! Compiled separately into every test binary that includes it, so anything only
//! one of them uses looks dead to the others.
#![allow(dead_code)]

use openquiver_compile::CompiledPlan;
use openquiver_nodes::Registry;
use openquiver_store::{Event, RunId, Store};

/// Parses and compiles a workflow, refusing anything the earlier layers reject.
///
/// Runtime tests are about execution, so a graph that does not compile is a
/// broken fixture rather than an interesting case.
pub fn plan(source: &str) -> CompiledPlan {
    plan_with(source, Registry::builtin())
}

/// Compiles against a registry that knows something the runtime does not.
///
/// The registry is fed by plugins and MCP servers as well as by built-in nodes,
/// so a graph that compiles against a type this build cannot execute is a real
/// state to be in, not a contrivance.
pub fn plan_with(source: &str, registry: Registry) -> CompiledPlan {
    let parsed = openquiver_syntax::parse(source);
    assert!(
        parsed.diagnostics.is_empty(),
        "fixture does not parse: {:#?}",
        parsed.diagnostics
    );
    let file = parsed.file.expect("a file with no diagnostics");
    openquiver_compile::compile(&file.document, &registry)
        .unwrap_or_else(|findings| panic!("fixture does not compile: {findings:#?}"))
}

/// Everything `io.print` wrote, in order.
pub async fn printed(store: &Store, run: &RunId) -> Vec<String> {
    events(store, run)
        .await
        .iter()
        .filter(|e| e.kind == "print")
        .map(|e| e.payload["text"].as_str().expect("print text").to_owned())
        .collect()
}

pub async fn events(store: &Store, run: &RunId) -> Vec<Event> {
    store.events(run).await.expect("read journal")
}

/// The kinds in a run's journal, for asserting on its shape.
pub async fn kinds(store: &Store, run: &RunId) -> Vec<String> {
    events(store, run)
        .await
        .into_iter()
        .map(|e| e.kind)
        .collect()
}

/// The smallest graph that exercises every M1 node except `flow.branch`.
pub const HELLO: &str = r#"
schema 1

graph "hello" {
  node start = event.manual
  node agent = acp.connect_agent { agent = "mock" }
  node sess = acp.new_session
  node ask = acp.prompt { text = "Say hello." }
  node out = io.print

  start.out -> agent.in
  agent.out -> sess.in
  sess.out -> ask.in
  ask.end_turn -> out.in

  agent.agent => sess.agent
  ask.text => out.value
  sess.session => ask.session
}
"#;