use protocol_droid_schema::wire::{
v2024_11_05 as schema_2024_11_05, v2025_03_26 as schema_2025_03_26,
v2025_06_18 as schema_2025_06_18, v2025_11_25 as schema_2025_11_25,
v2026_07_28 as schema_2026_07_28,
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use super::{
v2024_11_05::V2024_11_05, v2025_03_26::V2025_03_26, v2025_06_18::V2025_06_18,
v2025_11_25::V2025_11_25, v2026_07_28::V2026_07_28,
};
use crate::application::bridge::ToolResultCodec;
use crate::domain::ir::{Codec, LowerError, ToolResult, TranslationReport};
use crate::domain::{DegradePolicy, ProtocolVersion};
#[derive(Debug, Clone, Copy, Default)]
pub struct WireToolResults;
fn read<T: DeserializeOwned>(wire: &Value) -> Option<T> {
serde_json::from_value::<T>(wire.clone()).ok()
}
fn encode(version: ProtocolVersion, wire: impl Serialize) -> Result<Value, LowerError> {
serde_json::to_value(wire).map_err(|error| LowerError::FieldNotRepresentable {
field: "result".to_owned(),
expected: "a result that serialises",
detail: error.to_string(),
target: version,
})
}
impl ToolResultCodec for WireToolResults {
fn lift(&self, version: ProtocolVersion, wire: &Value) -> Option<ToolResult> {
match version {
ProtocolVersion::V2024_11_05 => read::<schema_2024_11_05::CallToolResult>(wire)
.map(|typed| V2024_11_05.lift(&typed)),
ProtocolVersion::V2025_03_26 => read::<schema_2025_03_26::CallToolResult>(wire)
.map(|typed| V2025_03_26.lift(&typed)),
ProtocolVersion::V2025_06_18 => read::<schema_2025_06_18::CallToolResult>(wire)
.map(|typed| V2025_06_18.lift(&typed)),
ProtocolVersion::V2025_11_25 => read::<schema_2025_11_25::CallToolResult>(wire)
.map(|typed| V2025_11_25.lift(&typed)),
ProtocolVersion::V2026_07_28 => read::<schema_2026_07_28::CallToolResult>(wire)
.map(|typed| V2026_07_28.lift(&typed)),
ProtocolVersion::Unknown(_) => None,
}
}
fn lower(
&self,
version: ProtocolVersion,
value: ToolResult,
policy: DegradePolicy,
report: &mut TranslationReport,
) -> Result<Value, LowerError> {
match version {
ProtocolVersion::V2024_11_05 => {
encode(version, V2024_11_05.lower(value, policy, report)?)
}
ProtocolVersion::V2025_03_26 => {
encode(version, V2025_03_26.lower(value, policy, report)?)
}
ProtocolVersion::V2025_06_18 => {
encode(version, V2025_06_18.lower(value, policy, report)?)
}
ProtocolVersion::V2025_11_25 => {
encode(version, V2025_11_25.lower(value, policy, report)?)
}
ProtocolVersion::V2026_07_28 => {
encode(version, V2026_07_28.lower(value, policy, report)?)
}
ProtocolVersion::Unknown(_) => Err(LowerError::FieldNotRepresentable {
field: "result".to_owned(),
expected: "a revision this build has generated wire types for",
detail: format!(
"{version} has no schema vendored here, so there is no \
CallToolResult shape to write the value into"
),
target: version,
}),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::WireToolResults;
use crate::application::bridge::ToolResultCodec;
use crate::domain::ir::lower::check_expressible;
use crate::domain::ir::{LowerError, ToolResult, TranslationReport};
use crate::domain::{
CapabilitySet, DegradePolicy, FeatureProfile, Negotiation, ProtocolVersion, TransportKind,
resolve,
};
type TestError = Box<dyn std::error::Error>;
fn vendored() -> Vec<ProtocolVersion> {
vec![
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
]
}
fn plain() -> ToolResult {
ToolResult::text("hello")
}
#[test]
fn every_vendored_revision_reads_back_what_it_wrote() -> Result<(), TestError> {
let codec = WireToolResults;
let value = plain();
for version in vendored() {
let mut report = TranslationReport::new(version);
let wire = codec.lower(
version,
value.clone(),
DegradePolicy::permissive(),
&mut report,
)?;
let back = codec
.lift(version, &wire)
.ok_or("the codec refused to read back its own output")?;
assert_eq!(back.content, value.content, "content differs at {version}");
assert_eq!(
back.is_error, value.is_error,
"isError differs at {version}"
);
assert!(
report.is_empty(),
"{version} downgraded a plain text result"
);
}
Ok(())
}
#[test]
fn a_block_this_build_cannot_read_lifts_to_nothing() {
let codec = WireToolResults;
let wire = json!({
"content": [{ "type": "annotation", "audience": ["assistant"] }]
});
assert_eq!(codec.lift(ProtocolVersion::V2026_07_28, &wire), None);
}
#[test]
fn a_payload_that_is_not_a_result_lifts_to_nothing() {
let codec = WireToolResults;
let wire = json!([{ "name": "echo" }]);
assert_eq!(codec.lift(ProtocolVersion::V2025_03_26, &wire), None);
}
#[test]
fn an_unknown_revision_is_refused_rather_than_guessed() -> Result<(), TestError> {
let version = ProtocolVersion::Unknown("2027-01-01".parse()?);
let codec = WireToolResults;
assert_eq!(
codec.lift(version, &json!({ "content": [] })),
None,
"an unknown revision is forwarded, not read"
);
let mut report = TranslationReport::new(version);
let outcome = codec.lower(version, plain(), DegradePolicy::permissive(), &mut report);
assert!(
matches!(outcome, Err(LowerError::FieldNotRepresentable { .. })),
"an unknown revision must refuse rather than fabricate a shape, got {outcome:?}"
);
Ok(())
}
#[test]
fn the_published_revision_answers_for_its_version() -> Result<(), TestError> {
let version = ProtocolVersion::V2026_07_28;
let caps = CapabilitySet::all_for(version);
let profile = resolve(Negotiation {
version,
client_caps: caps,
server_caps: caps,
transport: TransportKind::Stdio,
});
let value = ToolResult {
result_type: Some("search".to_owned()),
..ToolResult::text("hello")
};
assert!(
check_expressible(&value.required_features(), &profile, version).is_ok(),
"the fixture must be expressible at its own revision"
);
let mut report = TranslationReport::new(version);
let wire = WireToolResults.lower(version, value, DegradePolicy::strict(), &mut report)?;
assert_eq!(
wire.get("resultType"),
Some(&json!("search")),
"the newest schema must have been used"
);
Ok(())
}
#[test]
fn draft_is_still_a_working_codec() -> Result<(), TestError> {
let mut report = TranslationReport::new(ProtocolVersion::V2026_07_28);
let wire = WireToolResults.lower(
ProtocolVersion::V2026_07_28,
plain(),
DegradePolicy::strict(),
&mut report,
)?;
let back = WireToolResults.lift(ProtocolVersion::V2026_07_28, &wire);
assert!(back.is_some(), "the published revision must round-trip");
Ok(())
}
#[test]
fn a_profile_with_no_features_expresses_only_what_needs_nothing() {
assert!(
check_expressible(
&plain().required_features(),
&FeatureProfile::empty(),
ProtocolVersion::V2024_11_05,
)
.is_ok()
);
}
}