pe-graph 0.1.0

Graph execution engine for Potential Expectations — state graphs, Pregel model, ReAct topology, and builder DSL
Documentation
//! # pe-graph — Graph execution engine for Potential Expectations
//!
//! Implements the core graph primitives that agent topologies are built on:
//!
//! - [`StateGraph`] — declarative graph definition with typed nodes and edges
//! - [`CompiledGraph`] — validated, executable graph with `invoke()` / `resume()`
//! - [`GraphConfig`] — execution configuration (thread ID, recursion limit, etc.)
//! - [`Checkpointer`] — trait for durable state persistence
//! - [`GraphRegistry`] — named storage for compiled graphs
//!
//! The execution engine uses the **Pregel BSP model** (Bulk Synchronous Parallel):
//! nodes execute in parallel supersteps with snapshot isolation, writes are
//! collected and applied atomically between steps.
//!
//! Depends only on `pe-core` plus `tokio` and `futures` for async execution.

mod activation;
mod checkpoint_data;
pub mod checkpointer;
pub mod command;
pub mod compiled;
pub mod config;
pub mod graph;
pub mod matrix_hook;
pub mod pending_writes;
pub mod phase_store;
mod pregel;
pub mod registry;
pub mod retry;
pub mod snapshot;

// Primary re-exports
pub use checkpoint_data::CheckpointData;
pub use checkpointer::{CheckpointMeta, Checkpointer, InMemoryCheckpointer, PendingWrite};
pub use command::Command;
pub use compiled::{CompiledGraph, ExecutionOutcome};
pub use config::GraphConfig;
pub use graph::StateGraph;
pub use matrix_hook::{
    ConvergenceRecorder, DefaultMatrixHook, MatrixHook, MatrixHookHandle, RoutingResolver,
};
pub use pending_writes::PendingWrites;
pub use phase_store::{PhaseStateStore, PhaseStoreError};
pub use registry::GraphRegistry;
pub use retry::{RetryPolicy, with_retry};
pub use snapshot::StateSnapshot;

// Re-export START/END from pe-core for convenience
pub use pe_core::types::{END, START};

// ── Test support ──────────────────────────────────────────────────────
// Shared test types used across multiple test modules in this crate.

#[cfg(test)]
#[allow(dead_code)]
pub(crate) mod tests {
    use pe_core::node::{NodeContext, NodeFn, NodeFuture, NodeResult};
    use pe_core::state::{State, StateUpdate};
    use serde::{Deserialize, Serialize};

    // ── Test State ────────────────────────────────────────────────────

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    pub struct TestState {
        pub messages: Vec<String>,
        pub counter: u32,
        pub thread_id: String,
    }

    #[derive(Debug, Clone, Serialize, Deserialize, Default)]
    pub struct TestUpdate {
        pub messages: Option<Vec<String>>,
        pub counter: Option<u32>,
    }

    impl StateUpdate for TestUpdate {}

    impl State for TestState {
        type Update = TestUpdate;

        fn apply(&mut self, update: TestUpdate) {
            // messages: Appender semantics
            if let Some(msgs) = update.messages {
                self.messages.extend(msgs);
            }
            // counter: LastValue semantics
            if let Some(c) = update.counter {
                self.counter = c;
            }
        }
    }

    impl TestState {
        pub fn new() -> Self {
            Self {
                messages: Vec::new(),
                counter: 0,
                thread_id: "test-thread".into(),
            }
        }
    }

    impl TestUpdate {
        pub fn with_message(msg: impl Into<String>) -> Self {
            Self {
                messages: Some(vec![msg.into()]),
                counter: None,
            }
        }

        pub fn with_counter(n: u32) -> Self {
            Self {
                messages: None,
                counter: Some(n),
            }
        }
    }

    // ── Test Nodes ────────────────────────────────────────────────────

    /// Node that appends a message to state.
    pub struct AppendNode {
        node_name: &'static str,
        message: &'static str,
    }

    impl AppendNode {
        pub fn new(name: &'static str, message: &'static str) -> Self {
            Self {
                node_name: name,
                message,
            }
        }
    }

    impl NodeFn<TestState> for AppendNode {
        fn call(&self, _state: &TestState, _ctx: &NodeContext) -> NodeFuture<TestUpdate> {
            let msg = self.message.to_string();
            Box::pin(async move { NodeResult::Update(TestUpdate::with_message(msg)) })
        }

        fn name(&self) -> &str {
            self.node_name
        }
    }
}