a3s-code-core 6.9.0

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
use super::ToolResultLossModeV1;
use crate::text::truncate_utf8;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;

pub const TOOL_RESULT_TRANSFORM_SCHEMA_V1: &str = "a3s.code.tool-result-transform-policy.v1";
pub const TOOL_RESULT_TRANSFORM_ALGORITHM_V1: &str = "a3s.code.tool-result-transform.v1";
const MARKER_RESERVE_BYTES: usize = 512;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolResultTransformPolicyV1 {
    pub schema: String,
    pub max_output_bytes: usize,
    pub head_bytes: usize,
    pub tail_bytes: usize,
    pub fold_repeated_lines: bool,
    pub repeated_line_threshold: usize,
    pub structured_sample_items: usize,
}

impl Default for ToolResultTransformPolicyV1 {
    fn default() -> Self {
        Self::conservative()
    }
}

impl ToolResultTransformPolicyV1 {
    pub fn conservative() -> Self {
        Self {
            schema: TOOL_RESULT_TRANSFORM_SCHEMA_V1.to_string(),
            max_output_bytes: super::MAX_OUTPUT_SIZE,
            head_bytes: super::MAX_OUTPUT_SIZE,
            tail_bytes: 0,
            fold_repeated_lines: false,
            repeated_line_threshold: 3,
            structured_sample_items: 0,
        }
    }

    pub fn context_efficient() -> Self {
        Self {
            schema: TOOL_RESULT_TRANSFORM_SCHEMA_V1.to_string(),
            max_output_bytes: super::MAX_OUTPUT_SIZE,
            head_bytes: 64 * 1024,
            tail_bytes: 32 * 1024,
            fold_repeated_lines: true,
            repeated_line_threshold: 3,
            structured_sample_items: 32,
        }
    }

    pub fn validate(&self) -> Result<()> {
        anyhow::ensure!(
            self.schema == TOOL_RESULT_TRANSFORM_SCHEMA_V1,
            "unsupported Tool result transform policy schema {:?}",
            self.schema
        );
        anyhow::ensure!(
            (1024..=super::MAX_OUTPUT_SIZE).contains(&self.max_output_bytes),
            "Tool result max_output_bytes must be between 1024 and {}",
            super::MAX_OUTPUT_SIZE
        );
        anyhow::ensure!(
            self.head_bytes > 0,
            "Tool result head_bytes must be positive"
        );
        let retained = self.head_bytes.saturating_add(self.tail_bytes);
        let valid_compatibility_profile = self.tail_bytes == 0 && retained == self.max_output_bytes;
        anyhow::ensure!(
            valid_compatibility_profile
                || retained.saturating_add(MARKER_RESERVE_BYTES) <= self.max_output_bytes,
            "Tool result head_bytes + tail_bytes must reserve {MARKER_RESERVE_BYTES} bytes for transformation evidence"
        );
        anyhow::ensure!(
            (2..=10_000).contains(&self.repeated_line_threshold),
            "Tool result repeated_line_threshold must be between 2 and 10000"
        );
        anyhow::ensure!(
            self.structured_sample_items <= 1024,
            "Tool result structured_sample_items must not exceed 1024"
        );
        Ok(())
    }
}

pub(crate) struct ToolResultTransform {
    pub content: String,
    pub loss_mode: ToolResultLossModeV1,
    pub retained_original_bytes: usize,
}

pub(crate) fn transform(output: &str, policy: &ToolResultTransformPolicyV1) -> ToolResultTransform {
    let mut content = output.to_string();
    let mut transformed = false;

    if policy.structured_sample_items > 0 && output.len() > policy.max_output_bytes {
        if let Some(sampled) = sample_structured(output, policy.structured_sample_items) {
            content = sampled;
            transformed = true;
        }
    }
    if policy.fold_repeated_lines {
        let folded = fold_repeated_lines(&content, policy.repeated_line_threshold);
        transformed |= folded != content;
        content = folded;
    }
    if content.len() <= policy.max_output_bytes {
        return ToolResultTransform {
            retained_original_bytes: if transformed { 0 } else { output.len() },
            content,
            loss_mode: if transformed {
                ToolResultLossModeV1::DeterministicTransform
            } else {
                ToolResultLossModeV1::None
            },
        };
    }

    let head = truncate_utf8(&content, policy.head_bytes);
    let tail = utf8_tail(&content, policy.tail_bytes);
    let omitted = content.len().saturating_sub(head.len() + tail.len());
    let marker = if policy.tail_bytes == 0 && !transformed {
        format!(
            "\n\n[tool output truncated: showing the first {} of {} bytes. Full output is retained as an immutable artifact.]",
            head.len(),
            content.len()
        )
    } else {
        format!(
            "\n\n[tool output bounded by {}: omitted {} bytes between retained head/tail regions]\n\n",
            TOOL_RESULT_TRANSFORM_ALGORITHM_V1, omitted
        )
    };
    let projected = if tail.is_empty() {
        format!("{head}{marker}")
    } else {
        format!("{head}{marker}{tail}")
    };
    ToolResultTransform {
        content: projected,
        loss_mode: if transformed {
            ToolResultLossModeV1::Composite
        } else if policy.tail_bytes == 0 {
            ToolResultLossModeV1::BoundedPreview
        } else {
            ToolResultLossModeV1::HeadTail
        },
        retained_original_bytes: if transformed {
            0
        } else {
            head.len() + tail.len()
        },
    }
}

fn utf8_tail(value: &str, max_bytes: usize) -> &str {
    if max_bytes == 0 || value.is_empty() {
        return "";
    }
    let mut start = value.len().saturating_sub(max_bytes);
    while start < value.len() && !value.is_char_boundary(start) {
        start += 1;
    }
    &value[start..]
}

fn fold_repeated_lines(value: &str, threshold: usize) -> String {
    let lines = value.split_inclusive('\n').collect::<Vec<_>>();
    if lines.len() < threshold {
        return value.to_string();
    }
    let mut output = String::with_capacity(value.len());
    let mut index = 0;
    while index < lines.len() {
        let mut end = index + 1;
        while end < lines.len() && lines[end] == lines[index] {
            end += 1;
        }
        let count = end - index;
        if count >= threshold {
            output.push_str(lines[index]);
            output.push_str(&format!(
                "[a3s repeated-line fold: {} additional exact copies omitted]\n",
                count - 1
            ));
        } else {
            for line in &lines[index..end] {
                output.push_str(line);
            }
        }
        index = end;
    }
    if output.len() < value.len() {
        output
    } else {
        value.to_string()
    }
}

fn sample_structured(value: &str, max_items: usize) -> Option<String> {
    let Value::Array(items) = serde_json::from_str::<Value>(value).ok()? else {
        return None;
    };
    if items.len() <= max_items {
        return None;
    }
    let head_count = max_items.div_ceil(2);
    let tail_count = max_items / 2;
    let mut sampled = items[..head_count].to_vec();
    sampled.extend_from_slice(&items[items.len() - tail_count..]);
    serde_json::to_string(&serde_json::json!({
        "$a3s_sample": {
            "schema": TOOL_RESULT_TRANSFORM_ALGORITHM_V1,
            "kind": "json_array",
            "original_items": items.len(),
            "retained_items": sampled.len(),
            "omitted_items": items.len() - sampled.len(),
        },
        "items": sampled,
    }))
    .ok()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn profiles_are_closed_and_valid() {
        ToolResultTransformPolicyV1::conservative()
            .validate()
            .unwrap();
        ToolResultTransformPolicyV1::context_efficient()
            .validate()
            .unwrap();
        let mut invalid = ToolResultTransformPolicyV1::context_efficient();
        invalid.schema = "future".into();
        assert!(invalid.validate().is_err());
    }

    #[test]
    fn context_profile_retains_utf8_head_and_tail() {
        let mut policy = ToolResultTransformPolicyV1::context_efficient();
        policy.max_output_bytes = 1024;
        policy.head_bytes = 256;
        policy.tail_bytes = 256;
        policy.structured_sample_items = 0;
        let output = format!("BEGIN-{}-END", "".repeat(600));
        let transformed = transform(&output, &policy);
        assert_eq!(transformed.loss_mode, ToolResultLossModeV1::HeadTail);
        assert!(transformed.content.starts_with("BEGIN-"));
        assert!(transformed.content.ends_with("-END"));
        assert!(std::str::from_utf8(transformed.content.as_bytes()).is_ok());
    }

    #[test]
    fn folds_exact_runs_and_samples_large_json_arrays() {
        let policy = ToolResultTransformPolicyV1::context_efficient();
        let repeated = format!("{}\n", "same".repeat(32));
        let folded = transform(
            &format!("{repeated}{repeated}{repeated}{repeated}next\n"),
            &policy,
        );
        assert_eq!(
            folded.loss_mode,
            ToolResultLossModeV1::DeterministicTransform
        );
        assert!(folded.content.contains("3 additional exact copies"));

        let items = (0..20_000).map(Value::from).collect::<Vec<_>>();
        let sampled = transform(&serde_json::to_string(&items).unwrap(), &policy);
        assert!(matches!(
            sampled.loss_mode,
            ToolResultLossModeV1::DeterministicTransform | ToolResultLossModeV1::Composite
        ));
        assert!(sampled.content.contains("\"original_items\":20000"));
    }
}