Skip to main content

adk_graph/
interrupt.rs

1//! Human-in-the-loop interrupt types
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Interrupt request from a node or configuration
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum Interrupt {
9    /// Interrupt before executing a node
10    Before(String),
11    /// Interrupt after executing a node
12    After(String),
13    /// Dynamic interrupt from within a node
14    Dynamic {
15        /// Message to display to the user
16        message: String,
17        /// Optional data for the interrupt
18        data: Option<Value>,
19    },
20}
21
22impl std::fmt::Display for Interrupt {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Self::Before(node) => write!(f, "Interrupt before '{}'", node),
26            Self::After(node) => write!(f, "Interrupt after '{}'", node),
27            Self::Dynamic { message, .. } => write!(f, "Dynamic interrupt: {}", message),
28        }
29    }
30}
31
32/// Helper to create a dynamic interrupt from within a node
33pub fn interrupt(message: &str) -> Interrupt {
34    Interrupt::Dynamic { message: message.to_string(), data: None }
35}
36
37/// Helper to create a dynamic interrupt with data
38pub fn interrupt_with_data(message: &str, data: Value) -> Interrupt {
39    Interrupt::Dynamic { message: message.to_string(), data: Some(data) }
40}
41
42/// The reserved `Event::provider_metadata` key carrying a graph interrupt.
43pub const INTERRUPT_METADATA_KEY: &str = "adk.graph.interrupt";
44
45/// A graph interrupt as it crosses the [`Agent`](adk_core::Agent) boundary.
46///
47/// `GraphAgent` cannot return `GraphError::Interrupted` to a `Runner`: the trait
48/// yields events, and an error would end the invocation. It therefore emits one
49/// event carrying this payload, so a caller can read which node paused, why, and
50/// which checkpoint to resume from.
51///
52/// It travels as JSON in `Event::provider_metadata` under
53/// [`INTERRUPT_METADATA_KEY`] rather than as a field on `adk_core::EventActions`.
54/// A graph-shaped type in `adk-core` would put a Tier 3 concept in a Tier 1
55/// crate; every other consumer of the event is unaffected by an extra metadata
56/// key.
57///
58/// # Example
59///
60/// ```rust,no_run
61/// use adk_graph::interrupt::GraphInterruptPayload;
62/// # fn handle(event: &adk_core::Event) {
63/// if let Some(pause) = GraphInterruptPayload::from_event(event) {
64///     println!("paused at {:?}: {:?}", pause.node, pause.message);
65///     // Resume by invoking the same thread again, supplying any decision.
66/// }
67/// # }
68/// ```
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct GraphInterruptPayload {
71    /// `"before"`, `"after"`, or `"dynamic"`.
72    pub kind: String,
73    /// The gated node, for a static interrupt. `None` for a dynamic one, which
74    /// carries a message instead.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub node: Option<String>,
77    /// The message a node supplied, for a dynamic interrupt.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub message: Option<String>,
80    /// The data a node attached with `NodeOutput::interrupt_with_data`.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub data: Option<Value>,
83    /// The thread to resume.
84    pub thread_id: String,
85    /// The checkpoint the run stopped at.
86    pub checkpoint_id: String,
87}
88
89impl GraphInterruptPayload {
90    /// Build a payload from an interrupt and the run it stopped.
91    pub fn new(interrupt: &Interrupt, thread_id: &str, checkpoint_id: &str) -> Self {
92        let (kind, node, message, data) = match interrupt {
93            Interrupt::Before(node) => ("before", Some(node.clone()), None, None),
94            Interrupt::After(node) => ("after", Some(node.clone()), None, None),
95            Interrupt::Dynamic { message, data } => {
96                ("dynamic", None, Some(message.clone()), data.clone())
97            }
98        };
99        Self {
100            kind: kind.to_string(),
101            node,
102            message,
103            data,
104            thread_id: thread_id.to_string(),
105            checkpoint_id: checkpoint_id.to_string(),
106        }
107    }
108
109    /// Read the payload from an event, or `None` if the event is not a graph
110    /// interrupt.
111    pub fn from_event(event: &adk_core::Event) -> Option<Self> {
112        let raw = event.provider_metadata.get(INTERRUPT_METADATA_KEY)?;
113        serde_json::from_str(raw).ok()
114    }
115
116    /// Serialize for transport in `Event::provider_metadata`.
117    pub fn to_metadata_value(&self) -> String {
118        serde_json::to_string(self).unwrap_or_default()
119    }
120}