blob: 4a13f0288edc737a02e8e048b0770c1944bcfcac [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! A selected image subscription produces owned bytes, never a path or bearer.
use crate::contract::ImageFrame;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use maka_agent_facts::tool_output::ToolSuccess;
use maka_agent_facts::tools::ToolError;
use maka_capabilities::call::Attachments;
use maka_host_runtime::application::Context;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use maka_agent_api::IMAGE_CHUNK_BYTES as CHUNK_BYTES;
pub(super) async fn read(
request: &Context,
import: &str,
method: String,
input: Value,
cancellation: &CancellationToken,
) -> Result<ToolSuccess, ToolError> {
let mut subscription = request
.subscribe(import, 0, method, input, Attachments::default())
.map_err(failed)?
.await
.map_err(failed)?;
let mut image = Image::default();
loop {
let value = tokio::select! {
biased;
() = cancellation.cancelled() => return Err(failed("Image tool cancelled")),
value = subscription.recv() => value,
};
let Some(value) = value else {
break;
};
image.push(value)?;
}
let completion = subscription.completion().await;
completion.cleanup.map_err(ToolError::CleanupUnconfirmed)?;
let value = completion.result.map_err(failed)?;
if !matches!(
serde_json::from_value::<ImageFrame>(value),
Ok(ImageFrame::Complete)
) {
return Err(failed("Image producer did not complete"));
}
image.finish()
}
#[derive(Default)]
struct Image {
descriptor: Option<(String, usize)>,
bytes: Vec<u8>,
}
impl Image {
fn push(&mut self, value: Value) -> Result<(), ToolError> {
let ImageFrame::Chunk {
mime_type,
total_bytes,
offset,
data,
} = serde_json::from_value(value).map_err(|_| failed("Invalid image chunk"))?
else {
return Err(failed("Image completion cannot be emitted as a chunk"));
};
if total_bytes == 0
|| offset != self.bytes.len()
|| offset >= total_bytes
|| data.is_empty()
|| data.len() > 43692
{
return Err(failed("Image chunk exceeds bounds or is out of order"));
}
let chunk = STANDARD
.decode(data)
.map_err(|_| failed("Invalid image Base64"))?;
if chunk.len() != CHUNK_BYTES.min(total_bytes - offset) {
return Err(failed("Image chunk exceeds its declared bounds"));
}
match &self.descriptor {
Some((mime, total)) if mime != &mime_type || *total != total_bytes => {
return Err(failed("Image descriptor changed during transfer"));
}
Some(_) => {}
None => {
if !matches!(
mime_type.as_str(),
"image/png" | "image/jpeg" | "image/gif" | "image/webp"
) {
return Err(failed("Unsupported image MIME"));
}
self.descriptor = Some((mime_type, total_bytes));
}
}
self.bytes.try_reserve(chunk.len()).map_err(failed)?;
self.bytes.extend_from_slice(&chunk);
Ok(())
}
fn finish(self) -> Result<ToolSuccess, ToolError> {
let Some((mime, total)) = self.descriptor else {
return Err(failed("Image producer returned no image"));
};
if self.bytes.len() != total {
return Err(failed("Image transfer ended before all bytes arrived"));
}
if maka_fs_tools::image::validate(&self.bytes)? != mime {
return Err(failed("Image MIME does not match its content"));
}
ToolSuccess::image(self.bytes, mime).map_err(failed)
}
}
fn failed(error: impl std::fmt::Display) -> ToolError {
ToolError::Failed(error.to_string())
}