Skip to main content

talos_session/
runtime_state.rs

1//! Durable Session-owned runtime activation identity.
2//!
3//! The transcript remains the model-visible audit trail, but it is subject to
4//! normal compaction. This sidecar state is the machine authority used to
5//! reconstruct the exact provider/model/variant and generation independently of
6//! the active transcript retention window.
7
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11/// Exact declarative model identity owned by one durable Session.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct SessionRuntimeIdentity {
14    pub provider: String,
15    pub model: String,
16    pub variant: Option<String>,
17}
18
19impl SessionRuntimeIdentity {
20    #[must_use]
21    pub fn new(provider: &str, model: &str, variant: Option<&str>) -> Self {
22        Self {
23            provider: provider.to_string(),
24            model: model.to_string(),
25            variant: normalize_variant_id(variant).map(str::to_string),
26        }
27    }
28
29    #[must_use]
30    pub fn display_name(&self) -> String {
31        match self.variant.as_deref() {
32            Some(variant) => format!("{}/{}@{variant}", self.provider, self.model),
33            None => format!("{}/{}", self.provider, self.model),
34        }
35    }
36}
37
38/// Immutable logical activation identity.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct SessionRuntimeActivation {
41    pub version: u8,
42    pub activation_id: String,
43    pub generation: u64,
44    pub previous: SessionRuntimeIdentity,
45    pub target: SessionRuntimeIdentity,
46}
47
48impl SessionRuntimeActivation {
49    #[must_use]
50    pub fn new(
51        generation: u64,
52        previous: SessionRuntimeIdentity,
53        target: SessionRuntimeIdentity,
54    ) -> Self {
55        let canonical = serde_json::to_vec(&(generation, &previous, &target))
56            .expect("runtime activation identity contains only serializable values");
57        let digest = Sha256::digest(canonical);
58        let suffix: String = digest
59            .iter()
60            .take(16)
61            .map(|byte| format!("{byte:02x}"))
62            .collect();
63        Self {
64            version: 1,
65            activation_id: format!("model-activation-g{generation}-{suffix}"),
66            generation,
67            previous,
68            target,
69        }
70    }
71
72    #[must_use]
73    pub fn is_valid(&self) -> bool {
74        self.version == 1
75            && self.activation_id
76                == Self::new(self.generation, self.previous.clone(), self.target.clone())
77                    .activation_id
78    }
79}
80
81/// Whether a staged activation has crossed the transcript marker barrier.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum SessionRuntimeActivationStatus {
85    PendingMarker,
86    Committed,
87}
88
89impl SessionRuntimeActivationStatus {
90    pub(crate) const fn as_str(self) -> &'static str {
91        match self {
92            Self::PendingMarker => "pending_marker",
93            Self::Committed => "committed",
94        }
95    }
96
97    pub(crate) fn parse(value: &str) -> Option<Self> {
98        match value {
99            "pending_marker" => Some(Self::PendingMarker),
100            "committed" => Some(Self::Committed),
101            _ => None,
102        }
103    }
104}
105
106/// Current durable Session runtime state.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct SessionRuntimeState {
109    pub activation: SessionRuntimeActivation,
110    pub status: SessionRuntimeActivationStatus,
111}
112
113fn normalize_variant_id(variant: Option<&str>) -> Option<&str> {
114    let variant = variant.map(str::trim).filter(|value| !value.is_empty())?;
115    (!variant.eq_ignore_ascii_case("default")).then_some(variant)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn default_spellings_share_one_identity() {
124        assert_eq!(
125            SessionRuntimeIdentity::new("openai", "o3", None),
126            SessionRuntimeIdentity::new("openai", "o3", Some(" DEFAULT "))
127        );
128    }
129
130    #[test]
131    fn activation_validation_detects_tampering() {
132        let mut activation = SessionRuntimeActivation::new(
133            7,
134            SessionRuntimeIdentity::new("openai", "o3", None),
135            SessionRuntimeIdentity::new("openai", "o3", Some("high-reasoning")),
136        );
137        assert!(activation.is_valid());
138        activation.target.variant = Some("low-reasoning".to_string());
139        assert!(!activation.is_valid());
140    }
141}