use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DispositionTrait {
Analytical,
Concise,
Cautious,
Creative,
Systematic,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MemoryBankConfig {
pub mission: Option<String>,
pub directives: Vec<String>,
pub disposition: Vec<DispositionTrait>,
}
impl MemoryBankConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_mission(mut self, mission: impl Into<String>) -> Self {
self.mission = Some(mission.into());
self
}
pub fn with_directive(mut self, directive: impl Into<String>) -> Self {
self.directives.push(directive.into());
self
}
pub fn with_disposition(mut self, trait_: DispositionTrait) -> Self {
if !self.disposition.contains(&trait_) {
self.disposition.push(trait_);
}
self
}
pub fn is_noop(&self) -> bool {
self.mission.is_none() && self.directives.is_empty() && self.disposition.is_empty()
}
pub fn mission_tag(&self) -> Option<String> {
self.mission.as_ref().map(|m| {
let slug = m
.to_lowercase()
.split_whitespace()
.collect::<Vec<_>>()
.join("_");
format!("mission:{slug}")
})
}
pub fn blocks_content(&self, content: &str) -> bool {
let lower_content = content.to_lowercase();
for directive in &self.directives {
let object = if let Some(rest) = directive.strip_prefix("Never ") {
rest
} else if let Some(rest) = directive.strip_prefix("Do not ") {
rest
} else {
continue; };
let words: Vec<&str> = object.split_whitespace().collect();
if !words.is_empty()
&& words
.iter()
.all(|w| lower_content.contains(&w.to_lowercase()))
{
return true;
}
}
false
}
pub fn disposition_score_delta(&self, content: &str) -> f32 {
if self.disposition.is_empty() {
return 0.0;
}
let lower = content.to_lowercase();
let mut delta: f32 = 0.0;
for trait_ in &self.disposition {
delta += match trait_ {
DispositionTrait::Analytical => {
let has_numbers = lower.chars().any(|c| c.is_ascii_digit());
let has_code = lower.contains("```") || lower.contains(" ");
let has_bullets = lower.contains("- ") || lower.contains("* ");
if has_numbers || has_code || has_bullets {
0.05
} else {
0.0
}
}
DispositionTrait::Concise => {
if content.len() > 500 { -0.05 } else { 0.0 }
}
DispositionTrait::Cautious => {
let hedges = ["might", "could", "consider", "perhaps", "possibly", "maybe"];
if hedges.iter().any(|h| lower.contains(h)) {
0.05
} else {
0.0
}
}
DispositionTrait::Creative => {
let creative = [
"idea",
"what if",
"novel",
"alternative",
"propose",
"imagine",
];
if creative.iter().any(|c| lower.contains(c)) {
0.05
} else {
0.0
}
}
DispositionTrait::Systematic => {
let sequential = ["first", "then", "finally", "step ", "1.", "2.", "3."];
if sequential.iter().any(|s| lower.contains(s)) {
0.05
} else {
0.0
}
}
};
}
delta.clamp(-0.1, 0.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_is_noop() {
assert!(MemoryBankConfig::default().is_noop());
assert!(MemoryBankConfig::new().is_noop());
}
#[test]
fn test_builder_chain() {
let cfg = MemoryBankConfig::new()
.with_mission("Security assistant")
.with_directive("Never store PII")
.with_disposition(DispositionTrait::Analytical);
assert!(!cfg.is_noop());
assert_eq!(cfg.mission.as_deref(), Some("Security assistant"));
assert_eq!(cfg.directives.len(), 1);
assert_eq!(cfg.disposition.len(), 1);
}
#[test]
fn test_mission_tag() {
let cfg = MemoryBankConfig::new().with_mission("Security Assistant");
assert_eq!(cfg.mission_tag(), Some("mission:security_assistant".into()));
assert!(MemoryBankConfig::new().mission_tag().is_none());
}
#[test]
fn test_blocks_content_never() {
let cfg = MemoryBankConfig::new().with_directive("Never store PII");
assert!(cfg.blocks_content("we should store user PII here"));
assert!(!cfg.blocks_content("authentication token handling"));
}
#[test]
fn test_blocks_content_do_not() {
let cfg = MemoryBankConfig::new().with_directive("Do not log passwords");
assert!(cfg.blocks_content("log passwords to the debug output"));
assert!(!cfg.blocks_content("log request headers"));
}
#[test]
fn test_blocks_content_non_blocking_directive() {
let cfg = MemoryBankConfig::new().with_directive("Prefer Rust over Python");
assert!(!cfg.blocks_content("Prefer Rust over Python everywhere"));
}
#[test]
fn test_disposition_concise_penalty() {
let cfg = MemoryBankConfig::new().with_disposition(DispositionTrait::Concise);
let long_content = "x".repeat(501);
let short_content = "short";
assert!(cfg.disposition_score_delta(&long_content) < 0.0);
assert_eq!(cfg.disposition_score_delta(short_content), 0.0);
}
#[test]
fn test_disposition_analytical_boost() {
let cfg = MemoryBankConfig::new().with_disposition(DispositionTrait::Analytical);
assert!(cfg.disposition_score_delta("Step 1. Use 42 requests") > 0.0);
assert_eq!(cfg.disposition_score_delta("casual chat"), 0.0);
}
#[test]
fn test_disposition_delta_clamp() {
let cfg = MemoryBankConfig::new()
.with_disposition(DispositionTrait::Analytical)
.with_disposition(DispositionTrait::Cautious)
.with_disposition(DispositionTrait::Creative)
.with_disposition(DispositionTrait::Systematic)
.with_disposition(DispositionTrait::Concise);
let content = "first idea: might use 42 steps - consider alternatives";
let delta = cfg.disposition_score_delta(content);
assert!((-0.1..=0.1).contains(&delta));
}
}