Skip to main content

stasis/ports/outbound/memory/
identity_memory_models.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4use chrono::{DateTime, Utc};
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use serde_json::Value;
8
9#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
10pub enum IdentityEntityType {
11    PersonaEntity,
12    UserEntity,
13    ContactEntity,
14    ChannelProfileEntity,
15    PolicyProfileEntity,
16    #[default]
17    RelationshipEntity,
18}
19
20#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)]
21pub enum UpdateTier {
22    #[default]
23    AutoCommit,
24    ConfirmRequired,
25    ApprovalRequired,
26}
27
28#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
29pub enum ProposalState {
30    Proposed,
31    Committed,
32    Rejected,
33    Expired,
34}
35
36#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
37pub enum UpdateSource {
38    UserDirect,
39    #[default]
40    ModelInferred,
41    SystemEvent,
42}
43
44#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
45pub enum RelationshipStatus {
46    #[default]
47    Proposed,
48    Active,
49    Suspended,
50    Deprecated,
51    Revoked,
52}
53
54#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
55#[serde(rename_all = "snake_case")]
56pub enum IdentityContextMode {
57    #[default]
58    Full,
59    Policy,
60    Cognitive,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Hash)]
64pub enum RelationshipKind {
65    AssistantUser,
66    UserChannel,
67    Knows,
68    Prefers,
69    Delegation,
70    Colleague,
71    Legacy(String),
72}
73
74impl RelationshipKind {
75    pub fn parse(value: &str) -> Self {
76        match value {
77            "assistant_user" => Self::AssistantUser,
78            "user_channel" => Self::UserChannel,
79            "knows" => Self::Knows,
80            "prefers" => Self::Prefers,
81            "delegation" => Self::Delegation,
82            "colleague" => Self::Colleague,
83            other => Self::Legacy(other.to_string()),
84        }
85    }
86
87    pub fn as_str(&self) -> &str {
88        match self {
89            Self::AssistantUser => "assistant_user",
90            Self::UserChannel => "user_channel",
91            Self::Knows => "knows",
92            Self::Prefers => "prefers",
93            Self::Delegation => "delegation",
94            Self::Colleague => "colleague",
95            Self::Legacy(value) => value.as_str(),
96        }
97    }
98
99    pub fn is_structural(&self) -> bool {
100        matches!(self, Self::AssistantUser | Self::UserChannel)
101    }
102
103    pub fn is_social(&self) -> bool {
104        matches!(
105            self,
106            Self::Knows | Self::Prefers | Self::Delegation | Self::Colleague
107        )
108    }
109}
110
111impl Default for RelationshipKind {
112    fn default() -> Self {
113        Self::AssistantUser
114    }
115}
116
117impl Serialize for RelationshipKind {
118    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119    where
120        S: Serializer,
121    {
122        serializer.serialize_str(self.as_str())
123    }
124}
125
126impl<'de> Deserialize<'de> for RelationshipKind {
127    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128    where
129        D: Deserializer<'de>,
130    {
131        struct RelationshipKindVisitor;
132
133        impl Visitor<'_> for RelationshipKindVisitor {
134            type Value = RelationshipKind;
135
136            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137                formatter.write_str("a relationship kind string")
138            }
139
140            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
141            where
142                E: de::Error,
143            {
144                Ok(RelationshipKind::parse(value))
145            }
146
147            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
148            where
149                E: de::Error,
150            {
151                Ok(RelationshipKind::parse(&value))
152            }
153        }
154
155        deserializer.deserialize_str(RelationshipKindVisitor)
156    }
157}
158
159#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
160pub struct EntityRef {
161    pub entity_type: String,
162    pub entity_id: String,
163}
164
165#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
166pub struct EscalationPolicy {
167    pub mode: Option<String>,
168    pub fallback: Option<String>,
169}
170
171#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
172pub struct InterruptionPolicy {
173    pub quiet_hours: Option<String>,
174    pub allow_urgent_only: Option<bool>,
175    pub urgent_threshold: Option<f32>,
176}
177
178#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)]
179pub struct AutonomyScope {
180    pub allow: Vec<String>,
181    pub deny: Vec<String>,
182    pub approval_required: Vec<String>,
183}
184
185#[derive(Clone, Debug, Default, Serialize, Deserialize)]
186pub struct PersonaEntity {
187    pub persona_id: String,
188    pub display_name: String,
189    pub status: String,
190    pub version: i32,
191    pub updated_at: DateTime<Utc>,
192}
193
194#[derive(Clone, Debug, Default, Serialize, Deserialize)]
195pub struct UserEntity {
196    pub user_id: String,
197    pub timezone: String,
198    pub language_variant: Option<String>,
199    #[serde(default)]
200    pub preferences: BTreeMap<String, Value>,
201    pub status: String,
202    pub version: i32,
203    pub updated_at: DateTime<Utc>,
204}
205
206#[derive(Clone, Debug, Default, Serialize, Deserialize)]
207pub struct ContactEntity {
208    pub contact_id: String,
209    pub display_name: String,
210    #[serde(default)]
211    pub aliases: Vec<String>,
212    pub status: String,
213    pub version: i32,
214    pub updated_at: DateTime<Utc>,
215}
216
217#[derive(Clone, Debug, Default, Serialize, Deserialize)]
218pub struct ChannelProfileEntity {
219    pub channel_id: String,
220    pub channel_type: String,
221    pub proactive_allowed: bool,
222    pub status: String,
223    pub version: i32,
224    pub updated_at: DateTime<Utc>,
225}
226
227#[derive(Clone, Debug, Default, Serialize, Deserialize)]
228pub struct PolicyProfileEntity {
229    pub policy_profile_id: String,
230    pub graph_max_depth: usize,
231    pub trust_delta_max_per_window: f32,
232    pub status: String,
233    pub version: i32,
234    pub updated_at: DateTime<Utc>,
235}
236
237#[derive(Clone, Debug, Default, Serialize, Deserialize)]
238pub struct RelationshipEntity {
239    pub relationship_id: String,
240    pub source_entity_ref: EntityRef,
241    pub target_entity_ref: EntityRef,
242    pub relationship_kind: RelationshipKind,
243    pub status: RelationshipStatus,
244    pub trust_level: f32,
245    pub confidence: f32,
246    pub strength_score: f32,
247    pub recency_score: f32,
248    pub autonomy_scope: AutonomyScope,
249    pub approval_profile_id: Option<String>,
250    pub interruption_policy: InterruptionPolicy,
251    pub escalation_policy: EscalationPolicy,
252    pub policy_tags: Vec<String>,
253    pub provenance: UpdateSource,
254    pub parent_relationship_id: Option<String>,
255    pub governing_relationship_ids: Vec<String>,
256    pub derived_from_relationship_id: Option<String>,
257    pub last_transition_reason: Option<String>,
258    pub transition_receipt_id: Option<String>,
259    pub version: i32,
260    pub created_at: DateTime<Utc>,
261    pub updated_at: DateTime<Utc>,
262}
263
264#[derive(Clone, Debug, Default, Serialize, Deserialize)]
265pub struct FlattenedPolicyClaim {
266    pub claim_id: String,
267    pub source_relationship_ids: Vec<String>,
268    pub summary: String,
269    pub confidence: f32,
270    pub timestamp: DateTime<Utc>,
271}
272
273#[derive(Clone, Debug, Default, Serialize, Deserialize)]
274pub struct GetIdentityContextRequest {
275    pub user_id: String,
276    pub persona_id: String,
277    pub channel_id: String,
278    pub relationship_limit: usize,
279    #[serde(default)]
280    pub mode: IdentityContextMode,
281}
282
283#[derive(Clone, Debug, Default, Serialize, Deserialize)]
284pub struct GetIdentityContextResponse {
285    pub persona: Option<PersonaEntity>,
286    pub user: Option<UserEntity>,
287    pub channel: Option<ChannelProfileEntity>,
288    #[serde(default)]
289    pub contacts: Vec<ContactEntity>,
290    pub relationships: Vec<RelationshipEntity>,
291    pub policy_profiles: Vec<PolicyProfileEntity>,
292    pub graph_depth_used: usize,
293    pub flattened_claims: Vec<FlattenedPolicyClaim>,
294}
295
296#[derive(Clone, Debug, Default, Serialize, Deserialize)]
297pub struct ProposeEntityUpdateRequest {
298    pub entity_type: IdentityEntityType,
299    pub entity_id: String,
300    pub patch: Value,
301    pub source: UpdateSource,
302    pub confidence: f32,
303    pub reason: String,
304    pub actor: String,
305    pub receipt_id: Option<String>,
306    pub expires_at: Option<DateTime<Utc>>,
307}
308
309#[derive(Clone, Debug, Default, Serialize, Deserialize)]
310pub struct ProposeEntityUpdateResponse {
311    pub proposal_ids: Vec<String>,
312    pub tiers: Vec<UpdateTier>,
313    pub requires_approval: bool,
314    pub split_patch: bool,
315    pub policy_notes: Vec<String>,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize)]
319pub struct EntityUpdateProposalRecord {
320    pub proposal_id: String,
321    pub entity_type: IdentityEntityType,
322    pub entity_id: String,
323    pub patch: Value,
324    pub tier: UpdateTier,
325    pub source: UpdateSource,
326    pub confidence: f32,
327    pub reason: String,
328    pub state: ProposalState,
329    pub approver: Option<String>,
330    pub actor: String,
331    pub receipt_id: Option<String>,
332    pub expires_at: Option<DateTime<Utc>>,
333    pub created_at: DateTime<Utc>,
334    pub updated_at: DateTime<Utc>,
335}
336
337#[derive(Clone, Debug, Default, Serialize, Deserialize)]
338pub struct CommitEntityUpdateRequest {
339    pub proposal_id: String,
340    pub expected_version: i32,
341    pub approver: Option<String>,
342}
343
344#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
345pub enum CommitOutcomeCode {
346    Ok,
347    StaleState,
348    ApprovalRequired,
349    PolicyDenied,
350    InvalidPatch,
351    ExpiredProposal,
352    NotFound,
353}
354
355#[derive(Clone, Debug, Default, Serialize, Deserialize)]
356pub struct CommitEntityUpdateResponse {
357    pub committed: bool,
358    pub code: Option<CommitOutcomeCode>,
359    pub entity_type: Option<IdentityEntityType>,
360    pub entity_id: Option<String>,
361    pub new_version: Option<i32>,
362    pub receipt_id: Option<String>,
363    pub transition_event_id: Option<String>,
364    pub sttp_bridge_node: Option<String>,
365    pub sttp_bridge_reason: Option<String>,
366    pub rationale: Option<String>,
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize)]
370pub struct RelationshipTransitionEvent {
371    pub event_id: String,
372    pub relationship_id: String,
373    pub from_status: Option<RelationshipStatus>,
374    pub to_status: RelationshipStatus,
375    pub reason: String,
376    pub actor: String,
377    pub receipt_id: Option<String>,
378    pub occurred_at: DateTime<Utc>,
379    pub metadata: Option<Value>,
380}
381
382#[derive(Clone, Debug, Default, Serialize, Deserialize)]
383pub struct ListEntityHistoryRequest {
384    pub entity_type: IdentityEntityType,
385    pub entity_id: String,
386    pub limit: usize,
387}
388
389#[derive(Clone, Debug, Default, Serialize, Deserialize)]
390pub struct ListEntityHistoryResponse {
391    pub proposals: Vec<EntityUpdateProposalRecord>,
392    pub transitions: Vec<RelationshipTransitionEvent>,
393}
394
395#[derive(Clone, Debug, Default, Serialize, Deserialize)]
396pub struct RollbackEntityVersionRequest {
397    pub entity_type: IdentityEntityType,
398    pub entity_id: String,
399    pub target_version: i32,
400    pub reason: String,
401    pub approver: String,
402}
403
404#[derive(Clone, Debug, Default, Serialize, Deserialize)]
405pub struct RollbackEntityVersionResponse {
406    pub rolled_back: bool,
407    pub new_version: Option<i32>,
408    pub rollback_receipt_id: Option<String>,
409    pub rationale: Option<String>,
410}
411
412#[cfg(test)]
413mod tests {
414    use super::{IdentityContextMode, RelationshipKind};
415
416    #[test]
417    fn relationship_kind_deserializes_legacy_strings() {
418        let kind: RelationshipKind =
419            serde_json::from_value(serde_json::json!("assistant_user")).expect("deserialize");
420        assert_eq!(kind, RelationshipKind::AssistantUser);
421
422        let social: RelationshipKind =
423            serde_json::from_value(serde_json::json!("knows")).expect("deserialize");
424        assert!(social.is_social());
425    }
426
427    #[test]
428    fn relationship_kind_serializes_snake_case() {
429        let encoded = serde_json::to_string(&RelationshipKind::Delegation).expect("serialize");
430        assert_eq!(encoded, "\"delegation\"");
431    }
432
433    #[test]
434    fn identity_context_mode_defaults_to_full() {
435        let request: super::GetIdentityContextRequest = serde_json::from_value(serde_json::json!({
436            "user_id": "u1",
437            "persona_id": "p1",
438            "channel_id": "c1",
439            "relationship_limit": 4
440        }))
441        .expect("request should default mode to full");
442        assert_eq!(request.mode, IdentityContextMode::Full);
443    }
444}