gproxy-protocol 4.1.4

GPROXY v4 connection modelling, operation declarations, vendor wire types and pairwise conversion
Documentation
use super::parts::Part;
use super::*;
use std::collections::BTreeMap;

pub(super) struct Item {
    pub value: r::ResponseOutputItem,
    pub done: bool,
    pub parts: BTreeMap<(bool, i64), Part>,
    pub argument_done: bool,
    pub argument_streamed: bool,
    progress_phase: Option<u8>,
}

pub(super) fn item_id(item: &r::ResponseOutputItem) -> Option<&str> {
    match item {
        r::ResponseOutputItem::MultiAgentCall(v) => Some(&v.id),
        r::ResponseOutputItem::MultiAgentCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::AgentMessage(v) => Some(&v.id),
        r::ResponseOutputItem::Message(v) => Some(&v.id),
        r::ResponseOutputItem::FileSearchCall(v) => Some(&v.id),
        r::ResponseOutputItem::FunctionCall(v) => v.id.as_deref(),
        r::ResponseOutputItem::FunctionCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::WebSearchCall(v) => Some(&v.id),
        r::ResponseOutputItem::ComputerCall(v) => Some(&v.id),
        r::ResponseOutputItem::ComputerCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::Reasoning(v) => Some(&v.id),
        r::ResponseOutputItem::Program(v) => Some(&v.id),
        r::ResponseOutputItem::ProgramOutput(v) => Some(&v.id),
        r::ResponseOutputItem::ToolSearchCall(v) => Some(&v.id),
        r::ResponseOutputItem::ToolSearchOutput(v) => Some(&v.id),
        r::ResponseOutputItem::AdditionalTools(v) => Some(&v.id),
        r::ResponseOutputItem::ConfigurationUpdate(v) => v.id.as_ref().and_then(Option::as_deref),
        r::ResponseOutputItem::Compaction(v) => Some(&v.id),
        r::ResponseOutputItem::ImageGenerationCall(v) => Some(&v.id),
        r::ResponseOutputItem::CodeInterpreterCall(v) => Some(&v.id),
        r::ResponseOutputItem::LocalShellCall(v) => Some(&v.id),
        r::ResponseOutputItem::LocalShellCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::ShellCall(v) => Some(&v.id),
        r::ResponseOutputItem::ShellCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::ApplyPatchCall(v) => Some(&v.id),
        r::ResponseOutputItem::ApplyPatchCallOutput(v) => Some(&v.id),
        r::ResponseOutputItem::McpCall(v) => Some(&v.id),
        r::ResponseOutputItem::McpListTools(v) => Some(&v.id),
        r::ResponseOutputItem::McpApprovalRequest(v) => Some(&v.id),
        r::ResponseOutputItem::McpApprovalResponse(v) => Some(&v.id),
        r::ResponseOutputItem::CustomToolCall(v) => v.id.as_deref(),
        r::ResponseOutputItem::CustomToolCallOutput(v) => Some(&v.id),
    }
}

impl Item {
    pub fn new(mut value: r::ResponseOutputItem) -> Self {
        let mut parts = BTreeMap::new();
        match &mut value {
            r::ResponseOutputItem::Message(message) => {
                for (index, part) in std::mem::take(&mut message.content).into_iter().enumerate() {
                    let value = match part {
                        i::OutputContent::Text(value) => s::OutputContentPart::Text(value),
                        i::OutputContent::Refusal(value) => s::OutputContentPart::Refusal(value),
                    };
                    let mut part = Part::new(value, false);
                    part.seeded = true;
                    parts.insert((false, index as i64), part);
                }
            }
            r::ResponseOutputItem::Reasoning(reasoning) => {
                for (index, part) in std::mem::take(&mut reasoning.summary)
                    .into_iter()
                    .enumerate()
                {
                    let mut part = Part::new(
                        s::OutputContentPart::Reasoning(s::ReasoningText {
                            text: part.text,
                            type_: s::ReasoningTextType::ReasoningText,
                            rest: Default::default(),
                        }),
                        true,
                    );
                    part.seeded = true;
                    parts.insert((true, index as i64), part);
                }
                if let Some(content) = &mut reasoning.content {
                    for (index, part) in std::mem::take(content).into_iter().enumerate() {
                        let mut part = Part::new(
                            s::OutputContentPart::Reasoning(s::ReasoningText {
                                text: part.text,
                                type_: s::ReasoningTextType::ReasoningText,
                                rest: Default::default(),
                            }),
                            false,
                        );
                        part.seeded = true;
                        parts.insert((false, index as i64), part);
                    }
                }
            }
            _ => {}
        }
        Self {
            value,
            done: false,
            parts,
            argument_done: false,
            argument_streamed: false,
            progress_phase: None,
        }
    }
    pub fn arguments(&self) -> Result<&str, TransformError> {
        match &self.value {
            r::ResponseOutputItem::FunctionCall(v) => Ok(&v.arguments),
            r::ResponseOutputItem::CustomToolCall(v) => Ok(&v.input),
            r::ResponseOutputItem::McpCall(v) => Ok(&v.arguments),
            r::ResponseOutputItem::CodeInterpreterCall(v) => Ok(v.code.as_deref().unwrap_or("")),
            _ => Err(invalid("argument event on wrong item type")),
        }
    }
    pub fn arguments_mut(&mut self) -> Result<&mut String, TransformError> {
        match &mut self.value {
            r::ResponseOutputItem::FunctionCall(v) => Ok(&mut v.arguments),
            r::ResponseOutputItem::CustomToolCall(v) => Ok(&mut v.input),
            r::ResponseOutputItem::McpCall(v) => Ok(&mut v.arguments),
            r::ResponseOutputItem::CodeInterpreterCall(v) => Ok(v.code.get_or_insert_default()),
            _ => Err(invalid("argument event on wrong item type")),
        }
    }
    pub fn finish(&mut self, final_item: r::ResponseOutputItem) -> Result<(), TransformError> {
        if self
            .value
            .agent()
            .map_or("/root", |owner| owner.agent_name.as_str())
            != final_item
                .agent()
                .map_or("/root", |owner| owner.agent_name.as_str())
        {
            return Err(invalid("output item agent changed"));
        }
        if self.done
            || std::mem::discriminant(&self.value) != std::mem::discriminant(&final_item)
            || item_id(&self.value) != item_id(&final_item)
        {
            return Err(invalid("item identity/type changed or duplicate done"));
        }
        super::status::item(&final_item, false)?;
        self.check_progress(&final_item)?;
        if !same_links(&self.value, &final_item) {
            return Err(invalid("output item call/reference identity changed"));
        }
        match (&self.value, &final_item) {
            (r::ResponseOutputItem::Message(a), r::ResponseOutputItem::Message(b)) => {
                if a.role != b.role
                    || b.content.len()
                        != a.content.len()
                            + self.parts.keys().filter(|(summary, _)| !summary).count()
                {
                    return Err(invalid("message parts missing or duplicated"));
                }
                if !b.content.starts_with(&a.content) {
                    return Err(invalid("initial message contents changed"));
                }
            }
            (r::ResponseOutputItem::Reasoning(a), r::ResponseOutputItem::Reasoning(b)) => {
                if b.summary.len()
                    != a.summary.len() + self.parts.keys().filter(|(summary, _)| *summary).count()
                    || !b.summary.starts_with(&a.summary)
                    || b.content.as_deref().unwrap_or_default().len()
                        != a.content.as_deref().unwrap_or_default().len()
                            + self.parts.keys().filter(|(summary, _)| !summary).count()
                    || !b
                        .content
                        .as_deref()
                        .unwrap_or_default()
                        .starts_with(a.content.as_deref().unwrap_or_default())
                {
                    return Err(invalid("reasoning parts missing or changed"));
                }
            }
            (r::ResponseOutputItem::FunctionCall(a), r::ResponseOutputItem::FunctionCall(b))
                if a.call_id != b.call_id
                    || a.name != b.name
                    || a.namespace != b.namespace
                    || a.caller != b.caller
                    || a.arguments != b.arguments =>
            {
                return Err(invalid("function identity or arguments changed"));
            }
            (
                r::ResponseOutputItem::CustomToolCall(a),
                r::ResponseOutputItem::CustomToolCall(b),
            ) if a.call_id != b.call_id
                || a.name != b.name
                || a.namespace != b.namespace
                || a.caller != b.caller
                || a.input != b.input =>
            {
                return Err(invalid("custom tool identity or input changed"));
            }
            (r::ResponseOutputItem::McpCall(a), r::ResponseOutputItem::McpCall(b))
                if a.name != b.name
                    || a.server_label != b.server_label
                    || a.arguments != b.arguments =>
            {
                return Err(invalid("MCP identity or arguments changed"));
            }
            (
                r::ResponseOutputItem::CodeInterpreterCall(a),
                r::ResponseOutputItem::CodeInterpreterCall(b),
            ) if self.argument_streamed && a.code != b.code => {
                return Err(invalid("code deltas contradict final code"));
            }
            _ => {}
        }
        if self.argument_streamed && !self.argument_done {
            return Err(invalid("argument stream missing done"));
        }
        for ((summary, index), part) in &self.parts {
            part.check_final(&final_item, *summary, *index)?;
        }
        self.value = final_item;
        self.done = true;
        Ok(())
    }
    pub fn base_parts(&self, summary: bool) -> Result<usize, TransformError> {
        match (&self.value, summary) {
            (r::ResponseOutputItem::Message(v), false) => Ok(v.content.len()),
            (r::ResponseOutputItem::Reasoning(v), true) => Ok(v.summary.len()),
            (r::ResponseOutputItem::Reasoning(v), false) => {
                Ok(v.content.as_ref().map_or(0, Vec::len))
            }
            _ => Err(invalid("content event on wrong item type")),
        }
    }
}

impl Item {
    pub fn progress(&mut self, kind: u8, phase: u8) -> Result<(), TransformError> {
        let actual = match self.value {
            r::ResponseOutputItem::ImageGenerationCall(_) => 0,
            r::ResponseOutputItem::CodeInterpreterCall(_) => 1,
            r::ResponseOutputItem::FileSearchCall(_) => 2,
            r::ResponseOutputItem::WebSearchCall(_) => 3,
            r::ResponseOutputItem::McpCall(_) => 4,
            r::ResponseOutputItem::McpListTools(_) => 5,
            _ => 255,
        };
        if kind != actual
            || self
                .progress_phase
                .is_some_and(|old| old >= 2 || phase < old)
        {
            return Err(invalid("tool progress type/phase mismatch"));
        }
        self.progress_phase = Some(phase);
        Ok(())
    }
    fn check_progress(&self, item: &r::ResponseOutputItem) -> Result<(), TransformError> {
        let Some(phase) = self.progress_phase.filter(|v| *v >= 2) else {
            return Ok(());
        };
        let success = phase == 2;
        let valid = match item {
            r::ResponseOutputItem::ImageGenerationCall(v) => {
                success && v.status == i::ImageGenerationStatus::Completed
            }
            r::ResponseOutputItem::CodeInterpreterCall(v) => {
                success && v.status == i::CodeInterpreterStatus::Completed
            }
            r::ResponseOutputItem::FileSearchCall(v) => {
                success && v.status == i::FileSearchStatus::Completed
            }
            r::ResponseOutputItem::WebSearchCall(v) => {
                success && v.status == i::WebSearchStatus::Completed
            }
            r::ResponseOutputItem::McpCall(v) => {
                v.status
                    == Some(if success {
                        i::McpCallStatus::Completed
                    } else {
                        i::McpCallStatus::Failed
                    })
            }
            r::ResponseOutputItem::McpListTools(v) => {
                success != v.error.as_ref().and_then(|v| v.as_ref()).is_some()
            }
            _ => false,
        };
        if valid {
            Ok(())
        } else {
            Err(invalid("final tool item contradicts progress terminal"))
        }
    }
}

fn same_links(a: &r::ResponseOutputItem, b: &r::ResponseOutputItem) -> bool {
    match (a, b) {
        (
            r::ResponseOutputItem::FunctionCallOutput(a),
            r::ResponseOutputItem::FunctionCallOutput(b),
        ) => {
            a.call_id == b.call_id
                && a.name == b.name
                && a.namespace == b.namespace
                && a.caller == b.caller
        }
        (r::ResponseOutputItem::ComputerCall(a), r::ResponseOutputItem::ComputerCall(b)) => {
            a.call_id == b.call_id && a.action == b.action && a.actions == b.actions
        }
        (
            r::ResponseOutputItem::ComputerCallOutput(a),
            r::ResponseOutputItem::ComputerCallOutput(b),
        ) => a.call_id == b.call_id,
        (r::ResponseOutputItem::Program(a), r::ResponseOutputItem::Program(b)) => {
            a.call_id == b.call_id && a.code == b.code && a.fingerprint == b.fingerprint
        }
        (r::ResponseOutputItem::ProgramOutput(a), r::ResponseOutputItem::ProgramOutput(b)) => {
            a.call_id == b.call_id
        }
        (r::ResponseOutputItem::ToolSearchCall(a), r::ResponseOutputItem::ToolSearchCall(b)) => {
            a.call_id == b.call_id
        }
        (
            r::ResponseOutputItem::ToolSearchOutput(a),
            r::ResponseOutputItem::ToolSearchOutput(b),
        ) => a.call_id == b.call_id,
        (r::ResponseOutputItem::LocalShellCall(a), r::ResponseOutputItem::LocalShellCall(b)) => {
            a.call_id == b.call_id && a.action == b.action
        }
        (r::ResponseOutputItem::ShellCall(a), r::ResponseOutputItem::ShellCall(b)) => {
            a.call_id == b.call_id
                && a.action == b.action
                && a.environment == b.environment
                && a.caller == b.caller
        }
        (r::ResponseOutputItem::ShellCallOutput(a), r::ResponseOutputItem::ShellCallOutput(b)) => {
            a.call_id == b.call_id
        }
        (r::ResponseOutputItem::ApplyPatchCall(a), r::ResponseOutputItem::ApplyPatchCall(b)) => {
            a.call_id == b.call_id && a.operation == b.operation && a.caller == b.caller
        }
        (
            r::ResponseOutputItem::ApplyPatchCallOutput(a),
            r::ResponseOutputItem::ApplyPatchCallOutput(b),
        ) => a.call_id == b.call_id,
        (
            r::ResponseOutputItem::CustomToolCallOutput(a),
            r::ResponseOutputItem::CustomToolCallOutput(b),
        ) => a.call_id == b.call_id,
        (
            r::ResponseOutputItem::McpApprovalResponse(a),
            r::ResponseOutputItem::McpApprovalResponse(b),
        ) => a.approval_request_id == b.approval_request_id,
        (
            r::ResponseOutputItem::McpApprovalRequest(a),
            r::ResponseOutputItem::McpApprovalRequest(b),
        ) => a.name == b.name && a.server_label == b.server_label && a.arguments == b.arguments,
        (r::ResponseOutputItem::McpListTools(a), r::ResponseOutputItem::McpListTools(b)) => {
            a.server_label == b.server_label
        }
        (
            r::ResponseOutputItem::CodeInterpreterCall(a),
            r::ResponseOutputItem::CodeInterpreterCall(b),
        ) => a.container_id == b.container_id,
        _ => true,
    }
}