use std::sync::Arc;
use serde_json::{Map, Value};
use crate::domain::feature::Feature;
use crate::domain::ir::ToolResult;
use crate::domain::ir::lower::{LowerError, TranslationReport, check_expressible};
use crate::domain::{DegradePolicy, Leg, ProtocolVersion};
const RESULT: &str = "result";
const ERROR: &str = "error";
const RESULT_TYPE_COMPLETE: &str = "complete";
pub trait ToolResultCodec: Send + Sync {
fn lift(&self, version: ProtocolVersion, wire: &Value) -> Option<ToolResult>;
fn lower(
&self,
version: ProtocolVersion,
value: ToolResult,
policy: DegradePolicy,
report: &mut TranslationReport,
) -> Result<Value, LowerError>;
}
impl<C: ToolResultCodec> ToolResultCodec for Arc<C> {
fn lift(&self, version: ProtocolVersion, wire: &Value) -> Option<ToolResult> {
(**self).lift(version, wire)
}
fn lower(
&self,
version: ProtocolVersion,
value: ToolResult,
policy: DegradePolicy,
report: &mut TranslationReport,
) -> Result<Value, LowerError> {
(**self).lower(version, value, policy, report)
}
}
#[derive(Debug)]
pub struct Translated {
pub reply: Map<String, Value>,
pub report: TranslationReport,
}
#[derive(Debug)]
pub struct Translator<C> {
codec: C,
}
impl<C: ToolResultCodec> Translator<C> {
#[must_use]
pub const fn new(codec: C) -> Self {
Self { codec }
}
pub fn translate(
&self,
reply: Map<String, Value>,
from: ProtocolVersion,
to: &Leg,
policy: DegradePolicy,
) -> Result<Translated, LowerError> {
let untouched = TranslationReport::new(to.version);
let lifted = {
if reply.contains_key(ERROR) {
return Ok(Translated {
reply,
report: untouched,
});
}
let Some(payload) = reply.get(RESULT) else {
return Ok(Translated {
reply,
report: untouched,
});
};
if !payload.is_object() {
return Ok(Translated {
reply,
report: untouched,
});
}
self.codec.lift(from, payload)
};
let Some(lifted) = lifted else {
return Ok(Translated {
reply,
report: untouched,
});
};
let mut lifted = lifted;
if lifted.result_type.as_deref() == Some(RESULT_TYPE_COMPLETE)
&& !to.profile.allows(Feature::ToolResultType)
{
lifted.result_type = None;
}
if check_expressible(&lifted.required_features(), &to.profile, to.version).is_ok() {
return Ok(Translated {
reply,
report: untouched,
});
}
let mut report = TranslationReport::new(to.version);
let value = self.codec.lower(to.version, lifted, policy, &mut report)?;
let mut reply = reply;
reply.insert(RESULT.to_owned(), value);
Ok(Translated { reply, report })
}
}
#[cfg(test)]
pub(in crate::application::bridge) mod doubles {
use core::sync::atomic::{AtomicUsize, Ordering};
use serde_json::{Value, json};
use super::ToolResultCodec;
use crate::domain::ir::{LowerError, ToolResult, TranslationReport};
use crate::domain::{DegradePolicy, ProtocolVersion};
pub(in crate::application::bridge) struct Counting {
readable: Option<ToolResult>,
lifts: AtomicUsize,
lowers: AtomicUsize,
}
impl Counting {
#[must_use]
pub(in crate::application::bridge) fn reading(readable: ToolResult) -> Self {
Self {
readable: Some(readable),
lifts: AtomicUsize::new(0),
lowers: AtomicUsize::new(0),
}
}
#[must_use]
pub(in crate::application::bridge) fn reading_nothing() -> Self {
Self {
readable: None,
lifts: AtomicUsize::new(0),
lowers: AtomicUsize::new(0),
}
}
pub(in crate::application::bridge) fn lifts(&self) -> usize {
self.lifts.load(Ordering::Relaxed)
}
pub(in crate::application::bridge) fn lowers(&self) -> usize {
self.lowers.load(Ordering::Relaxed)
}
}
impl ToolResultCodec for Counting {
fn lift(&self, _version: ProtocolVersion, _wire: &Value) -> Option<ToolResult> {
self.lifts.fetch_add(1, Ordering::Relaxed);
self.readable.clone()
}
fn lower(
&self,
_version: ProtocolVersion,
value: ToolResult,
policy: DegradePolicy,
report: &mut TranslationReport,
) -> Result<Value, LowerError> {
self.lowers.fetch_add(1, Ordering::Relaxed);
let (lowered, lowered_report) = crate::domain::ir::lower::lower_tool_result(
value,
&crate::domain::FeatureProfile::empty(),
ProtocolVersion::V2025_03_26,
policy,
)?;
report.absorb(&lowered_report);
Ok(json!({ "rebuilt": true, "blocks": lowered.content.len() }))
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use serde_json::{Map, Value, json};
use super::doubles::Counting;
use super::{ERROR, RESULT, Translator};
use crate::domain::ir::lower::LowerError;
use crate::domain::ir::{FeatureSet, ToolResult};
use crate::domain::{
CapabilitySet, DegradePolicy, Feature, FeatureProfile, Leg, Negotiation, ProtocolVersion,
TransportKind, resolve,
};
type TestError = Box<dyn std::error::Error>;
fn client_leg() -> Leg {
let version = ProtocolVersion::V2025_03_26;
let caps = CapabilitySet::all_for(version);
Leg::new(
version,
resolve(Negotiation {
version,
client_caps: caps,
server_caps: caps,
transport: TransportKind::Stdio,
}),
)
}
fn bare_leg() -> Leg {
Leg::new(ProtocolVersion::V2024_11_05, FeatureProfile::empty())
}
fn reply(payload: Value) -> Map<String, Value> {
let mut reply = Map::new();
reply.insert("jsonrpc".to_owned(), json!("2.0"));
reply.insert("id".to_owned(), json!(1));
reply.insert(RESULT.to_owned(), payload);
reply
}
fn structured() -> Result<ToolResult, TestError> {
Ok(ToolResult::text("answer").with_structured(r#"{"a":1}"#)?)
}
#[test]
fn a_reply_needing_no_translation_never_reaches_the_codec() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading(ToolResult::text("answer")));
let sent = reply(json!({ "content": [{ "type": "text", "text": "answer" }] }));
let translated = Translator::new(Arc::clone(&codec)).translate(
sent.clone(),
ProtocolVersion::V2026_07_28,
&client_leg(),
DegradePolicy::strict(),
)?;
assert_eq!(translated.reply, sent, "the peer's own bytes must survive");
assert!(translated.report.is_empty());
assert_eq!(
codec.lifts(),
1,
"it looked, found nothing to do, and stopped"
);
assert_eq!(
codec.lowers(),
0,
"a reply with nothing to decide is not rebuilt"
);
Ok(())
}
#[test]
fn a_complete_result_type_does_not_stop_an_older_client() -> Result<(), TestError> {
let mut finished = ToolResult::text("answer");
finished.result_type = Some("complete".to_owned());
let codec = Arc::new(Counting::reading(finished));
let sent = reply(json!({
"content": [{ "type": "text", "text": "answer" }],
"resultType": "complete",
}));
let translated = Translator::new(Arc::clone(&codec)).translate(
sent.clone(),
ProtocolVersion::V2026_07_28,
&client_leg(),
DegradePolicy::strict(),
)?;
assert_eq!(translated.reply, sent);
assert_eq!(codec.lowers(), 0);
Ok(())
}
#[test]
fn an_input_required_result_is_still_refused_for_an_older_client() {
let mut pending = ToolResult::text("answer");
pending.result_type = Some("input_required".to_owned());
let codec = Arc::new(Counting::reading(pending));
let sent = reply(json!({
"content": [{ "type": "text", "text": "answer" }],
"resultType": "input_required",
}));
let outcome = Translator::new(codec).translate(
sent,
ProtocolVersion::V2026_07_28,
&client_leg(),
DegradePolicy::permissive(),
);
assert!(outcome.is_err(), "{outcome:?}");
}
#[test]
fn a_shape_this_build_cannot_read_is_forwarded_untouched() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading_nothing());
let sent =
reply(json!({ "content": [{ "type": "annotation", "audience": ["assistant"] }] }));
let translated = Translator::new(Arc::clone(&codec)).translate(
sent.clone(),
ProtocolVersion::V2026_07_28,
&client_leg(),
DegradePolicy::strict(),
)?;
assert_eq!(translated.reply, sent);
assert!(translated.report.is_empty());
assert_eq!(codec.lifts(), 1);
assert_eq!(codec.lowers(), 0);
Ok(())
}
#[test]
fn an_authorised_downgrade_rebuilds_the_result_and_records_it() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading(structured()?));
let sent = reply(json!({ "content": [{ "type": "text", "text": "answer" }],
"structuredContent": { "a": 1 } }));
let translated = Translator::new(Arc::clone(&codec)).translate(
sent,
ProtocolVersion::V2026_07_28,
&bare_leg(),
DegradePolicy::permissive(),
)?;
assert!(!translated.report.is_empty(), "a rebuild must be recorded");
assert_eq!(
translated.reply.get(RESULT),
Some(&json!({ "rebuilt": true, "blocks": 2 }))
);
assert_eq!(codec.lowers(), 1);
Ok(())
}
#[test]
fn a_strict_policy_refuses_rather_than_writing() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading(structured()?));
let sent = reply(json!({ "content": [], "structuredContent": { "a": 1 } }));
let outcome = Translator::new(Arc::clone(&codec)).translate(
sent,
ProtocolVersion::V2026_07_28,
&bare_leg(),
DegradePolicy::strict(),
);
assert!(
matches!(
outcome,
Err(LowerError::FeatureUnavailable {
feature: Feature::ToolStructuredOutput,
..
})
),
"expected the named feature to be refused, got {outcome:?}"
);
assert_eq!(
codec.lowers(),
1,
"the codec is where the policy is enforced, so it is entered once"
);
Ok(())
}
#[test]
fn an_error_reply_is_never_rebuilt() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading(structured()?));
let mut sent = Map::new();
sent.insert("jsonrpc".to_owned(), json!("2.0"));
sent.insert("id".to_owned(), json!(1));
sent.insert(
ERROR.to_owned(),
json!({ "code": -32601, "message": "no such tool" }),
);
let translated = Translator::new(Arc::clone(&codec)).translate(
sent.clone(),
ProtocolVersion::V2026_07_28,
&bare_leg(),
DegradePolicy::permissive(),
)?;
assert_eq!(translated.reply, sent);
assert!(translated.report.is_empty());
assert_eq!(codec.lifts(), 0, "an error envelope is not a tool result");
Ok(())
}
#[test]
fn a_non_object_payload_is_left_alone() -> Result<(), TestError> {
let codec = Arc::new(Counting::reading(structured()?));
let sent = reply(json!([{ "name": "echo" }]));
let translated = Translator::new(Arc::clone(&codec)).translate(
sent.clone(),
ProtocolVersion::V2026_07_28,
&bare_leg(),
DegradePolicy::permissive(),
)?;
assert_eq!(translated.reply, sent);
assert_eq!(codec.lowers(), 0);
Ok(())
}
#[test]
fn a_text_result_needs_nothing_from_any_profile() {
let required = ToolResult::text("answer").required_features();
let available = FeatureSet::union_of(FeatureProfile::empty().iter());
assert!(required.is_subset_of(available));
}
}