stasis-rs 0.9.3

Durable AI orchestration framework with runtime jobs, lineage, and memory integration
use crate::ports::outbound::memory::memory_models::{MemoryNode, MemoryRecallResponse};

pub fn format_memory_recall_context(nodes: &[MemoryNode]) -> Option<String> {
    if nodes.is_empty() {
        return None;
    }

    let sections: Vec<String> = nodes
        .iter()
        .enumerate()
        .map(|(index, node)| {
            let header = match node.context_summary.as_deref() {
                Some(summary) if !summary.trim().is_empty() => {
                    format!(
                        "--- memory node {} (tier={}, session={}, summary={}) ---",
                        index + 1,
                        node.tier,
                        node.session_id,
                        summary
                    )
                }
                _ => format!(
                    "--- memory node {} (tier={}, session={}) ---",
                    index + 1,
                    node.tier,
                    node.session_id
                ),
            };
            format!("{header}\n{}", node.raw.trim())
        })
        .collect();

    Some(format!("Recalled memory context:\n\n{}", sections.join("\n\n")))
}

pub fn prepend_memory_recall_context(user_prompt: &str, recall: &MemoryRecallResponse) -> String {
    let Some(context) = format_memory_recall_context(&recall.nodes) else {
        return user_prompt.to_string();
    };

    format!("{context}\n\nUser prompt:\n{user_prompt}")
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    fn sample_node(raw: &str) -> MemoryNode {
        MemoryNode {
            raw: raw.to_string(),
            session_id: "session-a".to_string(),
            tier: "raw".to_string(),
            sync_key: "sync-a".to_string(),
            timestamp: Utc::now(),
            updated_at: Utc::now(),
            ..Default::default()
        }
    }

    #[test]
    fn prepend_memory_recall_context_injects_raw_nodes() {
        let recall = MemoryRecallResponse {
            retrieved: 1,
            nodes: vec![sample_node("◈⟨ prior context ⟩")],
            node_sync_keys: vec!["sync-a".to_string()],
            ..Default::default()
        };

        let prompt = prepend_memory_recall_context("summarize rust trends", &recall);
        assert!(prompt.contains("Recalled memory context:"));
        assert!(prompt.contains("◈⟨ prior context ⟩"));
        assert!(prompt.contains("User prompt:\nsummarize rust trends"));
    }

    #[test]
    fn prepend_memory_recall_context_leaves_prompt_unchanged_when_empty() {
        let recall = MemoryRecallResponse::default();
        let prompt = prepend_memory_recall_context("summarize rust trends", &recall);
        assert_eq!(prompt, "summarize rust trends");
    }
}