Skip to main content

oxidized_state/
storage_traits.rs

1//! Storage trait definitions for AIVCS
2//!
3//! These traits define the core storage abstractions:
4//! - `CasStore`: Content-addressed storage (put/get by digest)
5//! - `RunLedger`: Execution run persistence (events, summaries)
6//! - `ReleaseRegistry`: Agent release management (promote/rollback)
7//!
8//! All traits are async and backend-agnostic. In-memory fakes are provided
9//! for testing via the `fakes` module.
10
11use async_trait::async_trait;
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use sha2::Sha256;
15
16use crate::error::StorageError;
17
18/// Result type for storage operations
19pub type StorageResult<T> = std::result::Result<T, StorageError>;
20
21// ---------------------------------------------------------------------------
22// CasStore — Content-Addressed Storage
23// ---------------------------------------------------------------------------
24
25/// Content digest (SHA-256 hex string).
26///
27/// The inner field is private to guarantee the string is always valid
28/// lowercase hex produced by `from_bytes` or validated via `TryFrom<String>`.
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct ContentDigest(String);
31
32impl ContentDigest {
33    /// Compute the SHA-256 digest of the given bytes.
34    pub fn from_bytes(data: &[u8]) -> Self {
35        use sha2::Digest;
36        let mut hasher = Sha256::new();
37        hasher.update(data);
38        ContentDigest(hex::encode(hasher.finalize()))
39    }
40
41    /// Return the full hex string.
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    /// Short form (first 12 hex chars).
47    pub fn short(&self) -> String {
48        self.0.chars().take(12).collect()
49    }
50}
51
52impl TryFrom<String> for ContentDigest {
53    type Error = StorageError;
54
55    fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
56        if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
57            return Err(StorageError::InvalidDigest { digest: s });
58        }
59        Ok(ContentDigest(s.to_ascii_lowercase()))
60    }
61}
62
63impl std::fmt::Display for ContentDigest {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.0)
66    }
67}
68
69/// Content-addressed blob store.
70///
71/// Guarantees:
72/// - `put(data)` always returns the SHA-256 digest of `data`.
73/// - `get(digest)` returns the exact bytes previously stored.
74/// - Same content always yields the same digest (deduplication).
75#[async_trait]
76pub trait CasStore: Send + Sync {
77    /// Store bytes and return their content digest.
78    async fn put(&self, data: &[u8]) -> StorageResult<ContentDigest>;
79
80    /// Retrieve bytes by digest. Returns `StorageError::NotFound` if absent.
81    async fn get(&self, digest: &ContentDigest) -> StorageResult<Vec<u8>>;
82
83    /// Check whether a digest exists in the store.
84    async fn contains(&self, digest: &ContentDigest) -> StorageResult<bool>;
85
86    /// Delete content by digest. No-op if absent.
87    async fn delete(&self, digest: &ContentDigest) -> StorageResult<()>;
88}
89
90// ---------------------------------------------------------------------------
91// RunLedger — Execution Run Persistence
92// ---------------------------------------------------------------------------
93
94/// Unique identifier for an execution run
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub struct RunId(pub String);
97
98impl RunId {
99    /// Generate a new random RunId
100    pub fn new() -> Self {
101        RunId(uuid::Uuid::new_v4().to_string())
102    }
103}
104
105impl Default for RunId {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl std::fmt::Display for RunId {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{}", self.0)
114    }
115}
116
117/// Metadata attached to a run at creation time
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RunMetadata {
120    /// Git SHA at time of run
121    pub git_sha: Option<String>,
122    /// Agent name
123    pub agent_name: String,
124    /// Arbitrary key-value tags
125    pub tags: serde_json::Value,
126}
127
128/// A single event in an execution run
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct RunEvent {
131    /// Monotonic sequence number within the run
132    pub seq: u64,
133    /// Event kind (e.g. "graph_started", "node_entered", "tool_called")
134    pub kind: String,
135    /// Event payload
136    pub payload: serde_json::Value,
137    /// Timestamp
138    pub timestamp: DateTime<Utc>,
139}
140
141/// Summary produced when a run completes
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RunSummary {
144    /// Total events recorded
145    pub total_events: u64,
146    /// Final state digest (if applicable)
147    pub final_state_digest: Option<ContentDigest>,
148    /// Duration in milliseconds
149    pub duration_ms: u64,
150    /// Whether the run succeeded
151    pub success: bool,
152}
153
154/// Status of a run
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "UPPERCASE")]
157pub enum RunStatus {
158    Running,
159    Completed,
160    Failed,
161    Cancelled,
162}
163
164/// Full run record
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RunRecord {
167    pub run_id: RunId,
168    pub spec_digest: ContentDigest,
169    pub metadata: RunMetadata,
170    pub status: RunStatus,
171    pub summary: Option<RunSummary>,
172    pub created_at: DateTime<Utc>,
173    pub completed_at: Option<DateTime<Utc>>,
174}
175
176/// Execution run ledger.
177///
178/// Guarantees:
179/// - Events are ordered by monotonic `seq` within a run.
180/// - A run transitions: Running → Completed | Failed (terminal).
181/// - Completed runs are immutable.
182#[async_trait]
183pub trait RunLedger: Send + Sync {
184    /// Create a new run, returning its unique ID.
185    async fn create_run(
186        &self,
187        spec_digest: &ContentDigest,
188        metadata: RunMetadata,
189    ) -> StorageResult<RunId>;
190
191    /// Append an event to an active run. Fails if the run is completed/failed.
192    async fn append_event(&self, run_id: &RunId, event: RunEvent) -> StorageResult<()>;
193
194    /// Mark a run as completed with a summary.
195    async fn complete_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
196
197    /// Mark a run as failed with a summary.
198    async fn fail_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
199
200    /// Mark a run as cancelled.
201    async fn cancel_run(&self, run_id: &RunId, summary: RunSummary) -> StorageResult<()>;
202
203    /// Retrieve a run record by ID.
204    async fn get_run(&self, run_id: &RunId) -> StorageResult<RunRecord>;
205
206    /// Retrieve all events for a run, ordered by seq.
207    async fn get_events(&self, run_id: &RunId) -> StorageResult<Vec<RunEvent>>;
208
209    /// List runs, optionally filtered by spec digest.
210    async fn list_runs(&self, spec_digest: Option<&ContentDigest>)
211        -> StorageResult<Vec<RunRecord>>;
212}
213
214// ---------------------------------------------------------------------------
215// ReleaseRegistry — Agent Release Management
216// ---------------------------------------------------------------------------
217
218/// Metadata for a release
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct ReleaseMetadata {
221    /// Human-readable version label (e.g. "v1.2.3")
222    pub version_label: Option<String>,
223    /// Who or what promoted this release
224    pub promoted_by: String,
225    /// Release notes
226    pub notes: Option<String>,
227}
228
229/// A single release record (pointer from name → spec digest)
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ReleaseRecord {
232    /// Agent name this release belongs to
233    pub name: String,
234    /// The spec digest being released
235    pub spec_digest: ContentDigest,
236    /// Release metadata
237    pub metadata: ReleaseMetadata,
238    /// When this release was created
239    pub created_at: DateTime<Utc>,
240}
241
242/// Agent release registry.
243///
244/// Semantics:
245/// - `promote` creates a new release entry as the new current release.
246/// - `rollback` reverts to the previous release by re-appending it as a new
247///   entry, preserving the full audit trail (history is append-only).
248/// - `history` returns the complete release chain in reverse chronological
249///   order (newest first).
250#[async_trait]
251pub trait ReleaseRegistry: Send + Sync {
252    /// Promote a new release for the given agent name.
253    async fn promote(
254        &self,
255        name: &str,
256        spec_digest: &ContentDigest,
257        metadata: ReleaseMetadata,
258    ) -> StorageResult<ReleaseRecord>;
259
260    /// Roll back to the previous release. Fails if no previous release exists.
261    async fn rollback(&self, name: &str) -> StorageResult<ReleaseRecord>;
262
263    /// Get the current (most recent) release for a name, if any.
264    async fn current(&self, name: &str) -> StorageResult<Option<ReleaseRecord>>;
265
266    /// Get full release history for a name (newest first).
267    async fn history(&self, name: &str) -> StorageResult<Vec<ReleaseRecord>>;
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_run_status_serialization() {
276        let status = RunStatus::Running;
277        let json = serde_json::to_string(&status).unwrap();
278        assert_eq!(json, "\"RUNNING\"");
279
280        let status = RunStatus::Completed;
281        let json = serde_json::to_string(&status).unwrap();
282        assert_eq!(json, "\"COMPLETED\"");
283    }
284}