Skip to main content

pe_graph/
command.rs

1//! Graph commands -- how users send input back to a paused graph.
2//!
3//! After a graph returns [`ExecutionOutcome::Interrupted`](crate::compiled::ExecutionOutcome::Interrupted), the caller
4//! constructs a [`Command`] and passes it to [`CompiledGraph::resume_with()`](crate::compiled::CompiledGraph::resume_with)
5//! to continue execution.
6
7use pe_core::node::HumanInput;
8use serde::{Deserialize, Serialize};
9
10/// A command sent to a graph to control execution after an interrupt.
11///
12/// # Variants
13///
14/// - `Resume` -- provide human input and continue from the interrupted node
15/// - `Goto` -- jump to a specific node (skip the normal edge traversal)
16/// - `Update` -- apply a raw JSON update to state before resuming
17///
18/// # Example
19///
20/// ```ignore
21/// let cmd = Command::resume(HumanInput {
22///     approved: true,
23///     feedback: Some("Looks good".into()),
24///     data: None,
25/// });
26/// let outcome = graph.resume_with("thread-1", cmd, config).await?;
27/// ```
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[non_exhaustive]
30pub enum Command {
31    /// Resume from the interrupted node with human input.
32    Resume {
33        /// The human's response to the interrupt prompt.
34        human_input: HumanInput,
35    },
36
37    /// Jump to a specific node, bypassing normal edge traversal.
38    Goto {
39        /// Target node name to activate.
40        node: String,
41    },
42
43    /// Apply a raw JSON update to state before resuming.
44    Update {
45        /// Arbitrary state patch as JSON (deserialized by the caller).
46        update: serde_json::Value,
47    },
48}
49
50impl Command {
51    /// Create a `Resume` command with the given human input.
52    pub fn resume(input: HumanInput) -> Self {
53        Self::Resume { human_input: input }
54    }
55
56    /// Create a `Goto` command targeting a specific node.
57    pub fn goto(node: impl Into<String>) -> Self {
58        Self::Goto { node: node.into() }
59    }
60
61    /// Create an `Update` command with a JSON value.
62    pub fn update(value: serde_json::Value) -> Self {
63        Self::Update { update: value }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_resume_construction() {
73        let cmd = Command::resume(HumanInput {
74            approved: true,
75            feedback: Some("ok".into()),
76            data: None,
77        });
78        match cmd {
79            Command::Resume { human_input } => {
80                assert!(human_input.approved);
81                assert_eq!(human_input.feedback.as_deref(), Some("ok"));
82            }
83            _ => panic!("expected Resume"),
84        }
85    }
86
87    #[test]
88    fn test_goto_construction() {
89        let cmd = Command::goto("my_node");
90        match cmd {
91            Command::Goto { node } => assert_eq!(node, "my_node"),
92            _ => panic!("expected Goto"),
93        }
94    }
95
96    #[test]
97    fn test_update_construction() {
98        let cmd = Command::update(serde_json::json!({"key": "value"}));
99        match cmd {
100            Command::Update { update } => {
101                assert_eq!(update["key"], "value");
102            }
103            _ => panic!("expected Update"),
104        }
105    }
106
107    #[test]
108    fn test_command_serialization_round_trip() {
109        let cmd = Command::resume(HumanInput {
110            approved: false,
111            feedback: None,
112            data: Some(serde_json::json!(42)),
113        });
114        let json = serde_json::to_string(&cmd).unwrap();
115        let restored: Command = serde_json::from_str(&json).unwrap();
116        match restored {
117            Command::Resume { human_input } => {
118                assert!(!human_input.approved);
119                assert_eq!(human_input.data, Some(serde_json::json!(42)));
120            }
121            _ => panic!("expected Resume"),
122        }
123    }
124}