use core::str;
pub const MAX_NAME: usize = 1024;
pub const MAX_CONTENT: usize = 1024 * 1024;
const ID_LEN: usize = 8;
const ROLE_LEN: usize = 1;
const TOKENS_LEN: usize = 4;
const NAME_LEN_LEN: usize = 2;
const CONTENT_LEN_LEN: usize = 4;
const FIXED_PREFIX: usize = ID_LEN + ROLE_LEN + TOKENS_LEN + NAME_LEN_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MsgError {
TooShort,
BadUtf8,
NameTooLong(usize),
ContentTooLong(usize),
BufferTooSmall { need: usize, have: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentMessage<'a> {
pub id: u64,
pub role: u8,
pub name: &'a str,
pub content: &'a str,
pub token_count: u32,
}
fn check_name_len(len: usize) -> Result<(), MsgError> {
if len > MAX_NAME {
Err(MsgError::NameTooLong(len))
} else {
Ok(())
}
}
fn check_content_len(len: usize) -> Result<(), MsgError> {
if len > MAX_CONTENT {
Err(MsgError::ContentTooLong(len))
} else {
Ok(())
}
}
pub fn encoded_len(msg: &AgentMessage) -> usize {
FIXED_PREFIX + msg.name.len() + CONTENT_LEN_LEN + msg.content.len()
}
pub fn encode(msg: &AgentMessage, out: &mut [u8]) -> Result<usize, MsgError> {
check_name_len(msg.name.len())?;
check_content_len(msg.content.len())?;
let need = encoded_len(msg);
if out.len() < need {
return Err(MsgError::BufferTooSmall {
need,
have: out.len(),
});
}
out[0..8].copy_from_slice(&msg.id.to_le_bytes());
out[8] = msg.role;
out[9..13].copy_from_slice(&msg.token_count.to_le_bytes());
out[13..15].copy_from_slice(&(msg.name.len() as u16).to_le_bytes());
let name_end = FIXED_PREFIX + msg.name.len();
out[FIXED_PREFIX..name_end].copy_from_slice(msg.name.as_bytes());
let clen_end = name_end + CONTENT_LEN_LEN;
out[name_end..clen_end].copy_from_slice(&(msg.content.len() as u32).to_le_bytes());
let content_end = clen_end + msg.content.len();
out[clen_end..content_end].copy_from_slice(msg.content.as_bytes());
Ok(need)
}
pub fn decode(buf: &[u8]) -> Result<AgentMessage<'_>, MsgError> {
let fixed = buf.get(0..FIXED_PREFIX).ok_or(MsgError::TooShort)?;
let id = u64::from_le_bytes([
fixed[0], fixed[1], fixed[2], fixed[3], fixed[4], fixed[5], fixed[6], fixed[7],
]);
let role = fixed[8];
let token_count = u32::from_le_bytes([fixed[9], fixed[10], fixed[11], fixed[12]]);
let name_len = u16::from_le_bytes([fixed[13], fixed[14]]) as usize;
check_name_len(name_len)?;
let name_end = FIXED_PREFIX + name_len;
let name_bytes = buf.get(FIXED_PREFIX..name_end).ok_or(MsgError::TooShort)?;
let name = str::from_utf8(name_bytes).map_err(|_| MsgError::BadUtf8)?;
let clen_end = name_end + CONTENT_LEN_LEN;
let clen_bytes = buf.get(name_end..clen_end).ok_or(MsgError::TooShort)?;
let content_len =
u32::from_le_bytes([clen_bytes[0], clen_bytes[1], clen_bytes[2], clen_bytes[3]]) as usize;
check_content_len(content_len)?;
let content_end = clen_end + content_len;
let content_bytes = buf.get(clen_end..content_end).ok_or(MsgError::TooShort)?;
let content = str::from_utf8(content_bytes).map_err(|_| MsgError::BadUtf8)?;
Ok(AgentMessage {
id,
role,
name,
content,
token_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> AgentMessage<'static> {
AgentMessage {
id: 0x0102_0304_0506_0708,
role: 3,
name: "web_search",
content: "find the latest release notes",
token_count: 4242,
}
}
#[test]
fn fixed_prefix_has_its_specified_geometry() {
assert_eq!(FIXED_PREFIX, 15);
assert_eq!(MAX_NAME, 1024);
assert_eq!(MAX_CONTENT, 1024 * 1024);
}
#[test]
fn round_trip_preserves_every_field() {
let msg = sample();
let mut buf = std::vec![0u8; encoded_len(&msg)];
let n = encode(&msg, &mut buf).unwrap();
assert_eq!(n, buf.len());
assert_eq!(decode(&buf).unwrap(), msg);
}
#[test]
fn round_trip_handles_empty_strings_and_multibyte_utf8() {
for (name, content) in [
("", ""),
("x", ""),
("", "y"),
("café", "日本語のテキスト — π ≈ 3.14"),
] {
let msg = AgentMessage {
id: 7,
role: 1,
name,
content,
token_count: 0,
};
let mut buf = std::vec![0u8; encoded_len(&msg)];
encode(&msg, &mut buf).unwrap();
let got = decode(&buf).unwrap();
assert_eq!(got.name, name);
assert_eq!(got.content, content);
}
}
#[test]
fn encodes_the_exact_wire_layout() {
let msg = AgentMessage {
id: 1,
role: 2,
name: "ab",
content: "cd",
token_count: 0x11,
};
let mut buf = std::vec![0u8; encoded_len(&msg)];
encode(&msg, &mut buf).unwrap();
assert_eq!(&buf[0..8], &1u64.to_le_bytes()); assert_eq!(buf[8], 2); assert_eq!(&buf[9..13], &0x11u32.to_le_bytes()); assert_eq!(&buf[13..15], &2u16.to_le_bytes()); assert_eq!(&buf[15..17], b"ab"); assert_eq!(&buf[17..21], &2u32.to_le_bytes()); assert_eq!(&buf[21..23], b"cd"); assert_eq!(encoded_len(&msg), 23);
}
#[test]
fn check_len_helpers_allow_the_cap_and_refuse_one_more() {
assert_eq!(check_name_len(MAX_NAME), Ok(()));
assert_eq!(
check_name_len(MAX_NAME + 1),
Err(MsgError::NameTooLong(MAX_NAME + 1))
);
assert_eq!(check_content_len(MAX_CONTENT), Ok(()));
assert_eq!(
check_content_len(MAX_CONTENT + 1),
Err(MsgError::ContentTooLong(MAX_CONTENT + 1))
);
}
#[test]
fn encode_rejects_a_too_small_buffer_at_the_exact_boundary() {
let msg = sample();
let need = encoded_len(&msg);
let mut short = std::vec![0u8; need - 1];
assert_eq!(
encode(&msg, &mut short),
Err(MsgError::BufferTooSmall {
need,
have: need - 1
})
);
let mut exact = std::vec![0u8; need];
assert_eq!(encode(&msg, &mut exact), Ok(need));
}
#[test]
fn decode_rejects_a_truncated_fixed_prefix() {
for len in 0..FIXED_PREFIX {
assert_eq!(decode(&std::vec![0u8; len]), Err(MsgError::TooShort));
}
}
#[test]
fn decode_rejects_a_name_running_past_the_buffer() {
let mut buf = std::vec![0u8; FIXED_PREFIX];
buf[13..15].copy_from_slice(&5u16.to_le_bytes()); assert_eq!(decode(&buf), Err(MsgError::TooShort));
}
#[test]
fn decode_rejects_content_running_past_the_buffer() {
let msg = AgentMessage {
id: 0,
role: 0,
name: "n",
content: "",
token_count: 0,
};
let mut buf = std::vec![0u8; encoded_len(&msg)];
encode(&msg, &mut buf).unwrap();
let clen_at = FIXED_PREFIX + 1;
buf[clen_at..clen_at + 4].copy_from_slice(&9u32.to_le_bytes());
assert_eq!(decode(&buf), Err(MsgError::TooShort));
}
#[test]
fn decode_rejects_an_over_large_declared_name_len() {
let mut buf = std::vec![0u8; FIXED_PREFIX];
buf[13..15].copy_from_slice(&((MAX_NAME as u16) + 1).to_le_bytes());
assert_eq!(decode(&buf), Err(MsgError::NameTooLong(MAX_NAME + 1)));
}
#[test]
fn decode_rejects_invalid_utf8_in_a_string_field() {
let msg = AgentMessage {
id: 0,
role: 0,
name: "n",
content: "c",
token_count: 0,
};
let mut buf = std::vec![0u8; encoded_len(&msg)];
encode(&msg, &mut buf).unwrap();
buf[FIXED_PREFIX] = 0xFF; assert_eq!(decode(&buf), Err(MsgError::BadUtf8));
}
}