use bytes::Bytes;
use serde_json::Value;
use sha2::{Digest, Sha256};
const PAYLOAD_CAP_BYTES: usize = 1024 * 1024;
const EXCERPT_BYTES: usize = 8 * 1024;
#[derive(Debug, Clone)]
pub struct PayloadCapture {
pub json: Option<Value>,
pub excerpt: Option<String>,
pub truncated: bool,
pub byte_len: i32,
pub sha256: String,
}
#[must_use]
pub fn digest_hex(bytes: &[u8]) -> String {
hex::encode(Sha256::digest(bytes))
}
#[must_use]
pub fn slice_payload(bytes: &Bytes) -> PayloadCapture {
let len = bytes.len();
let byte_len = len.min(i32::MAX as usize) as i32;
let sha256 = digest_hex(bytes);
if len <= PAYLOAD_CAP_BYTES {
serde_json::from_slice::<Value>(bytes).map_or_else(
|_| PayloadCapture {
json: None,
excerpt: Some(String::from_utf8_lossy(bytes).to_string()),
truncated: false,
byte_len,
sha256: sha256.clone(),
},
|v| PayloadCapture {
json: Some(v),
excerpt: None,
truncated: false,
byte_len,
sha256: sha256.clone(),
},
)
} else {
let head_len = EXCERPT_BYTES.min(len);
let head = String::from_utf8_lossy(&bytes[..head_len]).to_string();
let tail_start = len.saturating_sub(EXCERPT_BYTES);
let tail_len = len - tail_start;
let tail = String::from_utf8_lossy(&bytes[tail_start..]).to_string();
let dropped = len - head_len - tail_len;
let excerpt = format!("{head}\n...<truncated {dropped} bytes>...\n{tail}");
PayloadCapture {
json: None,
excerpt: Some(excerpt),
truncated: true,
byte_len,
sha256,
}
}
}
pub fn truncate_for_tool_input(input: &str) -> String {
const TOOL_INPUT_CAP: usize = 64 * 1024;
if input.len() <= TOOL_INPUT_CAP {
input.to_owned()
} else {
let mut cut = TOOL_INPUT_CAP;
while cut > 0 && !input.is_char_boundary(cut) {
cut -= 1;
}
let head = &input[..cut];
format!("{head}...<truncated {} bytes>", input.len() - cut)
}
}