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}