pe-graph 0.1.0

Graph execution engine for Potential Expectations — state graphs, Pregel model, ReAct topology, and builder DSL
Documentation
//! Graph commands -- how users send input back to a paused graph.
//!
//! After a graph returns [`ExecutionOutcome::Interrupted`](crate::compiled::ExecutionOutcome::Interrupted), the caller
//! constructs a [`Command`] and passes it to [`CompiledGraph::resume_with()`](crate::compiled::CompiledGraph::resume_with)
//! to continue execution.

use pe_core::node::HumanInput;
use serde::{Deserialize, Serialize};

/// A command sent to a graph to control execution after an interrupt.
///
/// # Variants
///
/// - `Resume` -- provide human input and continue from the interrupted node
/// - `Goto` -- jump to a specific node (skip the normal edge traversal)
/// - `Update` -- apply a raw JSON update to state before resuming
///
/// # Example
///
/// ```ignore
/// let cmd = Command::resume(HumanInput {
///     approved: true,
///     feedback: Some("Looks good".into()),
///     data: None,
/// });
/// let outcome = graph.resume_with("thread-1", cmd, config).await?;
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Command {
    /// Resume from the interrupted node with human input.
    Resume {
        /// The human's response to the interrupt prompt.
        human_input: HumanInput,
    },

    /// Jump to a specific node, bypassing normal edge traversal.
    Goto {
        /// Target node name to activate.
        node: String,
    },

    /// Apply a raw JSON update to state before resuming.
    Update {
        /// Arbitrary state patch as JSON (deserialized by the caller).
        update: serde_json::Value,
    },
}

impl Command {
    /// Create a `Resume` command with the given human input.
    pub fn resume(input: HumanInput) -> Self {
        Self::Resume { human_input: input }
    }

    /// Create a `Goto` command targeting a specific node.
    pub fn goto(node: impl Into<String>) -> Self {
        Self::Goto { node: node.into() }
    }

    /// Create an `Update` command with a JSON value.
    pub fn update(value: serde_json::Value) -> Self {
        Self::Update { update: value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_resume_construction() {
        let cmd = Command::resume(HumanInput {
            approved: true,
            feedback: Some("ok".into()),
            data: None,
        });
        match cmd {
            Command::Resume { human_input } => {
                assert!(human_input.approved);
                assert_eq!(human_input.feedback.as_deref(), Some("ok"));
            }
            _ => panic!("expected Resume"),
        }
    }

    #[test]
    fn test_goto_construction() {
        let cmd = Command::goto("my_node");
        match cmd {
            Command::Goto { node } => assert_eq!(node, "my_node"),
            _ => panic!("expected Goto"),
        }
    }

    #[test]
    fn test_update_construction() {
        let cmd = Command::update(serde_json::json!({"key": "value"}));
        match cmd {
            Command::Update { update } => {
                assert_eq!(update["key"], "value");
            }
            _ => panic!("expected Update"),
        }
    }

    #[test]
    fn test_command_serialization_round_trip() {
        let cmd = Command::resume(HumanInput {
            approved: false,
            feedback: None,
            data: Some(serde_json::json!(42)),
        });
        let json = serde_json::to_string(&cmd).unwrap();
        let restored: Command = serde_json::from_str(&json).unwrap();
        match restored {
            Command::Resume { human_input } => {
                assert!(!human_input.approved);
                assert_eq!(human_input.data, Some(serde_json::json!(42)));
            }
            _ => panic!("expected Resume"),
        }
    }
}