agentic_contracts/
sister.rs1use crate::errors::SisterResult;
4use crate::types::{Capability, HealthStatus, SisterType, Version};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SisterConfig {
19 #[serde(skip_serializing_if = "Option::is_none")]
22 pub data_path: Option<PathBuf>,
23
24 #[serde(default)]
31 pub data_paths: HashMap<String, PathBuf>,
32
33 pub create_if_missing: bool,
35
36 pub read_only: bool,
38
39 pub memory_budget_mb: Option<usize>,
41
42 #[serde(default)]
44 pub options: HashMap<String, serde_json::Value>,
45}
46
47impl Default for SisterConfig {
48 fn default() -> Self {
49 Self {
50 data_path: None,
51 data_paths: HashMap::new(),
52 create_if_missing: true,
53 read_only: false,
54 memory_budget_mb: None,
55 options: HashMap::new(),
56 }
57 }
58}
59
60impl SisterConfig {
61 pub fn new(data_path: impl Into<PathBuf>) -> Self {
63 Self {
64 data_path: Some(data_path.into()),
65 ..Default::default()
66 }
67 }
68
69 pub fn stateless() -> Self {
71 Self::default()
72 }
73
74 pub fn with_paths(paths: HashMap<String, PathBuf>) -> Self {
76 Self {
77 data_paths: paths,
78 ..Default::default()
79 }
80 }
81
82 pub fn primary_path(&self) -> PathBuf {
84 self.data_path.clone().unwrap_or_else(|| PathBuf::from("."))
85 }
86
87 pub fn get_path(&self, name: &str) -> Option<&PathBuf> {
89 self.data_paths.get(name)
90 }
91
92 pub fn add_path(mut self, name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
94 self.data_paths.insert(name.into(), path.into());
95 self
96 }
97
98 pub fn read_only(mut self, read_only: bool) -> Self {
100 self.read_only = read_only;
101 self
102 }
103
104 pub fn create_if_missing(mut self, create: bool) -> Self {
106 self.create_if_missing = create;
107 self
108 }
109
110 pub fn memory_budget(mut self, mb: usize) -> Self {
112 self.memory_budget_mb = Some(mb);
113 self
114 }
115
116 pub fn option(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
118 if let Ok(v) = serde_json::to_value(value) {
119 self.options.insert(key.into(), v);
120 }
121 self
122 }
123
124 pub fn get_option<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
126 self.options
127 .get(key)
128 .and_then(|v| serde_json::from_value(v.clone()).ok())
129 }
130}
131
132pub trait Sister: Send + Sync {
137 const SISTER_TYPE: SisterType;
139
140 const FILE_EXTENSION: &'static str;
142
143 fn init(config: SisterConfig) -> SisterResult<Self>
145 where
146 Self: Sized;
147
148 fn health(&self) -> HealthStatus;
150
151 fn version(&self) -> Version;
153
154 fn shutdown(&mut self) -> SisterResult<()>;
156
157 fn capabilities(&self) -> Vec<Capability>;
159
160 fn sister_type(&self) -> SisterType {
166 Self::SISTER_TYPE
167 }
168
169 fn file_extension(&self) -> &'static str {
171 Self::FILE_EXTENSION
172 }
173
174 fn is_healthy(&self) -> bool {
176 self.health().healthy
177 }
178
179 fn name(&self) -> String {
181 format!("Agentic{:?}", Self::SISTER_TYPE)
182 }
183
184 fn mcp_prefix(&self) -> &'static str {
186 Self::SISTER_TYPE.mcp_prefix()
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct SisterInfo {
193 pub sister_type: SisterType,
194 pub version: Version,
195 pub file_extension: String,
196 pub capabilities: Vec<Capability>,
197 pub mcp_prefix: String,
198}
199
200impl SisterInfo {
201 pub fn from_sister<S: Sister>(sister: &S) -> Self {
203 Self {
204 sister_type: S::SISTER_TYPE,
205 version: sister.version(),
206 file_extension: S::FILE_EXTENSION.to_string(),
207 capabilities: sister.capabilities(),
208 mcp_prefix: S::SISTER_TYPE.mcp_prefix().to_string(),
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_config_builder_single_path() {
219 let config = SisterConfig::new("/data/memory")
220 .read_only(true)
221 .memory_budget(512)
222 .option("custom_key", "custom_value");
223
224 assert_eq!(config.primary_path(), PathBuf::from("/data/memory"));
225 assert!(config.read_only);
226 assert_eq!(config.memory_budget_mb, Some(512));
227 assert_eq!(
228 config.get_option::<String>("custom_key"),
229 Some("custom_value".to_string())
230 );
231 }
232
233 #[test]
234 fn test_config_multi_path() {
235 let config = SisterConfig::default()
236 .add_path("identities", "/data/identities")
237 .add_path("receipts", "/data/receipts")
238 .add_path("trust", "/data/trust");
239
240 assert_eq!(
241 config.get_path("identities"),
242 Some(&PathBuf::from("/data/identities"))
243 );
244 assert_eq!(
245 config.get_path("receipts"),
246 Some(&PathBuf::from("/data/receipts"))
247 );
248 assert_eq!(config.data_paths.len(), 3);
249 }
250
251 #[test]
252 fn test_config_stateless() {
253 let config = SisterConfig::stateless();
254 assert!(config.data_path.is_none());
255 assert!(config.data_paths.is_empty());
256 }
257}