Skip to main content

adk_core/
shared_state.rs

1//! Thread-safe shared state for parallel agent coordination.
2//!
3//! [`SharedState`] is a concurrent key-value store scoped to a single
4//! `ParallelAgent::run()` invocation. Sub-agents use [`SharedState::set_shared`],
5//! [`SharedState::get_shared`], and [`SharedState::wait_for_key`] to exchange data and coordinate.
6
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10
11use serde::ser::{Serialize, SerializeMap, Serializer};
12use serde_json::Value;
13use tokio::sync::{Notify, RwLock};
14
15use crate::AdkError;
16
17/// Minimum allowed timeout for [`SharedState::wait_for_key`].
18const MIN_TIMEOUT: Duration = Duration::from_millis(1);
19/// Maximum allowed timeout for [`SharedState::wait_for_key`].
20const MAX_TIMEOUT: Duration = Duration::from_secs(300);
21/// Maximum key length in bytes.
22const MAX_KEY_LEN: usize = 256;
23
24/// Errors from [`SharedState`] operations.
25#[derive(Debug, thiserror::Error)]
26pub enum SharedStateError {
27    /// Key must not be empty.
28    #[error("shared state key must not be empty")]
29    EmptyKey,
30
31    /// Key exceeds the maximum length.
32    #[error("shared state key exceeds 256 bytes: {len} bytes")]
33    KeyTooLong {
34        /// The actual length of the key in bytes.
35        len: usize,
36    },
37
38    /// `wait_for_key` timed out.
39    #[error("wait_for_key timed out after {timeout:?} for key \"{key}\"")]
40    Timeout {
41        /// The key that was being waited on.
42        key: String,
43        /// The timeout duration that expired.
44        timeout: Duration,
45    },
46
47    /// Timeout value is outside the valid range.
48    #[error("invalid timeout {timeout:?}: must be between 1ms and 300s")]
49    InvalidTimeout {
50        /// The invalid timeout value that was provided.
51        timeout: Duration,
52    },
53}
54
55impl From<SharedStateError> for AdkError {
56    fn from(err: SharedStateError) -> Self {
57        AdkError::agent(err.to_string())
58    }
59}
60
61/// Thread-safe key-value store for parallel agent coordination.
62///
63/// Scoped to a single `ParallelAgent::run()` invocation. All sub-agents
64/// share the same `Arc<SharedState>` instance.
65///
66/// # Example
67///
68/// ```rust,ignore
69/// use adk_core::SharedState;
70/// use std::sync::Arc;
71/// use std::time::Duration;
72///
73/// let state = Arc::new(SharedState::new());
74///
75/// // Agent A publishes a workbook handle
76/// state.set_shared("workbook_id", serde_json::json!("wb-123")).await?;
77///
78/// // Agent B waits for the handle
79/// let handle = state.wait_for_key("workbook_id", Duration::from_secs(30)).await?;
80/// ```
81#[derive(Debug)]
82pub struct SharedState {
83    data: RwLock<HashMap<String, Value>>,
84    notifiers: RwLock<HashMap<String, Arc<Notify>>>,
85}
86
87impl SharedState {
88    /// Creates a new empty `SharedState`.
89    #[must_use]
90    pub fn new() -> Self {
91        Self { data: RwLock::new(HashMap::new()), notifiers: RwLock::new(HashMap::new()) }
92    }
93
94    /// Inserts a key-value pair. Notifies all waiters on that key.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`SharedStateError::EmptyKey`] if key is empty.
99    /// Returns [`SharedStateError::KeyTooLong`] if key exceeds 256 bytes.
100    pub async fn set_shared(
101        &self,
102        key: impl Into<String>,
103        value: Value,
104    ) -> Result<(), SharedStateError> {
105        let key = key.into();
106        validate_key(&key)?;
107
108        self.data.write().await.insert(key.clone(), value);
109
110        // Notify all waiters for this key
111        let notifiers = self.notifiers.read().await;
112        if let Some(notify) = notifiers.get(&key) {
113            notify.notify_waiters();
114        }
115
116        Ok(())
117    }
118
119    /// Returns the value for a key, or `None` if not present.
120    pub async fn get_shared(&self, key: &str) -> Option<Value> {
121        self.data.read().await.get(key).cloned()
122    }
123
124    /// Blocks until the key appears, or the timeout expires.
125    ///
126    /// If the key already exists, returns immediately. A [`SharedState::set_shared`]
127    /// for the key that runs concurrently with this call is never missed: the waiter
128    /// registers for notification before each read of the key.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`SharedStateError::Timeout`] if the timeout expires.
133    /// Returns [`SharedStateError::InvalidTimeout`] if timeout is outside [1ms, 300s].
134    pub async fn wait_for_key(
135        &self,
136        key: &str,
137        timeout: Duration,
138    ) -> Result<Value, SharedStateError> {
139        self.wait_for_key_with(key, timeout, || async {}).await
140    }
141
142    /// Implements [`SharedState::wait_for_key`]; `after_miss` runs each time the key is
143    /// found absent, while this waiter is already registered, which is the window a
144    /// concurrent `set_shared` must not fall into.
145    async fn wait_for_key_with<F, Fut>(
146        &self,
147        key: &str,
148        timeout: Duration,
149        mut after_miss: F,
150    ) -> Result<Value, SharedStateError>
151    where
152        F: FnMut() -> Fut,
153        Fut: std::future::Future<Output = ()>,
154    {
155        if timeout < MIN_TIMEOUT || timeout > MAX_TIMEOUT {
156            return Err(SharedStateError::InvalidTimeout { timeout });
157        }
158
159        // Fast path without taking the notifier write lock.
160        if let Some(value) = self.data.read().await.get(key).cloned() {
161            return Ok(value);
162        }
163
164        let notify = {
165            let mut notifiers = self.notifiers.write().await;
166            notifiers.entry(key.to_string()).or_insert_with(|| Arc::new(Notify::new())).clone()
167        };
168
169        let deadline = tokio::time::Instant::now() + timeout;
170        loop {
171            // Register before reading the key: `notify_waiters` wakes every `Notified`
172            // that exists when it is called, so a `set_shared` landing after the read
173            // below still wakes this waiter.
174            let notified = notify.notified();
175            tokio::pin!(notified);
176            notified.as_mut().enable();
177
178            if let Some(value) = self.data.read().await.get(key).cloned() {
179                return Ok(value);
180            }
181            after_miss().await;
182
183            if tokio::time::timeout_at(deadline, notified).await.is_err() {
184                return Err(SharedStateError::Timeout { key: key.to_string(), timeout });
185            }
186        }
187    }
188
189    /// Returns a snapshot of all current entries.
190    pub async fn snapshot(&self) -> HashMap<String, Value> {
191        self.data.read().await.clone()
192    }
193}
194
195impl Default for SharedState {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl Serialize for SharedState {
202    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
203        // Use try_read to avoid blocking in a sync context.
204        // If the lock is held, serialize as empty map.
205        match self.data.try_read() {
206            Ok(data) => {
207                let mut map = serializer.serialize_map(Some(data.len()))?;
208                for (k, v) in data.iter() {
209                    map.serialize_entry(k, v)?;
210                }
211                map.end()
212            }
213            Err(_) => serializer.serialize_map(Some(0))?.end(),
214        }
215    }
216}
217
218/// Validates a shared state key.
219fn validate_key(key: &str) -> Result<(), SharedStateError> {
220    if key.is_empty() {
221        return Err(SharedStateError::EmptyKey);
222    }
223    if key.len() > MAX_KEY_LEN {
224        return Err(SharedStateError::KeyTooLong { len: key.len() });
225    }
226    Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[tokio::test]
234    async fn new_shared_state_is_empty() {
235        let state = SharedState::new();
236        assert!(state.snapshot().await.is_empty());
237    }
238
239    #[tokio::test]
240    async fn set_and_get() {
241        let state = SharedState::new();
242        state.set_shared("key", serde_json::json!("value")).await.unwrap();
243        assert_eq!(state.get_shared("key").await, Some(serde_json::json!("value")));
244    }
245
246    #[tokio::test]
247    async fn get_missing_returns_none() {
248        let state = SharedState::new();
249        assert_eq!(state.get_shared("missing").await, None);
250    }
251
252    #[tokio::test]
253    async fn overwrite_replaces_value() {
254        let state = SharedState::new();
255        state.set_shared("key", serde_json::json!(1)).await.unwrap();
256        state.set_shared("key", serde_json::json!(2)).await.unwrap();
257        assert_eq!(state.get_shared("key").await, Some(serde_json::json!(2)));
258    }
259
260    #[tokio::test]
261    async fn empty_key_rejected() {
262        let state = SharedState::new();
263        let err = state.set_shared("", serde_json::json!(1)).await.unwrap_err();
264        assert!(matches!(err, SharedStateError::EmptyKey));
265    }
266
267    #[tokio::test]
268    async fn long_key_rejected() {
269        let state = SharedState::new();
270        let long_key = "x".repeat(257);
271        let err = state.set_shared(long_key, serde_json::json!(1)).await.unwrap_err();
272        assert!(matches!(err, SharedStateError::KeyTooLong { .. }));
273    }
274
275    #[tokio::test]
276    async fn key_at_256_bytes_accepted() {
277        let state = SharedState::new();
278        let key = "x".repeat(256);
279        state.set_shared(key.clone(), serde_json::json!(1)).await.unwrap();
280        assert_eq!(state.get_shared(&key).await, Some(serde_json::json!(1)));
281    }
282
283    #[tokio::test]
284    async fn wait_for_existing_key_returns_immediately() {
285        let state = SharedState::new();
286        state.set_shared("key", serde_json::json!("val")).await.unwrap();
287        let val = state.wait_for_key("key", Duration::from_secs(1)).await.unwrap();
288        assert_eq!(val, serde_json::json!("val"));
289    }
290
291    #[tokio::test]
292    async fn wait_for_key_timeout() {
293        let state = SharedState::new();
294        let err = state.wait_for_key("missing", Duration::from_millis(10)).await.unwrap_err();
295        assert!(matches!(err, SharedStateError::Timeout { .. }));
296    }
297
298    #[tokio::test]
299    async fn wait_for_key_invalid_timeout_too_small() {
300        let state = SharedState::new();
301        let err = state.wait_for_key("key", Duration::from_nanos(1)).await.unwrap_err();
302        assert!(matches!(err, SharedStateError::InvalidTimeout { .. }));
303    }
304
305    #[tokio::test]
306    async fn wait_for_key_invalid_timeout_too_large() {
307        let state = SharedState::new();
308        let err = state.wait_for_key("key", Duration::from_secs(301)).await.unwrap_err();
309        assert!(matches!(err, SharedStateError::InvalidTimeout { .. }));
310    }
311
312    /// A `set_shared` landing after the waiter's read of the key, but before it starts
313    /// waiting, must wake it. Paused time turns a lost wakeup into an immediate timeout.
314    #[tokio::test(start_paused = true)]
315    async fn wait_for_key_sees_a_set_between_its_check_and_its_wait() {
316        let state = SharedState::new();
317        let state_ref = &state;
318        let mut misses = 0;
319        let value = state
320            .wait_for_key_with("raced", MAX_TIMEOUT, || {
321                misses += 1;
322                let first_miss = misses == 1;
323                async move {
324                    if first_miss {
325                        state_ref.set_shared("raced", serde_json::json!("val")).await.unwrap();
326                    }
327                }
328            })
329            .await
330            .unwrap();
331        assert_eq!(value, serde_json::json!("val"));
332        assert_eq!(misses, 1);
333    }
334
335    #[tokio::test]
336    async fn error_converts_to_adk_error() {
337        let err = SharedStateError::EmptyKey;
338        let adk_err: AdkError = err.into();
339        assert!(adk_err.to_string().contains("empty"));
340    }
341}