1use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum AmpOp {
22 Remember,
23 Recall,
24 Forget,
25 Merge,
26 Expire,
27}
28
29impl AmpOp {
30 pub fn as_str(self) -> &'static str {
31 match self {
32 AmpOp::Remember => "remember",
33 AmpOp::Recall => "recall",
34 AmpOp::Forget => "forget",
35 AmpOp::Merge => "merge",
36 AmpOp::Expire => "expire",
37 }
38 }
39
40 pub const ALL: [AmpOp; 5] = [
42 AmpOp::Remember,
43 AmpOp::Recall,
44 AmpOp::Forget,
45 AmpOp::Merge,
46 AmpOp::Expire,
47 ];
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum AmpMemoryType {
55 Episodic,
56 Semantic,
57 Procedural,
58 Working,
59}
60
61impl AmpMemoryType {
62 pub fn as_str(self) -> &'static str {
63 match self {
64 AmpMemoryType::Episodic => "episodic",
65 AmpMemoryType::Semantic => "semantic",
66 AmpMemoryType::Procedural => "procedural",
67 AmpMemoryType::Working => "working",
68 }
69 }
70
71 pub fn is_long_term(self) -> bool {
75 matches!(self, AmpMemoryType::Semantic | AmpMemoryType::Procedural)
76 }
77
78 pub const ALL: [AmpMemoryType; 4] = [
80 AmpMemoryType::Episodic,
81 AmpMemoryType::Semantic,
82 AmpMemoryType::Procedural,
83 AmpMemoryType::Working,
84 ];
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct AmpEnvelope {
97 #[serde(default = "default_amp_version")]
99 pub amp_version: String,
100 pub op: AmpOp,
101 pub memory_type: AmpMemoryType,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub agent_id: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub content: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub query: Option<String>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub memory_ids: Vec<String>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub top_k: Option<usize>,
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub tags: Vec<String>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub ttl_seconds: Option<u64>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub metadata: Option<serde_json::Value>,
128}
129
130fn default_amp_version() -> String {
131 "amp/1".to_string()
132}
133
134impl AmpEnvelope {
135 pub fn new(op: AmpOp, memory_type: AmpMemoryType) -> Self {
137 Self {
138 amp_version: default_amp_version(),
139 op,
140 memory_type,
141 agent_id: None,
142 content: None,
143 query: None,
144 memory_ids: Vec::new(),
145 top_k: None,
146 tags: Vec::new(),
147 ttl_seconds: None,
148 metadata: None,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct AmpHit {
156 pub id: String,
157 pub content: String,
158 pub memory_type: AmpMemoryType,
159 pub score: f32,
160 pub tags: Vec<String>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct AmpResult {
166 pub op: AmpOp,
167 pub ok: bool,
168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub ids: Vec<String>,
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub hits: Vec<AmpHit>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub approved: Option<bool>,
179 #[serde(default, skip_serializing_if = "String::is_empty")]
181 pub detail: String,
182}
183
184impl AmpResult {
185 pub fn ok(op: AmpOp) -> Self {
186 Self {
187 op,
188 ok: true,
189 ids: Vec::new(),
190 hits: Vec::new(),
191 approved: None,
192 detail: String::new(),
193 }
194 }
195
196 pub fn rejected(op: AmpOp, detail: impl Into<String>) -> Self {
197 Self {
198 op,
199 ok: false,
200 ids: Vec::new(),
201 hits: Vec::new(),
202 approved: Some(false),
203 detail: detail.into(),
204 }
205 }
206}
207
208pub fn schema() -> serde_json::Value {
215 serde_json::json!({
216 "$schema": "https://json-schema.org/draft/2020-12/schema",
217 "$id": "https://mnemo.dev/schemas/amp/1/envelope.json",
218 "title": "AMP memory envelope",
219 "description": "AMP / memorywire request envelope: 5 operations over 4 memory types.",
220 "type": "object",
221 "required": ["op", "memory_type"],
222 "additionalProperties": false,
223 "properties": {
224 "amp_version": { "type": "string", "default": "amp/1" },
225 "op": {
226 "type": "string",
227 "enum": ["remember", "recall", "forget", "merge", "expire"]
228 },
229 "memory_type": {
230 "type": "string",
231 "enum": ["episodic", "semantic", "procedural", "working"]
232 },
233 "agent_id": { "type": ["string", "null"] },
234 "content": { "type": ["string", "null"] },
235 "query": { "type": ["string", "null"] },
236 "memory_ids": {
237 "type": "array",
238 "items": { "type": "string", "format": "uuid" }
239 },
240 "top_k": { "type": ["integer", "null"], "minimum": 1 },
241 "tags": { "type": "array", "items": { "type": "string" } },
242 "ttl_seconds": { "type": ["integer", "null"], "minimum": 0 },
243 "metadata": { "type": ["object", "null"] }
244 },
245 "allOf": [
246 {
247 "if": { "properties": { "op": { "const": "remember" } } },
248 "then": { "required": ["content"] }
249 },
250 {
251 "if": { "properties": { "op": { "const": "recall" } } },
252 "then": { "required": ["query"] }
253 },
254 {
255 "if": { "properties": { "op": { "const": "forget" } } },
256 "then": { "required": ["memory_ids"] }
257 },
258 {
259 "if": { "properties": { "op": { "const": "merge" } } },
260 "then": { "required": ["memory_ids"] }
261 },
262 {
263 "if": { "properties": { "op": { "const": "expire" } } },
264 "then": { "required": ["memory_ids"] }
265 }
266 ]
267 })
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn op_and_type_axes_are_complete() {
276 assert_eq!(AmpOp::ALL.len(), 5);
277 assert_eq!(AmpMemoryType::ALL.len(), 4);
278 assert_eq!(AmpOp::ALL.len() * AmpMemoryType::ALL.len(), 20);
280 }
281
282 #[test]
283 fn envelope_round_trips_through_json() {
284 let mut env = AmpEnvelope::new(AmpOp::Remember, AmpMemoryType::Semantic);
285 env.content = Some("the capital of France is Paris".into());
286 env.tags = vec!["geo".into()];
287 let s = serde_json::to_string(&env).unwrap();
288 let back: AmpEnvelope = serde_json::from_str(&s).unwrap();
289 assert_eq!(back.op, AmpOp::Remember);
290 assert_eq!(back.memory_type, AmpMemoryType::Semantic);
291 assert_eq!(
292 back.content.as_deref(),
293 Some("the capital of France is Paris")
294 );
295 assert_eq!(back.amp_version, "amp/1");
296 }
297
298 #[test]
299 fn schema_is_2020_12_and_pins_the_surface() {
300 let s = schema();
301 assert_eq!(s["$schema"], "https://json-schema.org/draft/2020-12/schema");
302 let ops = s["properties"]["op"]["enum"].as_array().unwrap();
303 assert_eq!(ops.len(), 5);
304 let types = s["properties"]["memory_type"]["enum"].as_array().unwrap();
305 assert_eq!(types.len(), 4);
306 }
307
308 #[test]
309 fn long_term_classification() {
310 assert!(AmpMemoryType::Semantic.is_long_term());
311 assert!(AmpMemoryType::Procedural.is_long_term());
312 assert!(!AmpMemoryType::Episodic.is_long_term());
313 assert!(!AmpMemoryType::Working.is_long_term());
314 }
315}