use serde_json::Value;
use crate::application::bridge::ToolResultCodec;
use crate::application::server::result::OutcomeError;
use crate::domain::ir::{JsonObject, ToolResult, TranslationReport};
use crate::domain::{DegradePolicy, ProtocolVersion};
use super::WireToolResults;
pub fn verbatim(version: ProtocolVersion, result: &ToolResult) -> Result<JsonObject, OutcomeError> {
let mut report = TranslationReport::default();
let value: Value = WireToolResults
.lower(
version,
result.clone(),
DegradePolicy::strict(),
&mut report,
)
.map_err(OutcomeError::NotLowerable)?;
JsonObject::from_value(value).map_err(OutcomeError::NotAnObject)
}
#[cfg(test)]
mod tests {
use super::verbatim;
use crate::domain::ProtocolVersion;
use crate::domain::ir::{AudioContent, Content, ImageContent, ToolResult};
fn text_result(text: &str) -> ToolResult {
ToolResult::text(text)
}
fn document(version: ProtocolVersion, result: &ToolResult) -> Option<serde_json::Value> {
let lowered = verbatim(version, result).ok()?;
serde_json::from_str(lowered.as_str()).ok()
}
fn first_block(value: &serde_json::Value) -> Option<&serde_json::Value> {
value
.get("content")
.and_then(serde_json::Value::as_array)
.and_then(|blocks| blocks.first())
}
fn is_error(value: &serde_json::Value) -> Option<bool> {
value.get("isError").and_then(serde_json::Value::as_bool)
}
fn is_success(value: &serde_json::Value) -> bool {
matches!(is_error(value), None | Some(false))
}
#[test]
fn a_text_answer_lands_in_the_content_array() -> Result<(), String> {
let result = text_result("hello");
let value =
document(ProtocolVersion::V2025_06_18, &result).ok_or("the document did not lower")?;
let block = first_block(&value).ok_or("no content block")?;
match block.get("text").and_then(serde_json::Value::as_str) {
Some("hello") => {}
other => return Err(format!("the text block was {other:?}")),
}
if is_success(&value) {
Ok(())
} else {
Err(format!("a successful result claimed an error: {value}"))
}
}
#[test]
fn an_image_answer_keeps_its_type_and_data() -> Result<(), String> {
let image = ImageContent::new("aGk=", "image/png");
let result = ToolResult::new(vec![Content::Image(image)]);
let value =
document(ProtocolVersion::V2025_06_18, &result).ok_or("the document did not lower")?;
let block = first_block(&value).ok_or("no content block")?;
match block.get("type").and_then(serde_json::Value::as_str) {
Some("image") => {}
other => return Err(format!("the block type was {other:?}")),
}
match block.get("data").and_then(serde_json::Value::as_str) {
Some("aGk=") => Ok(()),
other => Err(format!("the data was {other:?}")),
}
}
#[test]
fn audio_is_refused_by_the_revision_that_has_no_audio_block() -> Result<(), String> {
let audio = AudioContent::new("aGk=", "audio/wav");
let result = ToolResult::new(vec![Content::Audio(audio)]);
if document(ProtocolVersion::V2025_03_26, &result).is_none() {
return Err("2025-03-26 refused an audio block it introduced".to_owned());
}
if document(ProtocolVersion::V2024_11_05, &result).is_some() {
return Err("2024-11-05 answered with an audio block it does not have".to_owned());
}
Ok(())
}
#[test]
fn an_error_flag_survives_the_lowering() -> Result<(), String> {
let result = text_result("nope").error();
let value =
document(ProtocolVersion::V2025_06_18, &result).ok_or("the document did not lower")?;
match is_error(&value) {
Some(true) => Ok(()),
other => Err(format!("isError was {other:?}")),
}
}
}