agent-discord-rs 0.2.7

A high-performance Discord Bot daemon supporting multiple AI agents (pi, opencode).
use crate::agent::{AgentEvent, ContentType};
use crate::composer::{Block, BlockType, EmbedComposer};
use crate::ExecStatus;

pub fn apply_agent_event(
    comp: &mut EmbedComposer,
    status: &mut ExecStatus,
    event: AgentEvent,
) -> bool {
    match event {
        AgentEvent::MessageUpdate {
            thinking,
            text,
            is_delta,
            id,
        } => {
            if is_delta {
                if !thinking.is_empty() {
                    comp.push_delta(id.clone(), BlockType::Thinking, &thinking);
                }
                if !text.is_empty() {
                    comp.push_delta(id, BlockType::Text, &text);
                }
            } else {
                if !thinking.is_empty() {
                    comp.update_block_by_id(
                        &id.clone().unwrap_or_else(|| "think".into()),
                        BlockType::Thinking,
                        thinking,
                    );
                }
                if !text.is_empty() {
                    comp.update_block_by_id(
                        &id.unwrap_or_else(|| "text".into()),
                        BlockType::Text,
                        text,
                    );
                }
            }
        }
        AgentEvent::ContentSync { items } => {
            let mapped = items
                .into_iter()
                .map(|i| match i.type_ {
                    ContentType::Thinking => Block::new(BlockType::Thinking, i.content),
                    ContentType::Text => Block::new(BlockType::Text, i.content),
                    ContentType::ToolCall(name) => {
                        Block::with_label(BlockType::ToolCall, name, i.id)
                    }
                    ContentType::ToolOutput => {
                        let mut b = Block::new(BlockType::ToolOutput, i.content);
                        b.id = i.id;
                        b
                    }
                })
                .collect();
            comp.sync_content(mapped);
        }
        AgentEvent::ToolExecutionStart { id, name } => {
            comp.set_tool_call(id, name);
        }
        AgentEvent::ToolExecutionUpdate { id, output } => {
            comp.update_block_by_id(&id, BlockType::ToolOutput, output);
        }
        AgentEvent::AgentEnd { success, error } => {
            *status = if success {
                ExecStatus::Success
            } else {
                ExecStatus::Error(error.unwrap_or_else(|| "Error".to_string()))
            };
        }
        AgentEvent::Error { message } => {
            *status = ExecStatus::Error(message);
        }
        _ => {}
    }

    *status != ExecStatus::Running
}

#[cfg(test)]
mod tests {
    use super::apply_agent_event;
    use crate::agent::{AgentEvent, ContentItem, ContentType};
    use crate::composer::{BlockType, EmbedComposer};
    use crate::ExecStatus;

    #[test]
    fn test_apply_message_update_delta_updates_blocks() {
        let mut comp = EmbedComposer::new(2000);
        let mut status = ExecStatus::Running;
        let finished = apply_agent_event(
            &mut comp,
            &mut status,
            AgentEvent::MessageUpdate {
                thinking: "t1".to_string(),
                text: "x1".to_string(),
                is_delta: true,
                id: Some("id1".to_string()),
            },
        );
        assert!(!finished);
        assert_eq!(status, ExecStatus::Running);
        assert!(comp
            .blocks
            .iter()
            .any(|b| b.block_type == BlockType::Thinking && b.content.contains("t1")));
        assert!(comp
            .blocks
            .iter()
            .any(|b| b.block_type == BlockType::Text && b.content.contains("x1")));
    }

    #[test]
    fn test_apply_content_sync_tool_and_text_blocks() {
        let mut comp = EmbedComposer::new(2000);
        let mut status = ExecStatus::Running;
        let _ = apply_agent_event(
            &mut comp,
            &mut status,
            AgentEvent::ContentSync {
                items: vec![
                    ContentItem {
                        type_: ContentType::ToolCall("tool-a".to_string()),
                        content: String::new(),
                        id: Some("t1".to_string()),
                    },
                    ContentItem {
                        type_: ContentType::Text,
                        content: "hello".to_string(),
                        id: None,
                    },
                ],
            },
        );
        assert!(comp
            .blocks
            .iter()
            .any(|b| b.block_type == BlockType::ToolCall));
        assert!(comp
            .blocks
            .iter()
            .any(|b| b.block_type == BlockType::Text && b.content == "hello"));
    }

    #[test]
    fn test_apply_agent_end_and_error_finish() {
        let mut comp = EmbedComposer::new(2000);
        let mut status = ExecStatus::Running;
        let done = apply_agent_event(
            &mut comp,
            &mut status,
            AgentEvent::AgentEnd {
                success: false,
                error: Some("boom".to_string()),
            },
        );
        assert!(done);
        assert_eq!(status, ExecStatus::Error("boom".to_string()));

        let done2 = apply_agent_event(
            &mut comp,
            &mut status,
            AgentEvent::Error {
                message: "bad".to_string(),
            },
        );
        assert!(done2);
        assert_eq!(status, ExecStatus::Error("bad".to_string()));
    }
}