use chrono::{DateTime, Utc};
mod surreal_datetime {
use chrono::{DateTime, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
use surrealdb::sql::Datetime as SurrealDatetime;
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let sd = SurrealDatetime::from(*date);
serde::Serialize::serialize(&sd, serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let sd = SurrealDatetime::deserialize(deserializer)?;
Ok(DateTime::from(sd))
}
}
mod surreal_datetime_opt {
use chrono::{DateTime, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
use surrealdb::sql::Datetime as SurrealDatetime;
pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(d) => {
let sd = SurrealDatetime::from(*d);
serde::Serialize::serialize(&Some(sd), serializer)
}
None => serde::Serialize::serialize(&None::<SurrealDatetime>, serializer),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
let sd = Option::<SurrealDatetime>::deserialize(deserializer)?;
Ok(sd.map(DateTime::from))
}
}
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CommitId {
pub hash: String,
pub logic_hash: Option<String>,
pub state_hash: String,
pub env_hash: Option<String>,
}
impl CommitId {
pub fn from_state(state: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(state);
let state_hash = hex::encode(hasher.finalize());
Self::new(None, &state_hash, None)
}
pub fn new(logic_hash: Option<&str>, state_hash: &str, env_hash: Option<&str>) -> Self {
let mut hasher = Sha256::new();
hasher.update(b"L");
if let Some(lh) = logic_hash {
hasher.update(b"S");
hasher.update(lh.as_bytes());
} else {
hasher.update(b"N");
}
hasher.update(b"\0");
hasher.update(b"S:");
hasher.update(state_hash.as_bytes());
hasher.update(b"\0");
hasher.update(b"E");
if let Some(eh) = env_hash {
hasher.update(b"S");
hasher.update(eh.as_bytes());
} else {
hasher.update(b"N");
}
let composite = hex::encode(hasher.finalize());
CommitId {
hash: composite,
logic_hash: logic_hash.map(String::from),
state_hash: state_hash.to_string(),
env_hash: env_hash.map(String::from),
}
}
pub fn short(&self) -> String {
self.hash.chars().take(8).collect()
}
}
impl std::fmt::Display for CommitId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.hash)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitRecord {
pub id: Option<surrealdb::sql::Thing>,
pub commit_id: CommitId,
pub parent_ids: Vec<String>,
pub message: String,
pub author: String,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
pub branch: Option<String>,
}
impl CommitRecord {
pub fn new(commit_id: CommitId, parent_ids: Vec<String>, message: &str, author: &str) -> Self {
CommitRecord {
id: None,
commit_id,
parent_ids,
message: message.to_string(),
author: author.to_string(),
created_at: Utc::now(),
branch: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotRecord {
pub id: Option<surrealdb::sql::Thing>,
pub commit_id: String,
pub state: serde_json::Value,
pub size_bytes: u64,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
}
impl SnapshotRecord {
pub fn new(commit_id: &str, state: serde_json::Value) -> Self {
let size = serde_json::to_string(&state)
.map(|s| s.len() as u64)
.unwrap_or(0);
SnapshotRecord {
id: None,
commit_id: commit_id.to_string(),
state,
size_bytes: size,
created_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchRecord {
pub id: Option<surrealdb::sql::Thing>,
pub name: String,
pub head_commit_id: String,
pub is_default: bool,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
#[serde(with = "surreal_datetime")]
pub updated_at: DateTime<Utc>,
}
impl BranchRecord {
pub fn new(name: &str, head_commit_id: &str, is_default: bool) -> Self {
let now = Utc::now();
BranchRecord {
id: None,
name: name.to_string(),
head_commit_id: head_commit_id.to_string(),
is_default,
created_at: now,
updated_at: now,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentRecord {
pub id: Option<surrealdb::sql::Thing>,
pub agent_id: Uuid,
pub name: String,
pub agent_type: String,
pub config: serde_json::Value,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
}
impl AgentRecord {
pub fn new(name: &str, agent_type: &str, config: serde_json::Value) -> Self {
AgentRecord {
id: None,
agent_id: Uuid::new_v4(),
name: name.to_string(),
agent_type: agent_type.to_string(),
config,
created_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRecord {
pub id: Option<surrealdb::sql::Thing>,
pub commit_id: String,
pub key: String,
pub content: String,
pub embedding: Option<Vec<f32>>,
pub metadata: serde_json::Value,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
}
impl MemoryRecord {
pub fn new(commit_id: &str, key: &str, content: &str) -> Self {
MemoryRecord {
id: None,
commit_id: commit_id.to_string(),
key: key.to_string(),
content: content.to_string(),
embedding: None,
metadata: serde_json::json!({}),
created_at: Utc::now(),
}
}
pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
self.embedding = Some(embedding);
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
pub child_id: String,
pub parent_id: String,
pub edge_type: EdgeType,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum EdgeType {
Normal,
Merge,
Fork,
}
impl GraphEdge {
pub fn new(child_id: &str, parent_id: &str) -> Self {
GraphEdge {
child_id: child_id.to_string(),
parent_id: parent_id.to_string(),
edge_type: EdgeType::Normal,
created_at: Utc::now(),
}
}
pub fn merge(child_id: &str, parent_id: &str) -> Self {
GraphEdge {
child_id: child_id.to_string(),
parent_id: parent_id.to_string(),
edge_type: EdgeType::Merge,
created_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecord {
pub id: Option<surrealdb::sql::Thing>,
pub run_id: String,
pub spec_digest: String,
pub git_sha: Option<String>,
pub agent_name: String,
pub tags: serde_json::Value,
pub status: String,
pub total_events: u64,
pub final_state_digest: Option<String>,
pub duration_ms: u64,
pub success: bool,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
#[serde(default, with = "surreal_datetime_opt")]
pub completed_at: Option<DateTime<Utc>>,
}
impl RunRecord {
pub fn new(
run_id: String,
spec_digest: String,
git_sha: Option<String>,
agent_name: String,
tags: serde_json::Value,
) -> Self {
RunRecord {
id: None,
run_id,
spec_digest,
git_sha,
agent_name,
tags,
status: "RUNNING".to_string(),
total_events: 0,
final_state_digest: None,
duration_ms: 0,
success: false,
created_at: Utc::now(),
completed_at: None,
}
}
pub fn complete(
mut self,
total_events: u64,
final_state_digest: Option<String>,
duration_ms: u64,
) -> Self {
self.status = "COMPLETED".to_string();
self.total_events = total_events;
self.final_state_digest = final_state_digest;
self.duration_ms = duration_ms;
self.success = true;
self.completed_at = Some(Utc::now());
self
}
pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
self.status = "FAILED".to_string();
self.total_events = total_events;
self.duration_ms = duration_ms;
self.success = false;
self.completed_at = Some(Utc::now());
self
}
pub fn cancel(mut self, total_events: u64, duration_ms: u64) -> Self {
self.status = "CANCELLED".to_string();
self.total_events = total_events;
self.duration_ms = duration_ms;
self.success = false;
self.completed_at = Some(Utc::now());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunEventRecord {
pub id: Option<surrealdb::sql::Thing>,
pub run_id: String,
pub seq: u64,
pub kind: String,
pub payload: serde_json::Value,
#[serde(with = "surreal_datetime")]
pub timestamp: DateTime<Utc>,
}
impl RunEventRecord {
pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
RunEventRecord {
id: None,
run_id,
seq,
kind,
payload,
timestamp: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseRecordSchema {
pub id: Option<surrealdb::sql::Thing>,
pub name: String,
pub spec_digest: String,
pub version_label: Option<String>,
pub promoted_by: String,
pub notes: Option<String>,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
}
impl ReleaseRecordSchema {
pub fn new(
name: String,
spec_digest: String,
version_label: Option<String>,
promoted_by: String,
notes: Option<String>,
) -> Self {
ReleaseRecordSchema {
id: None,
name,
spec_digest,
version_label,
promoted_by,
notes,
created_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionRecord {
pub id: Option<surrealdb::sql::Thing>,
pub decision_id: String,
pub commit_id: String,
pub task: String,
pub action: String,
pub rationale: String,
pub alternatives: serde_json::Value,
pub confidence: f32,
pub outcome: Option<String>, #[serde(with = "surreal_datetime")]
pub timestamp: DateTime<Utc>,
#[serde(default, with = "surreal_datetime_opt")]
pub outcome_at: Option<DateTime<Utc>>,
}
impl DecisionRecord {
pub fn new(
decision_id: String,
commit_id: String,
task: String,
action: String,
rationale: String,
confidence: f32,
) -> Self {
DecisionRecord {
id: None,
decision_id,
commit_id,
task,
action,
rationale,
alternatives: serde_json::json!([]),
confidence,
outcome: None,
timestamp: Utc::now(),
outcome_at: None,
}
}
pub fn with_alternatives(mut self, alternatives: Vec<String>) -> Self {
self.alternatives = serde_json::json!(alternatives);
self
}
pub fn with_outcome(mut self, outcome: String) -> Self {
self.outcome = Some(outcome);
self.outcome_at = Some(Utc::now());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProvenanceSourceType {
RunTrace,
StateSnapshot,
UserAnnotation,
MemoryDerivation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryProvenanceRecord {
pub id: Option<surrealdb::sql::Thing>,
pub memory_id: String,
pub source_type: String,
pub source_data: serde_json::Value,
pub derived_from: Option<String>,
#[serde(with = "surreal_datetime")]
pub created_at: DateTime<Utc>,
#[serde(default, with = "surreal_datetime_opt")]
pub invalidated_at: Option<DateTime<Utc>>,
}
impl MemoryProvenanceRecord {
pub fn from_run_trace(memory_id: String, run_id: String, event_idx: usize) -> Self {
MemoryProvenanceRecord {
id: None,
memory_id,
source_type: ProvenanceSourceType::RunTrace.to_string(),
source_data: serde_json::json!({ "run_id": run_id, "event_idx": event_idx }),
derived_from: None,
created_at: Utc::now(),
invalidated_at: None,
}
}
pub fn from_snapshot(memory_id: String, commit_id: String) -> Self {
MemoryProvenanceRecord {
id: None,
memory_id,
source_type: ProvenanceSourceType::StateSnapshot.to_string(),
source_data: serde_json::json!({ "commit_id": commit_id }),
derived_from: None,
created_at: Utc::now(),
invalidated_at: None,
}
}
pub fn from_user_annotation(memory_id: String, user_id: String) -> Self {
MemoryProvenanceRecord {
id: None,
memory_id,
source_type: ProvenanceSourceType::UserAnnotation.to_string(),
source_data: serde_json::json!({ "user_id": user_id }),
derived_from: None,
created_at: Utc::now(),
invalidated_at: None,
}
}
pub fn from_derivation(memory_id: String, parent_id: String, derivation: String) -> Self {
MemoryProvenanceRecord {
id: None,
memory_id,
source_type: ProvenanceSourceType::MemoryDerivation.to_string(),
source_data: serde_json::json!({ "derivation": derivation }),
derived_from: Some(parent_id),
created_at: Utc::now(),
invalidated_at: None,
}
}
pub fn invalidate(mut self) -> Self {
self.invalidated_at = Some(Utc::now());
self
}
}
impl core::fmt::Display for ProvenanceSourceType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ProvenanceSourceType::RunTrace => write!(f, "run_trace"),
ProvenanceSourceType::StateSnapshot => write!(f, "state_snapshot"),
ProvenanceSourceType::UserAnnotation => write!(f, "user_annotation"),
ProvenanceSourceType::MemoryDerivation => write!(f, "memory_derivation"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_commit_id_from_state() {
let state = b"test state data";
let commit_id = CommitId::from_state(state);
assert!(!commit_id.hash.is_empty());
assert_eq!(commit_id.hash.len(), 64); assert!(commit_id.logic_hash.is_none());
assert!(commit_id.env_hash.is_none());
}
#[test]
fn test_commit_id_deterministic() {
let state = b"same state";
let id1 = CommitId::from_state(state);
let id2 = CommitId::from_state(state);
assert_eq!(id1.hash, id2.hash);
}
#[test]
fn test_commit_id_different_states() {
let id1 = CommitId::from_state(b"state 1");
let id2 = CommitId::from_state(b"state 2");
assert_ne!(id1.hash, id2.hash);
}
#[test]
fn test_commit_id_short() {
let commit_id = CommitId::from_state(b"test");
assert_eq!(commit_id.short().len(), 8);
}
#[test]
fn test_composite_commit_id() {
let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
assert!(!commit_id.hash.is_empty());
assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
}
#[test]
fn test_commit_id_collision_prevention() {
let id1 = CommitId::new(Some("ab"), "cd", None);
let id2 = CommitId::new(Some("a"), "bcd", None);
assert_ne!(id1.hash, id2.hash);
let id3 = CommitId::new(None, "state", None);
let id4 = CommitId::new(Some("none"), "state", None);
assert_ne!(id3.hash, id4.hash);
}
#[test]
fn test_snapshot_record_size() {
let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
let snapshot = SnapshotRecord::new("commit-123", state);
assert!(snapshot.size_bytes > 0);
}
#[test]
fn test_run_record_new() {
let run = RunRecord::new(
"run-123".to_string(),
"spec-digest-abc".to_string(),
Some("abc123".to_string()),
"test-agent".to_string(),
serde_json::json!({"env": "test"}),
);
assert_eq!(run.run_id, "run-123");
assert_eq!(run.status, "RUNNING");
assert_eq!(run.total_events, 0);
assert!(!run.success);
}
#[test]
fn test_run_record_complete() {
let run = RunRecord::new(
"run-123".to_string(),
"spec-digest-abc".to_string(),
Some("abc123".to_string()),
"test-agent".to_string(),
serde_json::json!({}),
)
.complete(5, Some("state-digest-xyz".to_string()), 1000);
assert_eq!(run.status, "COMPLETED");
assert_eq!(run.total_events, 5);
assert!(run.success);
assert!(run.completed_at.is_some());
}
#[test]
fn test_run_record_fail() {
let run = RunRecord::new(
"run-123".to_string(),
"spec-digest-abc".to_string(),
None,
"test-agent".to_string(),
serde_json::json!({}),
)
.fail(2, 500);
assert_eq!(run.status, "FAILED");
assert_eq!(run.total_events, 2);
assert!(!run.success);
assert!(run.completed_at.is_some());
}
#[test]
fn test_run_event_record() {
let event = RunEventRecord::new(
"run-123".to_string(),
1,
"graph_started".to_string(),
serde_json::json!({"graph_id": "g1"}),
);
assert_eq!(event.run_id, "run-123");
assert_eq!(event.seq, 1);
assert_eq!(event.kind, "graph_started");
}
#[test]
fn test_release_record() {
let release = ReleaseRecordSchema::new(
"my-agent".to_string(),
"spec-digest-abc".to_string(),
Some("v1.0.0".to_string()),
"alice".to_string(),
Some("Initial release".to_string()),
);
assert_eq!(release.name, "my-agent");
assert_eq!(release.version_label, Some("v1.0.0".to_string()));
}
#[test]
fn test_decision_record_new() {
let decision = DecisionRecord::new(
"dec-123".to_string(),
"commit-abc".to_string(),
"task-optimize".to_string(),
"use_parallel".to_string(),
"Improves throughput".to_string(),
0.85,
);
assert_eq!(decision.decision_id, "dec-123");
assert_eq!(decision.commit_id, "commit-abc");
assert_eq!(decision.task, "task-optimize");
assert_eq!(decision.action, "use_parallel");
assert_eq!(decision.confidence, 0.85);
assert!(decision.outcome.is_none());
}
#[test]
fn test_decision_record_with_alternatives() {
let decision = DecisionRecord::new(
"dec-456".to_string(),
"commit-def".to_string(),
"task-retry".to_string(),
"exponential_backoff".to_string(),
"Reduces thundering herd".to_string(),
0.75,
)
.with_alternatives(vec!["linear_backoff".to_string(), "no_retry".to_string()]);
let alts: Vec<String> = serde_json::from_value(decision.alternatives).unwrap();
assert_eq!(alts.len(), 2);
assert!(alts.contains(&"linear_backoff".to_string()));
}
#[test]
fn test_decision_record_with_outcome() {
let outcome_json = serde_json::json!({
"status": "success",
"benefit": 1.23,
"duration_ms": 5000
});
let decision = DecisionRecord::new(
"dec-789".to_string(),
"commit-ghi".to_string(),
"task-cache".to_string(),
"redis_cache".to_string(),
"Faster lookups".to_string(),
0.9,
)
.with_outcome(outcome_json.to_string());
assert!(decision.outcome.is_some());
assert!(decision.outcome_at.is_some());
}
#[test]
fn test_memory_provenance_from_run_trace() {
let prov = MemoryProvenanceRecord::from_run_trace(
"mem-123".to_string(),
"run-456".to_string(),
42,
);
assert_eq!(prov.memory_id, "mem-123");
assert_eq!(prov.source_type, ProvenanceSourceType::RunTrace.to_string());
assert!(prov.derived_from.is_none());
assert!(prov.invalidated_at.is_none());
let source_data: serde_json::Value = prov.source_data;
assert_eq!(source_data["run_id"], "run-456");
assert_eq!(source_data["event_idx"], 42);
}
#[test]
fn test_memory_provenance_from_snapshot() {
let prov =
MemoryProvenanceRecord::from_snapshot("mem-789".to_string(), "commit-abc".to_string());
assert_eq!(prov.memory_id, "mem-789");
assert_eq!(
prov.source_type,
ProvenanceSourceType::StateSnapshot.to_string()
);
assert_eq!(prov.source_data["commit_id"], "commit-abc");
}
#[test]
fn test_memory_provenance_from_derivation() {
let prov = MemoryProvenanceRecord::from_derivation(
"mem-new".to_string(),
"mem-parent".to_string(),
"summarize".to_string(),
);
assert_eq!(prov.memory_id, "mem-new");
assert_eq!(prov.derived_from, Some("mem-parent".to_string()));
assert_eq!(
prov.source_type,
ProvenanceSourceType::MemoryDerivation.to_string()
);
assert_eq!(prov.source_data["derivation"], "summarize");
}
#[test]
fn test_memory_provenance_invalidation() {
let prov = MemoryProvenanceRecord::from_user_annotation(
"mem-123".to_string(),
"user-456".to_string(),
);
assert!(prov.invalidated_at.is_none());
let invalidated = prov.invalidate();
assert!(invalidated.invalidated_at.is_some());
}
#[test]
fn test_provenance_source_type_display() {
assert_eq!(ProvenanceSourceType::RunTrace.to_string(), "run_trace");
assert_eq!(
ProvenanceSourceType::StateSnapshot.to_string(),
"state_snapshot"
);
assert_eq!(
ProvenanceSourceType::UserAnnotation.to_string(),
"user_annotation"
);
assert_eq!(
ProvenanceSourceType::MemoryDerivation.to_string(),
"memory_derivation"
);
}
}