weavegraph 0.7.0

Graph-driven, concurrent agent workflow framework with versioned state, deterministic barrier merges, and rich diagnostics.
Documentation
//! [`From`] conversions between weavegraph [`Message`] and Rig's [`RigMessage`].
use crate::message::{Message, Role};
use rig::completion::message::{
    AssistantContent, Message as RigMessage, ToolResultContent, UserContent,
};

impl From<Message> for RigMessage {
    fn from(msg: Message) -> Self {
        match msg.role {
            Role::User => RigMessage::user(msg.content),
            Role::Assistant => RigMessage::assistant(msg.content),
            // Rig history is user/assistant only; fold remaining roles into user.
            Role::System | Role::Tool | Role::Custom(_) => RigMessage::user(msg.content),
        }
    }
}

impl From<RigMessage> for Message {
    fn from(msg: RigMessage) -> Self {
        match msg {
            RigMessage::User { content } => Message::with_role(
                Role::User,
                &content.iter().find_map(user_text).unwrap_or_default(),
            ),
            RigMessage::Assistant { content, .. } => Message::with_role(
                Role::Assistant,
                &content.iter().find_map(assistant_text).unwrap_or_default(),
            ),
        }
    }
}

fn user_text(content: &UserContent) -> Option<String> {
    match content {
        UserContent::Text(t) => Some(t.text.clone()),
        UserContent::ToolResult(r) => r.content.iter().find_map(|chunk| match chunk {
            ToolResultContent::Text(t) => Some(t.text.clone()),
            ToolResultContent::Image(_) => None,
        }),
        UserContent::Image(_)
        | UserContent::Audio(_)
        | UserContent::Video(_)
        | UserContent::Document(_) => None,
    }
}

fn assistant_text(content: &AssistantContent) -> Option<String> {
    match content {
        AssistantContent::Text(t) => Some(t.text.clone()),
        AssistantContent::Reasoning(r) => r.reasoning.first().cloned(),
        AssistantContent::ToolCall(c) => Some(format!("[tool_call:{}]", c.function.name)),
        AssistantContent::Image(_) => None,
    }
}

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

    fn first_user_text(msg: RigMessage) -> Option<String> {
        match msg {
            RigMessage::User { content } => content.iter().find_map(user_text),
            RigMessage::Assistant { .. } => None,
        }
    }

    #[test]
    fn maps_weavegraph_roles_to_rig_messages() {
        let user = RigMessage::from(Message::user("u"));
        let assistant = RigMessage::from(Message::assistant("a"));
        let system = RigMessage::from(Message::system("s"));
        let tool = RigMessage::from(Message::tool("t"));
        let custom = RigMessage::from(Message::with_role(Role::Custom("worker".into()), "c"));

        assert_eq!(first_user_text(user), Some("u".to_string()));
        assert_eq!(first_user_text(system), Some("s".to_string()));
        assert_eq!(first_user_text(tool), Some("t".to_string()));
        assert_eq!(first_user_text(custom), Some("c".to_string()));

        match assistant {
            RigMessage::Assistant { content, .. } => {
                assert_eq!(
                    content.iter().find_map(assistant_text),
                    Some("a".to_string())
                );
            }
            RigMessage::User { .. } => panic!("assistant role should map to rig assistant"),
        }
    }

    #[test]
    fn maps_rig_messages_to_weavegraph_messages() {
        let user: Message = RigMessage::user("hello").into();
        assert_eq!(user.role, Role::User);
        assert_eq!(user.content, "hello");

        let assistant: Message = RigMessage::assistant("world").into();
        assert_eq!(assistant.role, Role::Assistant);
        assert_eq!(assistant.content, "world");
    }

    #[test]
    fn preserves_text_from_rig_tool_result_user_messages() {
        let tool_result: Message = RigMessage::tool_result("tool-1", "ok").into();
        assert_eq!(tool_result.role, Role::User);
        assert_eq!(tool_result.content, "ok");
    }
}