use super::*;
use crate::identity_first::{
AgentIdentity, AgentMemoryRecallRequest, AgentMemorySelection, NewAgentMemory,
};
use crate::memory::records::ManifestTier;
const MEMORY_SUPPORTED_STORES: [&str; 5] = [
"knowledge_graph",
"vector",
"timeline",
"todo",
"top_of_mind",
];
const DEFAULT_AGENT_MEMORY_MAX_ENTRIES: usize = 8;
const MAX_AGENT_MEMORY_MAX_ENTRIES: usize = 64;
const MAX_AGENT_MEMORY_TITLE_BYTES: usize = 200;
const MAX_AGENT_MEMORY_BODY_BYTES: usize = 64 * 1024;
const MAX_AGENT_MEMORY_TAGS: usize = 32;
const MAX_AGENT_MEMORY_TAG_BYTES: usize = 64;
const MAX_AGENT_MEMORY_QUERY_TEXT_BYTES: usize = 16 * 1024;
const DEFAULT_AGENT_MEMORY_MANIFEST_K: usize = 32;
const MAX_AGENT_MEMORY_MANIFEST_K: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MemoryParamsError {
ParamsMustBeObject,
EntityRequired,
TopicRequired,
StoreMustBeString,
UnsupportedStore(String),
FactMustBeString,
MetadataMustBeJson,
ConflictMustBeBoolean,
ConflictReasonMustBeString,
EntityMustBeString,
TopicMustBeString,
QueryMustBeString,
IdentityRequired,
RealmMustBeString,
MemoryIdRequired,
TitleRequired,
BodyRequired,
TagsMustBeArray,
TagMustBeString,
TitleTooLong,
BodyTooLong,
TooManyTags,
TagTooLong,
SelectionMustBeString,
UnsupportedSelection(String),
QueryTextMustBeString,
QueryTextTooLong,
QueryTermsMustBeArray,
QueryTermMustBeString,
MaxEntriesMustBePositiveInteger,
MaxEntriesOutOfRange,
TierMustBeString,
UnsupportedTier(String),
KMustBePositiveInteger,
KOutOfRange,
KRequiresWorkingSetTier,
Index(MemoryIndexError),
}
impl MemoryParamsError {
pub(super) fn backend_message(error: &LocalJsonMemoryStoreError) -> String {
match error {
LocalJsonMemoryStoreError::InvalidConfig(reason)
| LocalJsonMemoryStoreError::Io(reason)
| LocalJsonMemoryStoreError::Serialize(reason)
| LocalJsonMemoryStoreError::InvalidStoreData(reason)
| LocalJsonMemoryStoreError::ExternalCallFailed(reason) => reason.clone(),
}
}
pub(crate) fn message(&self) -> String {
match self {
MemoryParamsError::ParamsMustBeObject => "params must be a JSON object".to_string(),
MemoryParamsError::EntityRequired => "entity must be a non-empty string".to_string(),
MemoryParamsError::TopicRequired => "topic must be a non-empty string".to_string(),
MemoryParamsError::StoreMustBeString => {
"store must be a non-empty string when provided".to_string()
}
MemoryParamsError::UnsupportedStore(store) => format!(
"store '{store}' is unsupported (allowed: knowledge_graph, vector, timeline, todo, top_of_mind)"
),
MemoryParamsError::FactMustBeString => {
"fact must be a non-empty string when provided".to_string()
}
MemoryParamsError::MetadataMustBeJson => {
"metadata must be a JSON object when provided".to_string()
}
MemoryParamsError::ConflictMustBeBoolean => {
"conflict must be a boolean when provided".to_string()
}
MemoryParamsError::ConflictReasonMustBeString => {
"conflict_reason must be a string when provided".to_string()
}
MemoryParamsError::EntityMustBeString => "entity filter must be a string".to_string(),
MemoryParamsError::TopicMustBeString => "topic filter must be a string".to_string(),
MemoryParamsError::QueryMustBeString => "query filter must be a string".to_string(),
MemoryParamsError::IdentityRequired => {
"identity must be a valid non-empty string".to_string()
}
MemoryParamsError::RealmMustBeString => {
"realm must be a non-empty string when provided".to_string()
}
MemoryParamsError::MemoryIdRequired => {
"memory_id must be a non-empty string".to_string()
}
MemoryParamsError::TitleRequired => "title must be a non-empty string".to_string(),
MemoryParamsError::BodyRequired => "body must be a non-empty string".to_string(),
MemoryParamsError::TagsMustBeArray => "tags must be an array when provided".to_string(),
MemoryParamsError::TagMustBeString => {
"tags must contain only non-empty strings".to_string()
}
MemoryParamsError::TitleTooLong => "title must be at most 200 bytes".to_string(),
MemoryParamsError::BodyTooLong => "body must be at most 65536 bytes".to_string(),
MemoryParamsError::TooManyTags => "tags must contain at most 32 entries".to_string(),
MemoryParamsError::TagTooLong => "tags must be at most 64 bytes".to_string(),
MemoryParamsError::SelectionMustBeString => {
"selection must be 'always' or 'contextual' when provided".to_string()
}
MemoryParamsError::UnsupportedSelection(selection) => {
format!("selection must be 'always' or 'contextual' (got '{selection}')")
}
MemoryParamsError::QueryTextMustBeString => {
"query_text must be a non-empty string when provided".to_string()
}
MemoryParamsError::QueryTextTooLong => {
"query_text must be at most 16384 bytes".to_string()
}
MemoryParamsError::QueryTermsMustBeArray => {
"query_terms must be an array when provided".to_string()
}
MemoryParamsError::QueryTermMustBeString => {
"query_terms must contain only non-empty strings".to_string()
}
MemoryParamsError::MaxEntriesMustBePositiveInteger => {
"max_entries must be a positive integer when provided".to_string()
}
MemoryParamsError::MaxEntriesOutOfRange => {
"max_entries must be between 1 and 64".to_string()
}
MemoryParamsError::TierMustBeString => {
"tier must be 'working_set' or 'full' when provided".to_string()
}
MemoryParamsError::UnsupportedTier(tier) => {
format!("tier must be 'working_set' or 'full' (got '{tier}')")
}
MemoryParamsError::KMustBePositiveInteger => {
"k must be a positive integer when provided".to_string()
}
MemoryParamsError::KOutOfRange => {
format!("k must be between 1 and {MAX_AGENT_MEMORY_MANIFEST_K}")
}
MemoryParamsError::KRequiresWorkingSetTier => {
"k is only valid with tier 'working_set'".to_string()
}
MemoryParamsError::Index(MemoryIndexError::EntityRequired) => {
"entity must be a non-empty string".to_string()
}
MemoryParamsError::Index(MemoryIndexError::TopicRequired) => {
"topic must be a non-empty string".to_string()
}
MemoryParamsError::Index(MemoryIndexError::UnsupportedStore(store)) => format!(
"store '{store}' is unsupported (allowed: knowledge_graph, vector, timeline, todo, top_of_mind)"
),
MemoryParamsError::Index(MemoryIndexError::FactRequiredWhenConflictUnset) => {
"fact is required unless conflict=true".to_string()
}
MemoryParamsError::Index(MemoryIndexError::BackendPersistFailed(error)) => {
format!(
"memory backend persistence failed: {}",
Self::backend_message(error)
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentMemoryRememberRpcRequest {
pub(crate) identity: AgentIdentity,
pub(crate) realm: String,
pub(crate) memory: NewAgentMemory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentMemoryForgetRpcRequest {
pub(crate) identity: AgentIdentity,
pub(crate) realm: String,
pub(crate) memory_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentMemoryRecallRpcRequest {
pub(crate) request: AgentMemoryRecallRequest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentMemoryUpdateRpcRequest {
pub(crate) identity: AgentIdentity,
pub(crate) realm: String,
pub(crate) memory_id: String,
pub(crate) memory: NewAgentMemory,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AgentMemoryManifestRpcRequest {
pub(crate) identity: AgentIdentity,
pub(crate) realm: String,
pub(crate) tier: ManifestTier,
}
pub(super) fn parse_memory_stores_params(params: &Value) -> Result<(), MemoryParamsError> {
if params.is_null() || params.is_object() {
return Ok(());
}
Err(MemoryParamsError::ParamsMustBeObject)
}
pub(crate) fn parse_agent_memory_remember_params(
params: &Value,
) -> Result<AgentMemoryRememberRpcRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let identity = parse_agent_memory_identity(object)?;
let realm = parse_agent_memory_realm(object)?;
let title = object
.get("title")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::TitleRequired)?
.to_string();
if title.len() > MAX_AGENT_MEMORY_TITLE_BYTES {
return Err(MemoryParamsError::TitleTooLong);
}
let body = object
.get("body")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::BodyRequired)?
.to_string();
if body.len() > MAX_AGENT_MEMORY_BODY_BYTES {
return Err(MemoryParamsError::BodyTooLong);
}
let tags = match object.get("tags") {
None => Vec::new(),
Some(value) => {
let tags = value.as_array().ok_or(MemoryParamsError::TagsMustBeArray)?;
if tags.len() > MAX_AGENT_MEMORY_TAGS {
return Err(MemoryParamsError::TooManyTags);
}
tags.iter()
.map(|tag| {
let tag = tag
.as_str()
.ok_or(MemoryParamsError::TagMustBeString)?
.trim();
if tag.is_empty() {
return Err(MemoryParamsError::TagMustBeString);
}
if tag.len() > MAX_AGENT_MEMORY_TAG_BYTES {
return Err(MemoryParamsError::TagTooLong);
}
Ok(tag.to_string())
})
.collect::<Result<Vec<_>, _>>()?
}
};
Ok(AgentMemoryRememberRpcRequest {
identity,
realm,
memory: NewAgentMemory { title, body, tags },
})
}
pub(crate) fn parse_agent_memory_update_params(
params: &Value,
) -> Result<AgentMemoryUpdateRpcRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let identity = parse_agent_memory_identity(object)?;
let realm = parse_agent_memory_realm(object)?;
let memory_id = object
.get("memory_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::MemoryIdRequired)?
.to_string();
let remember = parse_agent_memory_remember_params(params)?;
Ok(AgentMemoryUpdateRpcRequest {
identity,
realm,
memory_id,
memory: remember.memory,
})
}
pub(crate) fn parse_agent_memory_manifest_params(
params: &Value,
) -> Result<AgentMemoryManifestRpcRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let identity = parse_agent_memory_identity(object)?;
let realm = parse_agent_memory_realm(object)?;
let tier_name = match object.get("tier") {
None => "working_set".to_string(),
Some(value) => {
let tier = value
.as_str()
.ok_or(MemoryParamsError::TierMustBeString)?
.trim()
.to_ascii_lowercase();
match tier.as_str() {
"working_set" | "full" => tier,
_ => return Err(MemoryParamsError::UnsupportedTier(tier)),
}
}
};
let k = match object.get("k") {
None => None,
Some(value) => {
let k = value
.as_u64()
.filter(|k| *k > 0)
.ok_or(MemoryParamsError::KMustBePositiveInteger)?;
let k = usize::try_from(k).map_err(|_| MemoryParamsError::KOutOfRange)?;
if k > MAX_AGENT_MEMORY_MANIFEST_K {
return Err(MemoryParamsError::KOutOfRange);
}
Some(k)
}
};
let tier = match tier_name.as_str() {
"full" => {
if k.is_some() {
return Err(MemoryParamsError::KRequiresWorkingSetTier);
}
ManifestTier::Full
}
_ => ManifestTier::WorkingSet(k.unwrap_or(DEFAULT_AGENT_MEMORY_MANIFEST_K)),
};
Ok(AgentMemoryManifestRpcRequest {
identity,
realm,
tier,
})
}
pub(crate) fn parse_agent_memory_forget_params(
params: &Value,
) -> Result<AgentMemoryForgetRpcRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let identity = parse_agent_memory_identity(object)?;
let realm = parse_agent_memory_realm(object)?;
let memory_id = object
.get("memory_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::MemoryIdRequired)?
.to_string();
Ok(AgentMemoryForgetRpcRequest {
identity,
realm,
memory_id,
})
}
pub(crate) fn parse_agent_memory_recall_params(
params: &Value,
) -> Result<AgentMemoryRecallRpcRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let identity = parse_agent_memory_identity(object)?;
let realm = parse_agent_memory_realm(object)?;
let selection = match object.get("selection") {
None => AgentMemorySelection::Contextual,
Some(value) => {
let selection = value
.as_str()
.ok_or(MemoryParamsError::SelectionMustBeString)?
.trim()
.to_ascii_lowercase();
match selection.as_str() {
"always" => AgentMemorySelection::Always,
"contextual" => AgentMemorySelection::Contextual,
_ => return Err(MemoryParamsError::UnsupportedSelection(selection)),
}
}
};
let max_entries = match object.get("max_entries") {
None => DEFAULT_AGENT_MEMORY_MAX_ENTRIES,
Some(value) => {
let entries = value
.as_u64()
.ok_or(MemoryParamsError::MaxEntriesMustBePositiveInteger)?;
if entries == 0 {
return Err(MemoryParamsError::MaxEntriesMustBePositiveInteger);
}
let entries =
usize::try_from(entries).map_err(|_| MemoryParamsError::MaxEntriesOutOfRange)?;
if entries > MAX_AGENT_MEMORY_MAX_ENTRIES {
return Err(MemoryParamsError::MaxEntriesOutOfRange);
}
entries
}
};
let query_terms = match object.get("query_terms") {
None => Vec::new(),
Some(value) => value
.as_array()
.ok_or(MemoryParamsError::QueryTermsMustBeArray)?
.iter()
.map(|term| {
let term = term
.as_str()
.ok_or(MemoryParamsError::QueryTermMustBeString)?
.trim();
if term.is_empty() {
return Err(MemoryParamsError::QueryTermMustBeString);
}
Ok(term.to_string())
})
.collect::<Result<Vec<_>, _>>()?,
};
let query_text = match object.get("query_text") {
None => None,
Some(value) => {
let query_text = value
.as_str()
.ok_or(MemoryParamsError::QueryTextMustBeString)?
.trim();
if query_text.is_empty() {
return Err(MemoryParamsError::QueryTextMustBeString);
}
if query_text.len() > MAX_AGENT_MEMORY_QUERY_TEXT_BYTES {
return Err(MemoryParamsError::QueryTextTooLong);
}
Some(query_text.to_string())
}
};
Ok(AgentMemoryRecallRpcRequest {
request: AgentMemoryRecallRequest {
identity,
realm,
query_text,
query_terms,
selection,
max_entries,
},
})
}
fn parse_agent_memory_identity(
object: &serde_json::Map<String, Value>,
) -> Result<AgentIdentity, MemoryParamsError> {
object
.get("identity")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::IdentityRequired)
.and_then(|value| {
AgentIdentity::parse(value).map_err(|_| MemoryParamsError::IdentityRequired)
})
}
fn parse_agent_memory_realm(
object: &serde_json::Map<String, Value>,
) -> Result<String, MemoryParamsError> {
match object.get("realm") {
None => Ok("default".to_string()),
Some(value) => {
let realm = value
.as_str()
.ok_or(MemoryParamsError::RealmMustBeString)?
.trim();
if realm.is_empty() {
return Err(MemoryParamsError::RealmMustBeString);
}
Ok(realm.to_string())
}
}
}
fn parse_memory_store_field(value: &Value) -> Result<String, MemoryParamsError> {
let store = value.as_str().ok_or(MemoryParamsError::StoreMustBeString)?;
let canonical = store.trim().to_ascii_lowercase();
if canonical.is_empty() {
return Err(MemoryParamsError::StoreMustBeString);
}
if MEMORY_SUPPORTED_STORES.contains(&canonical.as_str()) {
Ok(canonical)
} else {
Err(MemoryParamsError::UnsupportedStore(canonical))
}
}
pub(super) fn parse_memory_index_params(
params: &Value,
) -> Result<MemoryIndexRequest, MemoryParamsError> {
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let entity = object
.get("entity")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::EntityRequired)?;
let topic = object
.get("topic")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or(MemoryParamsError::TopicRequired)?;
let store = match object.get("store") {
None => None,
Some(value) => Some(parse_memory_store_field(value)?),
};
let fact = match object.get("fact") {
None => None,
Some(value) => Some(
value
.as_str()
.ok_or(MemoryParamsError::FactMustBeString)?
.trim()
.to_string(),
),
};
if fact.as_deref().is_some_and(str::is_empty) {
return Err(MemoryParamsError::FactMustBeString);
}
let metadata = match object.get("metadata") {
None => None,
Some(value) => {
if !value.is_object() {
return Err(MemoryParamsError::MetadataMustBeJson);
}
Some(value.clone())
}
};
let conflict = match object.get("conflict") {
None => None,
Some(value) => Some(
value
.as_bool()
.ok_or(MemoryParamsError::ConflictMustBeBoolean)?,
),
};
let conflict_reason = match object.get("conflict_reason") {
None => None,
Some(value) => Some(
value
.as_str()
.ok_or(MemoryParamsError::ConflictReasonMustBeString)?
.to_string(),
),
};
Ok(MemoryIndexRequest {
entity: entity.to_string(),
topic: topic.to_string(),
store,
fact,
metadata,
conflict,
conflict_reason,
})
}
pub(super) fn parse_memory_query_params(
params: &Value,
) -> Result<MemoryQueryRequest, MemoryParamsError> {
if params.is_null() {
return Ok(MemoryQueryRequest {
entity: None,
topic: None,
store: None,
query: None,
});
}
let object = params
.as_object()
.ok_or(MemoryParamsError::ParamsMustBeObject)?;
let entity = match object.get("entity") {
None => None,
Some(value) => Some(
value
.as_str()
.ok_or(MemoryParamsError::EntityMustBeString)?
.to_string(),
),
};
let topic = match object.get("topic") {
None => None,
Some(value) => Some(
value
.as_str()
.ok_or(MemoryParamsError::TopicMustBeString)?
.to_string(),
),
};
let store = match object.get("store") {
None => None,
Some(value) => Some(parse_memory_store_field(value)?),
};
let query = match object.get("query") {
None => None,
Some(value) => Some(
value
.as_str()
.ok_or(MemoryParamsError::QueryMustBeString)?
.to_string(),
),
};
Ok(MemoryQueryRequest {
entity,
topic,
store,
query,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::error::Error;
#[test]
fn agent_memory_recall_defaults_to_contextual_selection() -> Result<(), Box<dyn Error>> {
let parsed = parse_agent_memory_recall_params(&json!({
"identity": "identity:luka",
"query_text": "Where is my passport?",
"query_terms": ["passport"]
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.request.selection, AgentMemorySelection::Contextual);
assert_eq!(parsed.request.realm, "default");
assert_eq!(
parsed.request.query_text,
Some("Where is my passport?".to_string())
);
assert_eq!(parsed.request.query_terms, vec!["passport".to_string()]);
Ok(())
}
#[test]
fn agent_memory_recall_rejects_empty_query_text() {
let err = parse_agent_memory_recall_params(&json!({
"identity": "identity:luka",
"query_text": " "
}))
.err();
assert_eq!(err, Some(MemoryParamsError::QueryTextMustBeString));
}
#[test]
fn agent_memory_recall_preserves_explicit_always_selection() -> Result<(), Box<dyn Error>> {
let parsed = parse_agent_memory_recall_params(&json!({
"identity": "identity:luka",
"selection": "always"
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.request.selection, AgentMemorySelection::Always);
Ok(())
}
#[test]
fn memory_query_params_accept_legacy_query_string() -> Result<(), Box<dyn Error>> {
let parsed = parse_memory_query_params(&json!({
"query": "recipient consent",
"store": "todo"
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.query, Some("recipient consent".to_string()));
assert_eq!(parsed.store, Some("todo".to_string()));
assert_eq!(parsed.entity, None);
assert_eq!(parsed.topic, None);
Ok(())
}
#[test]
fn memory_query_params_reject_non_string_query() {
let err = parse_memory_query_params(&json!({ "query": 42 })).err();
assert_eq!(err, Some(MemoryParamsError::QueryMustBeString));
}
#[test]
fn agent_memory_update_parses_full_shape() -> Result<(), Box<dyn Error>> {
let parsed = parse_agent_memory_update_params(&json!({
"identity": "identity:luka",
"realm": "family",
"memory_id": "mem-1",
"title": "School pickup",
"body": "Corrected pickup time.",
"tags": ["family"]
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.identity.as_str(), "identity:luka");
assert_eq!(parsed.realm, "family");
assert_eq!(parsed.memory_id, "mem-1");
assert_eq!(parsed.memory.title, "School pickup");
assert_eq!(parsed.memory.body, "Corrected pickup time.");
assert_eq!(parsed.memory.tags, vec!["family".to_string()]);
Ok(())
}
#[test]
fn agent_memory_update_requires_memory_id_and_body() {
let missing_id = parse_agent_memory_update_params(&json!({
"identity": "identity:luka",
"title": "T",
"body": "B"
}))
.err();
assert_eq!(missing_id, Some(MemoryParamsError::MemoryIdRequired));
let missing_body = parse_agent_memory_update_params(&json!({
"identity": "identity:luka",
"memory_id": "mem-1",
"title": "T"
}))
.err();
assert_eq!(missing_body, Some(MemoryParamsError::BodyRequired));
}
#[test]
fn agent_memory_manifest_defaults_to_working_set() -> Result<(), Box<dyn Error>> {
let parsed = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka"
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.realm, "default");
assert_eq!(
parsed.tier,
ManifestTier::WorkingSet(DEFAULT_AGENT_MEMORY_MANIFEST_K)
);
let with_k = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"tier": "working_set",
"k": 4
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(with_k.tier, ManifestTier::WorkingSet(4));
let full = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"tier": "full"
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(full.tier, ManifestTier::Full);
Ok(())
}
#[test]
fn agent_memory_manifest_rejects_bad_tier_and_k() {
let bad_tier = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"tier": "vector"
}))
.err();
assert_eq!(
bad_tier,
Some(MemoryParamsError::UnsupportedTier("vector".to_string()))
);
let zero_k = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"k": 0
}))
.err();
assert_eq!(zero_k, Some(MemoryParamsError::KMustBePositiveInteger));
let huge_k = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"k": 100000
}))
.err();
assert_eq!(huge_k, Some(MemoryParamsError::KOutOfRange));
let k_with_full = parse_agent_memory_manifest_params(&json!({
"identity": "identity:luka",
"tier": "full",
"k": 8
}))
.err();
assert_eq!(
k_with_full,
Some(MemoryParamsError::KRequiresWorkingSetTier)
);
}
#[test]
fn agent_memory_forget_requires_memory_id() {
let err = parse_agent_memory_forget_params(&json!({
"identity": "identity:luka"
}))
.err();
assert_eq!(err, Some(MemoryParamsError::MemoryIdRequired));
}
#[test]
fn agent_memory_forget_parses_identity_realm_and_memory_id() -> Result<(), Box<dyn Error>> {
let parsed = parse_agent_memory_forget_params(&json!({
"identity": "identity:luka",
"realm": "family",
"memory_id": "mem-1"
}))
.map_err(|err| std::io::Error::other(err.message()))?;
assert_eq!(parsed.identity.as_str(), "identity:luka");
assert_eq!(parsed.realm, "family");
assert_eq!(parsed.memory_id, "mem-1");
Ok(())
}
}