use claw10_domain::{Memory, MemoryId, MemoryStatus};
#[derive(Debug, Clone, PartialEq)]
pub enum AdmissionDecision {
Continue,
Activate,
Reject { reason: String },
}
#[derive(Debug, Clone)]
pub struct AdmissionConfig {
pub min_confidence: f64,
pub injection_keywords: Vec<String>,
pub allow_duplicates: bool,
}
impl Default for AdmissionConfig {
fn default() -> Self {
Self {
min_confidence: 0.6,
injection_keywords: vec![
"ignore previous".into(),
"disregard".into(),
"forget all".into(),
"system prompt".into(),
"jailbreak".into(),
"</system>".into(),
"<|im_start|>".into(),
],
allow_duplicates: false,
}
}
}
#[derive(Debug)]
pub enum AdmissionResult {
Activated,
Rejected { reason: String },
}
pub struct AdmissionPipeline {
config: AdmissionConfig,
}
impl AdmissionPipeline {
#[must_use]
pub fn new(config: AdmissionConfig) -> Self {
Self { config }
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(AdmissionConfig::default())
}
pub fn evaluate(&self, candidate: &Memory, existing: &[Memory]) -> AdmissionResult {
if candidate.status != MemoryStatus::Candidate
&& candidate.status != MemoryStatus::Scanning
{
return AdmissionResult::Rejected {
reason: format!(
"status tidak valid untuk admission: {:?}",
candidate.status
),
};
}
if let AdmissionDecision::Reject { reason } = self.scan_injection(&candidate.content) {
return AdmissionResult::Rejected { reason };
}
if let AdmissionDecision::Reject { reason } =
self.check_confidence(candidate.confidence)
{
return AdmissionResult::Rejected { reason };
}
if let AdmissionDecision::Reject { reason } =
self.check_duplicate(candidate, existing)
{
return AdmissionResult::Rejected { reason };
}
if let AdmissionDecision::Reject { reason } = self.check_source(candidate) {
return AdmissionResult::Rejected { reason };
}
if let AdmissionDecision::Reject { reason } = self.check_classification(candidate) {
return AdmissionResult::Rejected { reason };
}
AdmissionResult::Activated
}
fn scan_injection(&self, content: &str) -> AdmissionDecision {
let lower = content.to_lowercase();
for keyword in &self.config.injection_keywords {
if lower.contains(keyword.as_str()) {
return AdmissionDecision::Reject {
reason: format!("injection pattern terdeteksi: '{keyword}'"),
};
}
}
AdmissionDecision::Continue
}
fn check_confidence(&self, confidence: f64) -> AdmissionDecision {
if confidence < self.config.min_confidence {
AdmissionDecision::Reject {
reason: format!(
"confidence {:.2} di bawah minimum {:.2}",
confidence, self.config.min_confidence
),
}
} else {
AdmissionDecision::Continue
}
}
fn check_duplicate(&self, candidate: &Memory, existing: &[Memory]) -> AdmissionDecision {
if self.config.allow_duplicates {
return AdmissionDecision::Continue;
}
for mem in existing {
if mem.id == candidate.id {
continue; }
if mem.scope == candidate.scope
&& mem.content.trim().to_lowercase()
== candidate.content.trim().to_lowercase()
&& mem.status == MemoryStatus::Active
{
return AdmissionDecision::Reject {
reason: format!(
"duplikat dengan memory aktif {} dalam scope '{}'",
mem.id.0, candidate.scope
),
};
}
}
AdmissionDecision::Continue
}
fn check_source(&self, candidate: &Memory) -> AdmissionDecision {
if candidate.source.agent_id.0.is_nil() {
return AdmissionDecision::Reject {
reason: "source agent_id tidak valid (nil UUID)".into(),
};
}
AdmissionDecision::Continue
}
fn check_classification(&self, candidate: &Memory) -> AdmissionDecision {
if candidate.classification.trim().is_empty() {
return AdmissionDecision::Reject {
reason: "classification tidak boleh kosong".into(),
};
}
AdmissionDecision::Continue
}
}
pub fn rejection_reason_for(id: &MemoryId, reason: &str) -> String {
format!("memory {} ditolak: {}", id.0, reason)
}
#[cfg(test)]
#[path = "admission_test.rs"]
mod tests;