use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmpOp {
Remember,
Recall,
Forget,
Merge,
Expire,
}
impl AmpOp {
pub fn as_str(self) -> &'static str {
match self {
AmpOp::Remember => "remember",
AmpOp::Recall => "recall",
AmpOp::Forget => "forget",
AmpOp::Merge => "merge",
AmpOp::Expire => "expire",
}
}
pub const ALL: [AmpOp; 5] = [
AmpOp::Remember,
AmpOp::Recall,
AmpOp::Forget,
AmpOp::Merge,
AmpOp::Expire,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmpMemoryType {
Episodic,
Semantic,
Procedural,
Working,
}
impl AmpMemoryType {
pub fn as_str(self) -> &'static str {
match self {
AmpMemoryType::Episodic => "episodic",
AmpMemoryType::Semantic => "semantic",
AmpMemoryType::Procedural => "procedural",
AmpMemoryType::Working => "working",
}
}
pub fn is_long_term(self) -> bool {
matches!(self, AmpMemoryType::Semantic | AmpMemoryType::Procedural)
}
pub const ALL: [AmpMemoryType; 4] = [
AmpMemoryType::Episodic,
AmpMemoryType::Semantic,
AmpMemoryType::Procedural,
AmpMemoryType::Working,
];
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmpEnvelope {
#[serde(default = "default_amp_version")]
pub amp_version: String,
pub op: AmpOp,
pub memory_type: AmpMemoryType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub memory_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_k: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
fn default_amp_version() -> String {
"amp/1".to_string()
}
impl AmpEnvelope {
pub fn new(op: AmpOp, memory_type: AmpMemoryType) -> Self {
Self {
amp_version: default_amp_version(),
op,
memory_type,
agent_id: None,
content: None,
query: None,
memory_ids: Vec::new(),
top_k: None,
tags: Vec::new(),
ttl_seconds: None,
metadata: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AmpHit {
pub id: String,
pub content: String,
pub memory_type: AmpMemoryType,
pub score: f32,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmpResult {
pub op: AmpOp,
pub ok: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hits: Vec<AmpHit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved: Option<bool>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub detail: String,
}
impl AmpResult {
pub fn ok(op: AmpOp) -> Self {
Self {
op,
ok: true,
ids: Vec::new(),
hits: Vec::new(),
approved: None,
detail: String::new(),
}
}
pub fn rejected(op: AmpOp, detail: impl Into<String>) -> Self {
Self {
op,
ok: false,
ids: Vec::new(),
hits: Vec::new(),
approved: Some(false),
detail: detail.into(),
}
}
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mnemo.dev/schemas/amp/1/envelope.json",
"title": "AMP memory envelope",
"description": "AMP / memorywire request envelope: 5 operations over 4 memory types.",
"type": "object",
"required": ["op", "memory_type"],
"additionalProperties": false,
"properties": {
"amp_version": { "type": "string", "default": "amp/1" },
"op": {
"type": "string",
"enum": ["remember", "recall", "forget", "merge", "expire"]
},
"memory_type": {
"type": "string",
"enum": ["episodic", "semantic", "procedural", "working"]
},
"agent_id": { "type": ["string", "null"] },
"content": { "type": ["string", "null"] },
"query": { "type": ["string", "null"] },
"memory_ids": {
"type": "array",
"items": { "type": "string", "format": "uuid" }
},
"top_k": { "type": ["integer", "null"], "minimum": 1 },
"tags": { "type": "array", "items": { "type": "string" } },
"ttl_seconds": { "type": ["integer", "null"], "minimum": 0 },
"metadata": { "type": ["object", "null"] }
},
"allOf": [
{
"if": { "properties": { "op": { "const": "remember" } } },
"then": { "required": ["content"] }
},
{
"if": { "properties": { "op": { "const": "recall" } } },
"then": { "required": ["query"] }
},
{
"if": { "properties": { "op": { "const": "forget" } } },
"then": { "required": ["memory_ids"] }
},
{
"if": { "properties": { "op": { "const": "merge" } } },
"then": { "required": ["memory_ids"] }
},
{
"if": { "properties": { "op": { "const": "expire" } } },
"then": { "required": ["memory_ids"] }
}
]
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_and_type_axes_are_complete() {
assert_eq!(AmpOp::ALL.len(), 5);
assert_eq!(AmpMemoryType::ALL.len(), 4);
assert_eq!(AmpOp::ALL.len() * AmpMemoryType::ALL.len(), 20);
}
#[test]
fn envelope_round_trips_through_json() {
let mut env = AmpEnvelope::new(AmpOp::Remember, AmpMemoryType::Semantic);
env.content = Some("the capital of France is Paris".into());
env.tags = vec!["geo".into()];
let s = serde_json::to_string(&env).unwrap();
let back: AmpEnvelope = serde_json::from_str(&s).unwrap();
assert_eq!(back.op, AmpOp::Remember);
assert_eq!(back.memory_type, AmpMemoryType::Semantic);
assert_eq!(
back.content.as_deref(),
Some("the capital of France is Paris")
);
assert_eq!(back.amp_version, "amp/1");
}
#[test]
fn schema_is_2020_12_and_pins_the_surface() {
let s = schema();
assert_eq!(s["$schema"], "https://json-schema.org/draft/2020-12/schema");
let ops = s["properties"]["op"]["enum"].as_array().unwrap();
assert_eq!(ops.len(), 5);
let types = s["properties"]["memory_type"]["enum"].as_array().unwrap();
assert_eq!(types.len(), 4);
}
#[test]
fn long_term_classification() {
assert!(AmpMemoryType::Semantic.is_long_term());
assert!(AmpMemoryType::Procedural.is_long_term());
assert!(!AmpMemoryType::Episodic.is_long_term());
assert!(!AmpMemoryType::Working.is_long_term());
}
}