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}