pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Built-in reducers for channel merge operations.
//!
//! Reducers define how state field updates are combined. The primary reducer
//! is `add_messages()` which handles message list merging with deduplication
//! and the REMOVE_ALL_MESSAGES sentinel.

use crate::message::Message;

// Re-use the sentinel from message module
pub use crate::message::REMOVE_ALL_MESSAGES;

/// Merge incoming messages into an existing message list.
///
/// Behavior:
/// - If any update message has `id == REMOVE_ALL_MESSAGES`, the existing list is cleared first
/// - Messages with an `id` matching an existing message replace that message (update in place)
/// - Messages with no matching `id` are appended
///
/// This is the standard reducer for the `messages` field in any agent state.
///
/// # Examples
///
/// ```
/// use pe_core::reducers::add_messages;
/// use pe_core::message::Message;
///
/// let mut existing = vec![Message::human("hello")];
/// let updates = vec![Message::ai("hi back")];
/// add_messages(&mut existing, updates);
/// assert_eq!(existing.len(), 2);
/// ```
pub fn add_messages(existing: &mut Vec<Message>, updates: Vec<Message>) {
    // Check for REMOVE_ALL sentinel
    let has_remove = updates.iter().any(|m| m.id() == Some(REMOVE_ALL_MESSAGES));

    if has_remove {
        existing.clear();
        // Add all non-sentinel messages
        for msg in updates {
            if msg.id() != Some(REMOVE_ALL_MESSAGES) {
                existing.push(msg);
            }
        }
        return;
    }

    // Merge: update in place if id matches, otherwise append
    for msg in updates {
        if let Some(msg_id) = msg.id() {
            if let Some(pos) = existing.iter().position(|m| m.id() == Some(msg_id)) {
                existing[pos] = msg;
                continue;
            }
        }
        existing.push(msg);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::{HumanMessage, MessageContent, SystemMessage};

    fn make_msg(id: &str, text: &str) -> Message {
        Message::Human(HumanMessage {
            content: MessageContent::Text(text.to_string()),
            id: Some(id.to_string()),
            name: None,
        })
    }

    fn make_sentinel() -> Message {
        Message::System(SystemMessage {
            content: String::new(),
            id: Some(REMOVE_ALL_MESSAGES.to_string()),
        })
    }

    #[test]
    fn test_append_new_messages() {
        let mut existing = vec![make_msg("m1", "hello")];
        let updates = vec![make_msg("m2", "world")];
        add_messages(&mut existing, updates);

        assert_eq!(existing.len(), 2);
        assert_eq!(existing[1].id(), Some("m2"));
    }

    #[test]
    fn test_update_existing_by_id() {
        let mut existing = vec![make_msg("m1", "original")];
        let updates = vec![make_msg("m1", "updated")];
        add_messages(&mut existing, updates);

        assert_eq!(existing.len(), 1);
        if let Message::Human(ref h) = existing[0] {
            match &h.content {
                MessageContent::Text(t) => assert_eq!(t, "updated"),
                _ => panic!("expected text content"),
            }
        } else {
            panic!("expected Human message");
        }
    }

    #[test]
    fn test_remove_all_sentinel() {
        let mut existing = vec![make_msg("m1", "a"), make_msg("m2", "b")];
        let updates = vec![make_sentinel(), make_msg("m3", "fresh")];
        add_messages(&mut existing, updates);

        assert_eq!(existing.len(), 1);
        assert_eq!(existing[0].id(), Some("m3"));
    }

    #[test]
    fn test_append_messages_without_id() {
        let mut existing = vec![make_msg("m1", "hello")];
        let updates = vec![Message::human("anonymous")]; // no id
        add_messages(&mut existing, updates);

        assert_eq!(existing.len(), 2);
        assert!(existing[1].id().is_none());
    }

    #[test]
    fn test_mixed_update_and_append() {
        let mut existing = vec![make_msg("m1", "first"), make_msg("m2", "second")];
        let updates = vec![make_msg("m1", "updated first"), make_msg("m3", "third")];
        add_messages(&mut existing, updates);

        assert_eq!(existing.len(), 3);
        if let Message::Human(ref h) = existing[0] {
            match &h.content {
                MessageContent::Text(t) => assert_eq!(t, "updated first"),
                _ => panic!("expected text"),
            }
        }
        assert_eq!(existing[2].id(), Some("m3"));
    }

    #[test]
    fn test_empty_updates() {
        let mut existing = vec![make_msg("m1", "hello")];
        add_messages(&mut existing, vec![]);
        assert_eq!(existing.len(), 1);
    }

    #[test]
    fn test_remove_all_with_empty_list() {
        let mut existing = vec![];
        add_messages(&mut existing, vec![make_sentinel()]);
        assert!(existing.is_empty());
    }
}