1use crate::wire::AmpMemoryType;
16
17#[derive(Debug, Clone, PartialEq)]
23pub struct WriteDiff {
24 pub agent_id: String,
25 pub memory_type: AmpMemoryType,
26 pub before: Option<String>,
28 pub after: String,
30 pub tags: Vec<String>,
31}
32
33impl WriteDiff {
34 pub fn render(&self) -> String {
38 match &self.before {
39 Some(b) => format!(
40 "[{}] tags={:?}\n- {}\n+ {}",
41 self.memory_type.as_str(),
42 self.tags,
43 b,
44 self.after
45 ),
46 None => format!(
47 "[{}] tags={:?}\n+ {}",
48 self.memory_type.as_str(),
49 self.tags,
50 self.after
51 ),
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum Approval {
59 Approve,
60 Reject(String),
61}
62
63impl Approval {
64 pub fn is_approved(&self) -> bool {
65 matches!(self, Approval::Approve)
66 }
67}
68
69pub trait ApprovalHook: Send + Sync {
77 fn review(&self, diff: &WriteDiff) -> Approval;
78 fn name(&self) -> &str;
79}
80
81#[derive(Debug, Clone, Default)]
84pub struct AutoApprove;
85
86impl ApprovalHook for AutoApprove {
87 fn review(&self, _diff: &WriteDiff) -> Approval {
88 Approval::Approve
89 }
90 fn name(&self) -> &str {
91 "auto_approve"
92 }
93}
94
95pub struct ClosureApprove {
99 f: Box<dyn Fn(&WriteDiff) -> Approval + Send + Sync>,
100}
101
102impl ClosureApprove {
103 pub fn new<F>(f: F) -> Self
104 where
105 F: Fn(&WriteDiff) -> Approval + Send + Sync + 'static,
106 {
107 Self { f: Box::new(f) }
108 }
109}
110
111impl ApprovalHook for ClosureApprove {
112 fn review(&self, diff: &WriteDiff) -> Approval {
113 (self.f)(diff)
114 }
115 fn name(&self) -> &str {
116 "closure_approve"
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn diff() -> WriteDiff {
125 WriteDiff {
126 agent_id: "a".into(),
127 memory_type: AmpMemoryType::Semantic,
128 before: None,
129 after: "Paris is the capital of France".into(),
130 tags: vec!["geo".into()],
131 }
132 }
133
134 #[test]
135 fn auto_approve_always_approves() {
136 assert_eq!(AutoApprove.review(&diff()), Approval::Approve);
137 }
138
139 #[test]
140 fn closure_hook_is_honoured() {
141 let hook = ClosureApprove::new(|d| {
142 if d.after.contains("France") {
143 Approval::Approve
144 } else {
145 Approval::Reject("off-topic".into())
146 }
147 });
148 assert!(hook.review(&diff()).is_approved());
149
150 let mut other = diff();
151 other.after = "unrelated".into();
152 assert_eq!(hook.review(&other), Approval::Reject("off-topic".into()));
153 }
154
155 #[test]
156 fn diff_render_is_deterministic() {
157 let d = diff();
158 assert_eq!(d.render(), d.render());
159 assert!(d.render().contains("+ Paris is the capital of France"));
160 }
161}