defect-agent 0.1.0-alpha.4

Core agent runtime for defect: turn loop, context compaction, tools and session orchestration.
Documentation
//! Conversion from ACP `ContentBlock` to internal `MessageContent`.
//!
//! A collection of pure functions extracted from the main turn flow: translates ACP
//! content blocks sent by the client into `MessageContent` for the LLM protocol layer. No
//! IO, no state, easy to unit-test (see `turn/test.rs`).

use std::io;

use agent_client_protocol_schema::{
    ContentBlock, EmbeddedResource, EmbeddedResourceResource, ImageContent, ResourceLink,
    TextContent, TextResourceContents,
};

use crate::error::BoxError;
use crate::llm::MessageContent;
use crate::session::TurnError;

pub(super) fn content_block_to_message_content(
    cb: ContentBlock,
) -> Result<Vec<MessageContent>, TurnError> {
    match cb {
        ContentBlock::Text(TextContent { text, .. }) => Ok(vec![MessageContent::Text { text }]),
        ContentBlock::Image(image) => Ok(vec![image_content_to_message_content(image)?]),
        ContentBlock::ResourceLink(link) => Ok(vec![MessageContent::Text {
            text: resource_link_to_text(link),
        }]),
        ContentBlock::Resource(resource) => resource_to_message_content(resource),
        ContentBlock::Audio(_) => Err(invalid_prompt_content(
            "ACP audio content is not supported yet",
        )),
        _ => Err(invalid_prompt_content(
            "unsupported ACP content block variant",
        )),
    }
}

fn image_content_to_message_content(image: ImageContent) -> Result<MessageContent, TurnError> {
    let data = if image.data.is_empty() {
        let Some(uri) = image.uri else {
            return Err(invalid_prompt_content(
                "ACP image content must include data or uri",
            ));
        };
        crate::llm::ImageData::Url { url: uri }
    } else {
        crate::llm::ImageData::Base64 {
            encoded: image.data,
        }
    };

    Ok(MessageContent::Image {
        mime: image.mime_type,
        data,
    })
}

fn resource_to_message_content(
    resource: EmbeddedResource,
) -> Result<Vec<MessageContent>, TurnError> {
    match resource.resource {
        EmbeddedResourceResource::TextResourceContents(text) => Ok(vec![MessageContent::Text {
            text: text_resource_to_text(text),
        }]),
        EmbeddedResourceResource::BlobResourceContents(blob) => {
            Err(invalid_prompt_content(&format!(
                "embedded binary resource is not supported yet: {}",
                blob.uri
            )))
        }
        _ => Err(invalid_prompt_content(
            "unsupported embedded resource variant",
        )),
    }
}

fn resource_link_to_text(link: ResourceLink) -> String {
    let mut lines = vec![format!("resource: {}", link.name)];
    if let Some(title) = link.title {
        lines.push(format!("title: {title}"));
    }
    if let Some(description) = link.description {
        lines.push(format!("description: {description}"));
    }
    if let Some(mime_type) = link.mime_type {
        lines.push(format!("mime_type: {mime_type}"));
    }
    if let Some(size) = link.size {
        lines.push(format!("size: {size}"));
    }
    lines.push(format!("uri: {}", link.uri));
    lines.join("\n")
}

fn text_resource_to_text(resource: TextResourceContents) -> String {
    let mut text = format!("resource: {}", resource.uri);
    if let Some(mime_type) = resource.mime_type {
        text.push_str(&format!("\nmime_type: {mime_type}"));
    }
    text.push_str("\n\n");
    text.push_str(&resource.text);
    text
}

pub(super) fn invalid_prompt_content(message: &str) -> TurnError {
    TurnError::Internal(BoxError::new(io::Error::other(message)))
}