malvin 0.2.5

Non-interactive research and coding agent
use pi::model::AssistantMessageEvent;
use pi::sdk::AgentEvent;

use crate::bridge_protocol::BridgeEvent;

use super::map_agent_event_end::map_agent_end;
use super::map_event_summary::tool_summary_from_pi;

#[must_use]
pub(crate) fn map_pi_agent_event(event: &AgentEvent) -> Vec<BridgeEvent> {
    match event {
        AgentEvent::MessageUpdate {
            assistant_message_event,
            ..
        } => map_assistant_message_event(assistant_message_event),
        AgentEvent::ToolExecutionStart {
            tool_call_id,
            tool_name,
            args,
        } => vec![tool_call(tool_call_id, tool_name, args, "start")],
        AgentEvent::ToolExecutionUpdate {
            tool_call_id,
            tool_name,
            args,
            ..
        } => vec![tool_call(tool_call_id, tool_name, args, "update")],
        AgentEvent::ToolExecutionEnd {
            tool_call_id,
            tool_name,
            is_error,
            ..
        } => vec![tool_call(
            tool_call_id,
            tool_name,
            &serde_json::Value::Null,
            if *is_error { "error" } else { "complete" },
        )],
        AgentEvent::AgentEnd {
            messages, error, ..
        } => vec![map_agent_end(messages, error.as_deref())],
        AgentEvent::ExtensionError { error, .. } => vec![BridgeEvent::Fatal {
            message: format!("pi extension event is unsupported: {error}"),
            retryable: Some(false),
        }],
        _ => Vec::new(),
    }
}

fn map_assistant_message_event(event: &AssistantMessageEvent) -> Vec<BridgeEvent> {
    match event {
        AssistantMessageEvent::TextDelta { delta, .. } if !delta.is_empty() => {
            vec![BridgeEvent::Assistant {
                text: delta.clone(),
            }]
        }
        AssistantMessageEvent::ThinkingDelta { delta, .. } if !delta.is_empty() => {
            vec![BridgeEvent::Thinking {
                text: delta.clone(),
            }]
        }
        _ => Vec::new(),
    }
}

fn tool_call(
    tool_call_id: &str,
    tool_name: &str,
    args: &serde_json::Value,
    phase: &str,
) -> BridgeEvent {
    let summary = tool_summary_from_pi(Some(tool_name), Some(args));
    BridgeEvent::ToolCall {
        phase: phase.into(),
        name: Some(tool_name.to_string()),
        summary,
        tool_call_id: Some(tool_call_id.to_string()),
    }
}