Skip to main content

agent_effects/
payload.rs

1//! Versioned stored payloads and application-supplied JSON migrations.
2
3use std::sync::Arc;
4
5use serde_json::Value;
6
7/// Why a stored payload cannot be read by the current application.
8#[derive(Debug, thiserror::Error)]
9pub enum PayloadError {
10    /// No migration supports this stored schema.
11    #[error("unsupported payload schema {stored}; current schema is {current}")]
12    UnsupportedVersion {
13        /// The version written by the original application.
14        stored: u32,
15        /// The version expected by the current application.
16        current: u32,
17    },
18    /// The application's migration rejected the payload.
19    #[error("payload migration failed: {0}")]
20    Migration(String),
21}
22
23type Migration = Arc<dyn Fn(u32, Value) -> Result<Value, PayloadError> + Send + Sync>;
24
25/// A current schema version and optional migration from older stored JSON.
26///
27/// Migrations run only when versions differ. They must be deterministic,
28/// perform no external side effects, and avoid including secrets in errors.
29/// The runtime deserializes the returned JSON into the current Rust type;
30/// stored historical payloads remain unchanged.
31#[derive(Clone)]
32pub struct PayloadSchema {
33    version: u32,
34    migration: Option<Migration>,
35}
36
37impl Default for PayloadSchema {
38    fn default() -> Self {
39        Self::new(1)
40    }
41}
42
43impl PayloadSchema {
44    /// Uses `version` for newly serialized payloads. Legacy rows are version 1.
45    pub fn new(version: u32) -> Self {
46        Self {
47            version,
48            migration: None,
49        }
50    }
51
52    /// The version this application writes and expects to read.
53    pub fn version(&self) -> u32 {
54        self.version
55    }
56
57    /// Adds a migration receiving the stored version and JSON. Return an
58    /// explicit error for versions the application cannot safely interpret.
59    #[must_use]
60    pub fn migrate(
61        mut self,
62        migration: impl Fn(u32, Value) -> Result<Value, PayloadError> + Send + Sync + 'static,
63    ) -> Self {
64        self.migration = Some(Arc::new(migration));
65        self
66    }
67
68    /// Reads stored JSON, migrating it if needed. This does not update the
69    /// record, its input fingerprint, or its authoritative effect status.
70    ///
71    /// # Errors
72    ///
73    /// [`PayloadError::UnsupportedVersion`] when no migration is available,
74    /// or an application migration error.
75    pub fn decode(&self, stored: u32, value: Value) -> Result<Value, PayloadError> {
76        if stored == self.version {
77            return Ok(value);
78        }
79        match &self.migration {
80            Some(migrate) => migrate(stored, value),
81            None => Err(PayloadError::UnsupportedVersion {
82                stored,
83                current: self.version,
84            }),
85        }
86    }
87}