a3s-memory 0.1.4

A3S Memory - Pluggable memory storage for AI agents
Documentation
use super::{MemoryNode, MemoryRelation, MemoryRelationKind, MemoryRepositoryError, MemoryStatus};
use std::collections::BTreeMap;

pub(crate) fn validate_graph_node(
    node: &MemoryNode,
    staged: &BTreeMap<String, MemoryNode>,
    base: Option<&BTreeMap<String, MemoryNode>>,
) -> Result<(), MemoryRepositoryError> {
    if node.status == MemoryStatus::Superseded
        && !node
            .relations
            .iter()
            .any(|relation| relation.kind == MemoryRelationKind::SupersededBy)
    {
        return Err(MemoryRepositoryError::invariant(format!(
            "superseded memory node {} requires a superseded_by relation",
            node.id
        )));
    }
    if node.status == MemoryStatus::Conflicted
        && !node
            .relations
            .iter()
            .any(|relation| relation.kind == MemoryRelationKind::ConflictsWith)
    {
        return Err(MemoryRepositoryError::invariant(format!(
            "conflicted memory node {} requires a conflicts_with relation",
            node.id
        )));
    }

    for relation in &node.relations {
        let Some(target) = find_node(staged, base, &relation.target_id) else {
            return Err(MemoryRepositoryError::RelationTargetNotFound {
                node_id: node.id.clone(),
                target_id: relation.target_id.clone(),
            });
        };
        let inverse = MemoryRelation::new(relation.kind.inverse(), &node.id);
        if !target.relations.contains(&inverse) {
            return Err(MemoryRepositoryError::invariant(format!(
                "relation {:?} from {} to {} is missing its {:?} counterpart",
                relation.kind, node.id, target.id, inverse.kind
            )));
        }
        if relation.kind == MemoryRelationKind::Supersedes
            && target.status != MemoryStatus::Superseded
        {
            return Err(MemoryRepositoryError::invariant(format!(
                "memory node {} supersedes {}, but the target is not superseded",
                node.id, target.id
            )));
        }
    }
    Ok(())
}

pub(crate) fn find_node<'a>(
    staged: &'a BTreeMap<String, MemoryNode>,
    base: Option<&'a BTreeMap<String, MemoryNode>>,
    node_id: &str,
) -> Option<&'a MemoryNode> {
    staged
        .get(node_id)
        .or_else(|| base.and_then(|nodes| nodes.get(node_id)))
}