1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum GovernedMemoryTier {
10 Session,
11 Project,
12 Team,
13 Curated,
14}
15
16impl std::fmt::Display for GovernedMemoryTier {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::Session => write!(f, "session"),
20 Self::Project => write!(f, "project"),
21 Self::Team => write!(f, "team"),
22 Self::Curated => write!(f, "curated"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct MemoryPartition {
30 pub org_id: String,
31 pub workspace_id: String,
32 pub project_id: String,
33 pub tier: GovernedMemoryTier,
34}
35
36impl MemoryPartition {
37 pub fn key(&self) -> String {
38 format!(
39 "{}/{}/{}/{}",
40 self.org_id, self.workspace_id, self.project_id, self.tier
41 )
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum MemoryClassification {
48 Internal,
49 Restricted,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct MemoryCapabilities {
54 #[serde(default)]
55 pub read_tiers: Vec<GovernedMemoryTier>,
56 #[serde(default)]
57 pub write_tiers: Vec<GovernedMemoryTier>,
58 #[serde(default)]
59 pub promote_targets: Vec<GovernedMemoryTier>,
60 #[serde(default = "default_require_review_for_promote")]
61 pub require_review_for_promote: bool,
62 #[serde(default)]
63 pub allow_auto_use_tiers: Vec<GovernedMemoryTier>,
64}
65
66fn default_require_review_for_promote() -> bool {
67 true
68}
69
70impl Default for MemoryCapabilities {
71 fn default() -> Self {
72 Self {
73 read_tiers: vec![GovernedMemoryTier::Session, GovernedMemoryTier::Project],
74 write_tiers: vec![GovernedMemoryTier::Session],
75 promote_targets: Vec::new(),
76 require_review_for_promote: true,
77 allow_auto_use_tiers: vec![GovernedMemoryTier::Curated],
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct MemoryCapabilityToken {
85 pub run_id: String,
86 pub subject: String,
87 pub org_id: String,
88 pub workspace_id: String,
89 pub project_id: String,
90 pub memory: MemoryCapabilities,
91 pub expires_at: u64,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum MemoryContentKind {
97 SolutionCapsule,
98 Note,
99 Fact,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct MemoryPutRequest {
104 pub run_id: String,
105 pub partition: MemoryPartition,
106 pub kind: MemoryContentKind,
107 pub content: String,
108 #[serde(default)]
109 pub artifact_refs: Vec<String>,
110 pub classification: MemoryClassification,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub metadata: Option<serde_json::Value>,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct MemoryPutResponse {
117 pub id: String,
118 pub stored: bool,
119 pub tier: GovernedMemoryTier,
120 pub partition_key: String,
121 pub audit_id: String,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct PromotionReview {
126 pub required: bool,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub reviewer_id: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub approval_id: Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct MemoryPromoteRequest {
135 pub run_id: String,
136 pub source_memory_id: String,
137 pub from_tier: GovernedMemoryTier,
138 pub to_tier: GovernedMemoryTier,
139 pub partition: MemoryPartition,
140 pub reason: String,
141 pub review: PromotionReview,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum ScrubStatus {
147 Passed,
148 Redacted,
149 Blocked,
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct ScrubReport {
154 pub status: ScrubStatus,
155 pub redactions: u32,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub block_reason: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub struct MemoryPromoteResponse {
162 pub promoted: bool,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub new_memory_id: Option<String>,
165 pub to_tier: GovernedMemoryTier,
166 pub scrub_report: ScrubReport,
167 pub audit_id: String,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct MemorySearchRequest {
172 pub run_id: String,
173 pub query: String,
174 #[serde(default)]
175 pub read_scopes: Vec<GovernedMemoryTier>,
176 pub partition: MemoryPartition,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub limit: Option<i64>,
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct MemorySearchResponse {
183 #[serde(default)]
184 pub results: Vec<serde_json::Value>,
185 #[serde(default)]
186 pub scopes_used: Vec<GovernedMemoryTier>,
187 #[serde(default)]
188 pub blocked_scopes: Vec<GovernedMemoryTier>,
189 pub audit_id: String,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn default_capabilities_are_fail_safe() {
198 let caps = MemoryCapabilities::default();
199 assert_eq!(
200 caps.read_tiers,
201 vec![GovernedMemoryTier::Session, GovernedMemoryTier::Project]
202 );
203 assert_eq!(caps.write_tiers, vec![GovernedMemoryTier::Session]);
204 assert!(caps.promote_targets.is_empty());
205 assert!(caps.require_review_for_promote);
206 assert_eq!(caps.allow_auto_use_tiers, vec![GovernedMemoryTier::Curated]);
207 }
208
209 #[test]
210 fn partition_key_is_stable() {
211 let partition = MemoryPartition {
212 org_id: "org_acme".to_string(),
213 workspace_id: "ws_tandem".to_string(),
214 project_id: "proj_engine".to_string(),
215 tier: GovernedMemoryTier::Project,
216 };
217 assert_eq!(
218 partition.key(),
219 "org_acme/ws_tandem/proj_engine/project".to_string()
220 );
221 }
222}