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}