use crate::error::InvalidError;
use crate::query::Value;
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum IdParseError {
#[error("id must be 26 characters, got {got}")]
Length { got: usize },
#[error("id contains invalid character `{0}`")]
Char(char),
#[error("id overflows 128 bits")]
Overflow,
}
macro_rules! wire_id {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(u128);
impl $name {
pub const fn from_u128(value: u128) -> Self {
Self(value)
}
pub const fn as_u128(self) -> u128 {
self.0
}
pub const fn to_bytes(self) -> [u8; 16] {
self.0.to_be_bytes()
}
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
Self(u128::from_be_bytes(bytes))
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded = crockford_encode(self.0);
f.write_str(std::str::from_utf8(&encoded).expect("crockford output is ASCII"))
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self)
}
}
impl FromStr for $name {
type Err = IdParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
crockford_decode(s).map(Self)
}
}
impl From<u128> for $name {
fn from(value: u128) -> Self {
Self(value)
}
}
impl From<$name> for u128 {
fn from(value: $name) -> u128 {
value.0
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.to_bytes())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct BytesVisitor;
impl<'de> Visitor<'de> for BytesVisitor {
type Value = $name;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("16 big-endian id bytes")
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
let bytes: [u8; 16] = v
.try_into()
.map_err(|_| E::invalid_length(v.len(), &self))?;
Ok($name::from_bytes(bytes))
}
}
deserializer.deserialize_bytes(BytesVisitor)
}
}
};
}
pub(crate) use wire_id;
wire_id!(
RecordId
);
wire_id!(
ConversationId
);
wire_id!(
CorrelationId
);
wire_id!(
ChannelId
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LogPosition {
pub stream_id: u32,
pub topic_id: u32,
pub partition_id: u32,
pub offset: u64,
}
const LOG_POSITION_BYTES: usize = 20;
impl LogPosition {
pub const fn new(stream_id: u32, topic_id: u32, partition_id: u32, offset: u64) -> Self {
Self {
stream_id,
topic_id,
partition_id,
offset,
}
}
pub fn to_bytes(self) -> [u8; LOG_POSITION_BYTES] {
let mut out = [0u8; LOG_POSITION_BYTES];
out[0..4].copy_from_slice(&self.stream_id.to_be_bytes());
out[4..8].copy_from_slice(&self.topic_id.to_be_bytes());
out[8..12].copy_from_slice(&self.partition_id.to_be_bytes());
out[12..20].copy_from_slice(&self.offset.to_be_bytes());
out
}
pub fn from_bytes(bytes: [u8; LOG_POSITION_BYTES]) -> Self {
let u32_at = |start: usize| {
u32::from_be_bytes(bytes[start..start + 4].try_into().expect("4-byte slice"))
};
Self {
stream_id: u32_at(0),
topic_id: u32_at(4),
partition_id: u32_at(8),
offset: u64::from_be_bytes(bytes[12..20].try_into().expect("8-byte slice")),
}
}
}
impl Serialize for LogPosition {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.to_bytes())
}
}
impl<'de> Deserialize<'de> for LogPosition {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct LocatorVisitor;
impl<'de> Visitor<'de> for LocatorVisitor {
type Value = LogPosition;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("20 packed locator bytes")
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
let bytes: [u8; LOG_POSITION_BYTES] = v
.try_into()
.map_err(|_| E::invalid_length(v.len(), &self))?;
Ok(LogPosition::from_bytes(bytes))
}
}
deserializer.deserialize_bytes(LocatorVisitor)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct IdempotencyKey(String);
impl IdempotencyKey {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for IdempotencyKey {
type Err = InvalidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.to_owned().try_into()
}
}
impl TryFrom<String> for IdempotencyKey {
type Error = InvalidError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(InvalidError::new("idempotency key must not be empty"));
}
if value.len() > crate::limits::MAX_IDEMPOTENCY_KEY_BYTES {
return Err(InvalidError::new(format!(
"idempotency key is {}B, exceeds cap {}B",
value.len(),
crate::limits::MAX_IDEMPOTENCY_KEY_BYTES
)));
}
Ok(Self(value))
}
}
impl From<IdempotencyKey> for String {
fn from(value: IdempotencyKey) -> Self {
value.0
}
}
impl fmt::Display for IdempotencyKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct AgentId(String);
impl AgentId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for AgentId {
type Err = InvalidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.to_owned().try_into()
}
}
impl TryFrom<String> for AgentId {
type Error = InvalidError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(InvalidError::new("agent id must not be empty"));
}
if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
return Err(InvalidError::new(format!(
"agent id is {}B, exceeds cap {}B",
value.len(),
crate::limits::MAX_AGENT_STRING_BYTES
)));
}
if let Some(c) = value.chars().find(|c| c.is_control()) {
return Err(InvalidError::new(format!(
"agent id must not contain control characters (found {c:?})"
)));
}
Ok(Self(value))
}
}
impl TryFrom<&str> for AgentId {
type Error = InvalidError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.to_owned().try_into()
}
}
impl From<AgentId> for String {
fn from(value: AgentId) -> Self {
value.0
}
}
impl fmt::Display for AgentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
strum::Display,
strum::EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum AgentKind {
Command,
Response,
Event,
Chunk,
Status,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Canceled,
Failed,
Rejected,
AuthRequired,
Unknown,
Unrecognized(u8),
}
impl TaskState {
pub const fn code(self) -> u8 {
match self {
TaskState::Submitted => 1,
TaskState::Working => 2,
TaskState::InputRequired => 3,
TaskState::Completed => 4,
TaskState::Canceled => 5,
TaskState::Failed => 6,
TaskState::Rejected => 7,
TaskState::AuthRequired => 8,
TaskState::Unknown => 9,
TaskState::Unrecognized(code) => code,
}
}
pub const fn from_code(code: u8) -> Self {
match code {
1 => TaskState::Submitted,
2 => TaskState::Working,
3 => TaskState::InputRequired,
4 => TaskState::Completed,
5 => TaskState::Canceled,
6 => TaskState::Failed,
7 => TaskState::Rejected,
8 => TaskState::AuthRequired,
9 => TaskState::Unknown,
other => TaskState::Unrecognized(other),
}
}
pub const fn is_terminal(self) -> bool {
matches!(
self,
TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
)
}
}
impl From<u8> for TaskState {
fn from(code: u8) -> Self {
Self::from_code(code)
}
}
impl From<TaskState> for u8 {
fn from(state: TaskState) -> u8 {
state.code()
}
}
impl fmt::Display for TaskState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TaskState::Submitted => f.write_str("submitted"),
TaskState::Working => f.write_str("working"),
TaskState::InputRequired => f.write_str("input-required"),
TaskState::Completed => f.write_str("completed"),
TaskState::Canceled => f.write_str("canceled"),
TaskState::Failed => f.write_str("failed"),
TaskState::Rejected => f.write_str("rejected"),
TaskState::AuthRequired => f.write_str("auth-required"),
TaskState::Unknown => f.write_str("unknown"),
TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
}
}
}
impl FromStr for TaskState {
type Err = InvalidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"submitted" => TaskState::Submitted,
"working" => TaskState::Working,
"input-required" => TaskState::InputRequired,
"completed" => TaskState::Completed,
"canceled" => TaskState::Canceled,
"failed" => TaskState::Failed,
"rejected" => TaskState::Rejected,
"auth-required" => TaskState::AuthRequired,
"unknown" => TaskState::Unknown,
other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenUsage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_output_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
pub enum AgentErrorCode {
InvalidRequest,
Unauthorized,
Unsupported,
DeadlineExceeded,
Cancelled,
ToolFailure,
Internal,
Unrecognized(u8),
}
impl AgentErrorCode {
pub const fn code(self) -> u8 {
match self {
AgentErrorCode::InvalidRequest => 1,
AgentErrorCode::Unauthorized => 2,
AgentErrorCode::Unsupported => 3,
AgentErrorCode::DeadlineExceeded => 4,
AgentErrorCode::Cancelled => 5,
AgentErrorCode::ToolFailure => 6,
AgentErrorCode::Internal => 7,
AgentErrorCode::Unrecognized(code) => code,
}
}
pub const fn from_code(code: u8) -> Self {
match code {
1 => AgentErrorCode::InvalidRequest,
2 => AgentErrorCode::Unauthorized,
3 => AgentErrorCode::Unsupported,
4 => AgentErrorCode::DeadlineExceeded,
5 => AgentErrorCode::Cancelled,
6 => AgentErrorCode::ToolFailure,
7 => AgentErrorCode::Internal,
other => AgentErrorCode::Unrecognized(other),
}
}
}
impl From<u8> for AgentErrorCode {
fn from(code: u8) -> Self {
Self::from_code(code)
}
}
impl From<AgentErrorCode> for u8 {
fn from(code: AgentErrorCode) -> u8 {
code.code()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AgentErrorBody {
pub code: AgentErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default)]
pub retryable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<BTreeMap<String, Value>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
pub enum DeadLetterReason {
RetryExhausted,
Rejected,
DecodeFailed,
DeadlineExceeded,
Unrecognized(u8),
}
impl DeadLetterReason {
pub const fn code(self) -> u8 {
match self {
DeadLetterReason::RetryExhausted => 1,
DeadLetterReason::Rejected => 2,
DeadLetterReason::DecodeFailed => 3,
DeadLetterReason::DeadlineExceeded => 4,
DeadLetterReason::Unrecognized(code) => code,
}
}
pub const fn from_code(code: u8) -> Self {
match code {
1 => DeadLetterReason::RetryExhausted,
2 => DeadLetterReason::Rejected,
3 => DeadLetterReason::DecodeFailed,
4 => DeadLetterReason::DeadlineExceeded,
other => DeadLetterReason::Unrecognized(other),
}
}
}
impl From<u8> for DeadLetterReason {
fn from(code: u8) -> Self {
Self::from_code(code)
}
}
impl From<DeadLetterReason> for u8 {
fn from(reason: DeadLetterReason) -> u8 {
reason.code()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AgentDeadLetter {
pub source: LogPosition,
pub reason: DeadLetterReason,
pub attempts: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(with = "crate::encoding::bin_bytes")]
pub payload: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
pub enum Health {
Healthy,
Degraded,
Unavailable,
Unrecognized(u8),
}
impl Health {
pub const fn code(self) -> u8 {
match self {
Health::Healthy => 1,
Health::Degraded => 2,
Health::Unavailable => 3,
Health::Unrecognized(code) => code,
}
}
pub const fn from_code(code: u8) -> Self {
match code {
1 => Health::Healthy,
2 => Health::Degraded,
3 => Health::Unavailable,
other => Health::Unrecognized(other),
}
}
}
impl From<u8> for Health {
fn from(code: u8) -> Self {
Self::from_code(code)
}
}
impl From<Health> for u8 {
fn from(health: Health) -> u8 {
health.code()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentRef {
ContentType(crate::content::ContentType),
SchemaId(String),
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CapabilityDescriptor {
pub skill_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<ContentRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<ContentRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_class: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub latency_class: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_concurrency: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub health: Option<Health>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub load: Option<u16>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct AgentCard {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<CapabilityDescriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_micros: Option<u64>,
}
impl AgentCard {
pub fn validate(&self) -> Result<(), ValidateError> {
cap_str(self.name.as_deref(), "name")?;
cap_str(self.version.as_deref(), "version")?;
if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
return Err(ValidateError::TooLarge {
field: "capabilities",
size: self.capabilities.len(),
cap: crate::limits::MAX_CARD_CAPABILITIES,
});
}
for capability in &self.capabilities {
cap_str(Some(&capability.skill_id), "capability skill_id")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentPresence {
pub v: u32,
pub agent: AgentId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inbox: Option<String>,
}
impl AgentPresence {
pub fn new(agent: AgentId) -> Self {
Self {
v: crate::codes::PRESENCE_OP_VERSION,
agent,
inbox: None,
}
}
pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
self.inbox = Some(inbox.into());
self
}
pub fn validate(&self) -> Result<(), ValidateError> {
cap_str(self.inbox.as_deref(), "inbox")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BodyRef {
pub reference: String,
pub size_bytes: u64,
#[serde(with = "crate::encoding::bin_bytes")]
pub sha256: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encryption: Option<u8>,
}
const SHA256_BYTES: usize = 32;
impl BodyRef {
pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
Self {
reference: reference.into(),
size_bytes,
sha256: sha256.to_vec(),
encryption: None,
}
}
pub fn validate(&self) -> Result<(), ValidateError> {
if self.reference.is_empty() {
return Err(ValidateError::Invalid {
field: "reference",
reason: "reference must not be empty".to_owned(),
});
}
if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
return Err(ValidateError::TooLarge {
field: "reference",
size: self.reference.len(),
cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
});
}
if self.sha256.len() != SHA256_BYTES {
return Err(ValidateError::Invalid {
field: "sha256",
reason: format!(
"digest must be {SHA256_BYTES} bytes, got {}",
self.sha256.len()
),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature {
pub scheme: u8,
#[serde(with = "crate::encoding::bin_bytes")]
pub key_id: Vec<u8>,
#[serde(with = "crate::encoding::bin_bytes")]
pub bytes: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<SignatureContext>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignatureContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_type: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_version: Option<u32>,
}
pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
const ED25519_KEY_ID_BYTES: usize = 8;
const ED25519_SIGNATURE_BYTES: usize = 64;
impl Signature {
pub fn validate(&self) -> Result<(), ValidateError> {
if self.scheme != SIGNATURE_SCHEME_ED25519 {
return Ok(());
}
if self.key_id.len() != ED25519_KEY_ID_BYTES {
return Err(ValidateError::Invalid {
field: "key_id",
reason: format!(
"Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
self.key_id.len()
),
});
}
if self.bytes.len() != ED25519_SIGNATURE_BYTES {
return Err(ValidateError::Invalid {
field: "bytes",
reason: format!(
"Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
self.bytes.len()
),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AgentEnvelope {
pub kind: AgentKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub record: Option<RecordId>,
pub conversation: ConversationId,
pub source: AgentId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<RecordId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause_at: Option<LogPosition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<CorrelationId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<ChannelId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<IdempotencyKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deadline_micros: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sequence: Option<u64>,
#[serde(default, skip_serializing_if = "is_false")]
pub last: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_state: Option<TaskState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<BTreeMap<String, Value>>,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub must_understand: u64,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
with = "crate::encoding::bin_bytes"
)]
pub body: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<Signature>,
}
fn is_false(value: &bool) -> bool {
!*value
}
fn is_zero_u64(value: &u64) -> bool {
*value == 0
}
pub mod features {
pub const NONE: u64 = 0;
}
impl AgentEnvelope {
fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
Self {
kind,
record: None,
conversation,
source,
target: None,
cause: None,
cause_at: None,
correlation: None,
channel: None,
idempotency_key: None,
deadline_micros: None,
sequence: None,
last: false,
finish_reason: None,
task_state: None,
operation: None,
tool: None,
usage: None,
metadata: None,
must_understand: 0,
body: Vec::new(),
signature: None,
}
}
#[must_use]
pub fn requiring(mut self, bits: u64) -> Self {
self.must_understand = bits;
self
}
pub fn unmet_requirements(&self, understood: u64) -> u64 {
self.must_understand & !understood
}
pub fn command(
record: RecordId,
conversation: ConversationId,
source: AgentId,
correlation: CorrelationId,
body: Vec<u8>,
) -> Self {
let mut envelope = Self::base(AgentKind::Command, conversation, source);
envelope.record = Some(record);
envelope.correlation = Some(correlation);
envelope.body = body;
envelope
}
pub fn response(
record: RecordId,
conversation: ConversationId,
source: AgentId,
correlation: CorrelationId,
body: Vec<u8>,
) -> Self {
let mut envelope = Self::base(AgentKind::Response, conversation, source);
envelope.record = Some(record);
envelope.correlation = Some(correlation);
envelope.body = body;
envelope
}
pub fn event(
record: RecordId,
conversation: ConversationId,
source: AgentId,
body: Vec<u8>,
) -> Self {
let mut envelope = Self::base(AgentKind::Event, conversation, source);
envelope.record = Some(record);
envelope.body = body;
envelope
}
pub fn chunk(
conversation: ConversationId,
source: AgentId,
correlation: CorrelationId,
channel: ChannelId,
sequence: u64,
body: Vec<u8>,
) -> Self {
let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
envelope.correlation = Some(correlation);
envelope.channel = Some(channel);
envelope.sequence = Some(sequence);
envelope.body = body;
envelope
}
pub fn status(
record: RecordId,
conversation: ConversationId,
source: AgentId,
operation: impl Into<String>,
) -> Self {
let mut envelope = Self::base(AgentKind::Status, conversation, source);
envelope.record = Some(record);
envelope.operation = Some(operation.into());
envelope
}
pub fn error(
record: RecordId,
conversation: ConversationId,
source: AgentId,
correlation: CorrelationId,
body: Vec<u8>,
) -> Self {
let mut envelope = Self::base(AgentKind::Error, conversation, source);
envelope.record = Some(record);
envelope.correlation = Some(correlation);
envelope.body = body;
envelope
}
pub fn with_target(mut self, target: AgentId) -> Self {
self.target = Some(target);
self
}
pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
self.cause = Some(cause);
self.cause_at = cause_at;
self
}
pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
self.correlation = Some(correlation);
self
}
pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
self.idempotency_key = Some(key);
self
}
pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
self.deadline_micros = Some(deadline_micros);
self
}
pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
self.last = true;
self.finish_reason = Some(finish_reason.into());
self
}
pub fn with_task_state(mut self, state: TaskState) -> Self {
self.task_state = Some(state);
self
}
pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
self.operation = Some(operation.into());
self
}
pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
self.tool = Some(tool.into());
self
}
pub fn with_usage(mut self, usage: TokenUsage) -> Self {
self.usage = Some(usage);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.metadata
.get_or_insert_with(BTreeMap::new)
.insert(key.into(), value.into());
self
}
pub fn with_signature(mut self, signature: Signature) -> Self {
self.signature = Some(signature);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ValidateError {
#[error("{kind} requires `{field}`")]
Missing {
kind: AgentKind,
field: &'static str,
},
#[error("`{field}` is invalid on {kind}")]
Forbidden {
kind: AgentKind,
field: &'static str,
},
#[error("`{field}` is {size}B, exceeds cap {cap}B")]
TooLarge {
field: &'static str,
size: usize,
cap: usize,
},
#[error("`{field}`: {reason}")]
Invalid { field: &'static str, reason: String },
}
pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
use AgentKind::*;
let kind = envelope.kind;
let require = |present: bool, field: &'static str| {
if present {
Ok(())
} else {
Err(ValidateError::Missing { kind, field })
}
};
let forbid = |absent: bool, field: &'static str| {
if absent {
Ok(())
} else {
Err(ValidateError::Forbidden { kind, field })
}
};
if kind != Chunk {
require(envelope.record.is_some(), "record")?;
}
match kind {
Command | Response | Chunk | Error => {
require(envelope.correlation.is_some(), "correlation")?
}
Status => {
if envelope.operation.as_deref() == Some(OPERATION_TASK) {
require(envelope.correlation.is_some(), "correlation")?;
}
}
Event => {}
}
match kind {
Chunk => {
require(envelope.channel.is_some(), "channel")?;
require(envelope.sequence.is_some(), "sequence")?;
}
Error => {
if envelope.sequence.is_some() && envelope.channel.is_none() {
return Err(ValidateError::Invalid {
field: "sequence",
reason: "sequence requires channel".to_owned(),
});
}
}
_ => {
forbid(envelope.channel.is_none(), "channel")?;
forbid(envelope.sequence.is_none(), "sequence")?;
}
}
if envelope.last && !matches!(kind, Chunk | Status) {
return Err(ValidateError::Forbidden {
kind,
field: "last",
});
}
match kind {
Response => {}
Chunk => {
if envelope.finish_reason.is_some() && !envelope.last {
return Err(ValidateError::Invalid {
field: "finish_reason",
reason: "finish_reason rides only the terminal chunk".to_owned(),
});
}
}
_ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
}
if matches!(kind, Chunk | Status | Error) {
forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
}
if matches!(kind, Response | Event | Status | Error) {
forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
}
if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
return Err(ValidateError::Invalid {
field: "deadline_micros",
reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
});
}
match kind {
Status => {
if envelope.operation.as_deref() == Some(OPERATION_TASK) {
require(envelope.task_state.is_some(), "task_state")?;
}
}
Response | Error => {}
_ => forbid(envelope.task_state.is_none(), "task_state")?,
}
match kind {
Status => {
require(envelope.operation.is_some(), "operation")?;
if let Some(operation) = envelope.operation.as_deref()
&& !matches!(
operation,
OPERATION_TASK
| OPERATION_CARD
| OPERATION_PROGRESS
| OPERATION_QUARANTINE
| OPERATION_UNQUARANTINE
)
{
return Err(ValidateError::Invalid {
field: "operation",
reason: format!(
"status operation must be `{OPERATION_TASK}`, `{OPERATION_CARD}`, \
`{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
`{OPERATION_UNQUARANTINE}`, got `{operation}`"
),
});
}
}
Chunk => {
if envelope.sequence == Some(0) {
require(envelope.operation.is_some(), "operation")?;
}
if let Some(operation) = envelope.operation.as_deref() {
if envelope.sequence != Some(0) {
return Err(ValidateError::Invalid {
field: "operation",
reason: "the stream purpose rides the opening chunk (sequence 0)"
.to_owned(),
});
}
if !matches!(
operation,
OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
) {
return Err(ValidateError::Invalid {
field: "operation",
reason: format!(
"chunk-stream purpose must be `{OPERATION_CHAT}`, \
`{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
got `{operation}`"
),
});
}
}
}
Command | Response | Event | Error => {}
}
if kind == Status {
forbid(envelope.tool.is_none(), "tool")?;
}
match kind {
Command => forbid(envelope.usage.is_none(), "usage")?,
Chunk if envelope.usage.is_some() && !envelope.last => {
return Err(ValidateError::Invalid {
field: "usage",
reason: "whole-stream accounting rides the terminal chunk".to_owned(),
});
}
_ => {}
}
match kind {
Status => {}
Chunk => {
if envelope.body.is_empty() && !envelope.last {
return Err(ValidateError::Missing {
kind,
field: "body",
});
}
}
_ => require(!envelope.body.is_empty(), "body")?,
}
cap_str(envelope.operation.as_deref(), "operation")?;
cap_str(envelope.tool.as_deref(), "tool")?;
cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
if let Some(metadata) = &envelope.metadata {
if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
return Err(ValidateError::TooLarge {
field: "metadata",
size: metadata.len(),
cap: crate::limits::MAX_METADATA_ENTRIES,
});
}
let mut total = 0usize;
for (key, value) in metadata {
if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
return Err(ValidateError::TooLarge {
field: "metadata key",
size: key.len(),
cap: crate::limits::MAX_METADATA_KEY_BYTES,
});
}
let value_size = value_size(value);
if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
return Err(ValidateError::TooLarge {
field: "metadata value",
size: value_size,
cap: crate::limits::MAX_METADATA_VALUE_BYTES,
});
}
total += key.len() + value_size;
}
if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
return Err(ValidateError::TooLarge {
field: "metadata",
size: total,
cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
});
}
}
if let Some(signature) = &envelope.signature {
signature.validate()?;
}
Ok(())
}
pub const OPERATION_TASK: &str = "task";
pub const OPERATION_CARD: &str = "card";
pub const OPERATION_PROGRESS: &str = "progress";
pub const OPERATION_QUARANTINE: &str = "quarantine";
pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
pub const OPERATION_CHAT: &str = "chat";
pub const OPERATION_REASONING: &str = "reasoning";
pub const OPERATION_TOOL_ARGS: &str = "tool_args";
pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
pub const OPERATION_STATE_DELTA: &str = "state_delta";
pub const METADATA_ROLE: &str = "role";
pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
pub const METADATA_RUN: &str = "run";
pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
pub const METADATA_PURPOSE: &str = "purpose";
pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
pub const METADATA_TASK_CONTEXT: &str = "task_context";
pub const METADATA_SESSION_INTENT: &str = "session_intent";
const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
let mut out = [0u8; 26];
let mut v = value;
for slot in out.iter_mut().rev() {
*slot = CROCKFORD[(v & 0x1f) as usize];
v >>= 5;
}
out
}
pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
let bytes = s.as_bytes();
if bytes.len() != 26 {
return Err(IdParseError::Length { got: bytes.len() });
}
let mut value: u128 = 0;
for (i, byte) in bytes.iter().enumerate() {
let digit = CROCKFORD
.iter()
.position(|c| *c == byte.to_ascii_uppercase())
.ok_or(IdParseError::Char(*byte as char))?;
if i == 0 && digit > 7 {
return Err(IdParseError::Overflow);
}
value = (value << 5) | digit as u128;
}
Ok(value)
}
fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
if let Some(value) = value
&& value.len() > crate::limits::MAX_AGENT_STRING_BYTES
{
return Err(ValidateError::TooLarge {
field,
size: value.len(),
cap: crate::limits::MAX_AGENT_STRING_BYTES,
});
}
Ok(())
}
fn value_size(value: &Value) -> usize {
match value {
Value::Str(s) => s.len(),
Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
_ => 9,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
let text = id.to_string();
assert_eq!(text.len(), 26);
assert_eq!(text.parse::<RecordId>().expect("parses"), id);
assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
assert_eq!(
RecordId::from_u128(0).to_string(),
"00000000000000000000000000"
);
assert_eq!(
RecordId::from_u128(u128::MAX).to_string(),
"7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
);
}
#[test]
fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
assert_eq!(
"short".parse::<RecordId>(),
Err(IdParseError::Length { got: 5 })
);
assert_eq!(
"8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
Err(IdParseError::Overflow)
);
assert!(matches!(
"UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
Err(IdParseError::Char('U'))
));
}
#[test]
fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
assert_eq!(
s.parse::<AgentId>()
.expect("a printable agent id is valid")
.as_str(),
s
);
}
assert!("".parse::<AgentId>().is_err());
assert!("bad\nid".parse::<AgentId>().is_err());
}
#[test]
fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
let expected = [
(TaskState::Submitted, 1u8),
(TaskState::Working, 2),
(TaskState::InputRequired, 3),
(TaskState::Completed, 4),
(TaskState::Canceled, 5),
(TaskState::Failed, 6),
(TaskState::Rejected, 7),
(TaskState::AuthRequired, 8),
(TaskState::Unknown, 9),
];
for (state, code) in expected {
assert_eq!(state.code(), code);
assert_eq!(TaskState::from_code(code), state);
}
let future = TaskState::from_code(42);
assert_eq!(future, TaskState::Unrecognized(42));
assert_eq!(future.code(), 42);
assert!(!future.is_terminal());
assert!(TaskState::Completed.is_terminal());
assert!(!TaskState::Working.is_terminal());
}
#[test]
fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
assert_eq!(TaskState::InputRequired.to_string(), "input-required");
assert_eq!(
"auth-required".parse::<TaskState>().expect("parses"),
TaskState::AuthRequired
);
assert!("nope".parse::<TaskState>().is_err());
}
#[test]
fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
assert_eq!(AgentErrorCode::Internal.code(), 7);
assert_eq!(
AgentErrorCode::from_code(99),
AgentErrorCode::Unrecognized(99)
);
assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
assert_eq!(
DeadLetterReason::from_code(77),
DeadLetterReason::Unrecognized(77)
);
}
#[test]
fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
for code in 0u8..=u8::MAX {
assert_eq!(TaskState::from_code(code).code(), code);
assert_eq!(AgentErrorCode::from_code(code).code(), code);
assert_eq!(DeadLetterReason::from_code(code).code(), code);
}
}
#[test]
fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
assert!("order-123-attempt-2".parse::<IdempotencyKey>().is_ok());
assert!("".parse::<IdempotencyKey>().is_err());
assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
}
#[test]
fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
let (record, conversation, source, correlation) = ids();
let command =
AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
validate(&command).expect("a well-formed command validates");
let mut missing = command.clone();
missing.correlation = None;
assert_eq!(
validate(&missing),
Err(ValidateError::Missing {
kind: AgentKind::Command,
field: "correlation"
})
);
let with_usage = command.clone().with_usage(TokenUsage::default());
assert!(matches!(
validate(&with_usage),
Err(ValidateError::Forbidden { field: "usage", .. })
));
let mut with_channel = command;
with_channel.channel = Some(ChannelId::from_u128(1));
assert!(matches!(
validate(&with_channel),
Err(ValidateError::Forbidden {
field: "channel",
..
})
));
}
#[test]
fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
let (_, conversation, source, correlation) = ids();
let channel = ChannelId::from_u128(23);
let chunk = AgentEnvelope::chunk(
conversation,
source.clone(),
correlation,
channel,
0,
b"tok".to_vec(),
)
.with_operation(OPERATION_CHAT);
validate(&chunk).expect("a stream chunk validates");
let mut undeclared = chunk.clone();
undeclared.operation = None;
assert!(matches!(
validate(&undeclared),
Err(ValidateError::Missing {
field: "operation",
..
})
));
let mut off_vocabulary = chunk.clone();
off_vocabulary.operation = Some("telemetry".to_owned());
assert!(matches!(
validate(&off_vocabulary),
Err(ValidateError::Invalid {
field: "operation",
..
})
));
let redeclared = AgentEnvelope::chunk(
conversation,
source.clone(),
correlation,
channel,
3,
b"tok".to_vec(),
)
.with_operation(OPERATION_REASONING);
assert!(matches!(
validate(&redeclared),
Err(ValidateError::Invalid {
field: "operation",
..
})
));
let terminal = AgentEnvelope::chunk(
conversation,
source.clone(),
correlation,
channel,
41,
Vec::new(),
)
.terminal("stop")
.with_usage(TokenUsage {
input_tokens: 100,
output_tokens: 42,
..Default::default()
});
validate(&terminal).expect("a terminal chunk validates");
let mut early_finish = chunk.clone();
early_finish.finish_reason = Some("stop".to_owned());
assert!(matches!(
validate(&early_finish),
Err(ValidateError::Invalid {
field: "finish_reason",
..
})
));
let empty = AgentEnvelope::chunk(
conversation,
source.clone(),
correlation,
channel,
1,
Vec::new(),
);
assert!(matches!(
validate(&empty),
Err(ValidateError::Missing { field: "body", .. })
));
let mut keyed = chunk.clone();
keyed.idempotency_key = Some("k".parse().expect("valid key"));
assert!(matches!(
validate(&keyed),
Err(ValidateError::Forbidden {
field: "idempotency_key",
..
})
));
let opening = chunk.with_deadline_micros(1);
validate(&opening).expect("an opening chunk may declare the bound");
let late = AgentEnvelope::chunk(
conversation,
source.clone(),
correlation,
channel,
5,
b"tok".to_vec(),
)
.with_deadline_micros(1);
assert!(matches!(
validate(&late),
Err(ValidateError::Invalid {
field: "deadline_micros",
..
})
));
}
#[test]
fn given_status_signals_when_validated_then_task_updates_should_require_state() {
let (record, conversation, source, correlation) = ids();
let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
validate(&card).expect("a card validates");
let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
assert!(matches!(
validate(&bare_task),
Err(ValidateError::Missing {
field: "correlation",
..
})
));
let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
.with_correlation(correlation)
.with_task_state(TaskState::Working);
validate(&task).expect("a task update validates");
let off_vocabulary =
AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
assert!(matches!(
validate(&off_vocabulary),
Err(ValidateError::Invalid {
field: "operation",
..
})
));
}
#[test]
fn given_an_error_when_validated_then_last_should_be_forbidden() {
let (record, conversation, source, correlation) = ids();
let error =
AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
validate(&error).expect("an error validates");
let mut flagged = error.clone();
flagged.last = true;
assert!(matches!(
validate(&flagged),
Err(ValidateError::Forbidden { field: "last", .. })
));
let mut dangling = error;
dangling.sequence = Some(3);
assert!(matches!(
validate(&dangling),
Err(ValidateError::Invalid {
field: "sequence",
..
})
));
}
fn descriptor(skill_id: &str) -> CapabilityDescriptor {
CapabilityDescriptor {
skill_id: skill_id.to_owned(),
input: None,
output: None,
cost_class: None,
latency_class: None,
max_concurrency: None,
health: None,
load: None,
}
}
#[test]
fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
let card = AgentCard {
name: Some("trip-planner".to_owned()),
version: Some("1.4.2".to_owned()),
capabilities: vec![
CapabilityDescriptor {
skill_id: "chat".to_owned(),
input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
cost_class: Some(2),
latency_class: Some(1),
max_concurrency: Some(8),
health: Some(Health::Healthy),
load: Some(250),
},
descriptor("search_flights"),
],
ttl_micros: Some(30_000_000),
};
card.validate().expect("a well-formed card validates");
let mut crowded = card.clone();
crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
assert!(matches!(
crowded.validate(),
Err(ValidateError::TooLarge {
field: "capabilities",
..
})
));
let mut oversized = card;
oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
assert!(matches!(
oversized.validate(),
Err(ValidateError::TooLarge { field: "name", .. })
));
}
#[cfg(feature = "cbor")]
#[test]
fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
{
use crate::framing::{decode_named, encode_named};
let reference = ContentRef::SchemaId("json".to_owned());
let bytes = encode_named(&reference).expect("encodes");
let back: ContentRef = decode_named(&bytes).expect("decodes");
assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
let ct = ContentRef::ContentType(crate::content::ContentType::Json);
let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
assert_eq!(back, ct);
}
#[test]
fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
let valid = Signature {
scheme: SIGNATURE_SCHEME_ED25519,
key_id: vec![1u8; 8],
bytes: vec![2u8; 64],
context: None,
};
valid
.validate()
.expect("a well-formed Ed25519 signature validates");
let mut short_key = valid.clone();
short_key.key_id = vec![1u8; 4];
assert!(matches!(
short_key.validate(),
Err(ValidateError::Invalid {
field: "key_id",
..
})
));
let mut short_signature = valid.clone();
short_signature.bytes = vec![2u8; 32];
assert!(matches!(
short_signature.validate(),
Err(ValidateError::Invalid { field: "bytes", .. })
));
let future = Signature {
scheme: 42,
key_id: vec![1u8; 3],
bytes: vec![2u8; 99],
context: None,
};
future.validate().expect("an unknown scheme flows through");
}
#[test]
fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
valid.validate().expect("a well-formed reference validates");
let mut empty = valid.clone();
empty.reference = String::new();
assert!(matches!(
empty.validate(),
Err(ValidateError::Invalid {
field: "reference",
..
})
));
let mut oversized = valid.clone();
oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
assert!(matches!(
oversized.validate(),
Err(ValidateError::TooLarge {
field: "reference",
..
})
));
let mut truncated = valid;
truncated.sha256 = vec![7u8; 16];
assert!(matches!(
truncated.validate(),
Err(ValidateError::Invalid {
field: "sha256",
..
})
));
}
#[test]
fn given_oversized_metadata_when_validated_then_should_reject() {
let (record, conversation, source, correlation) = ids();
let mut command =
AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
command = command.with_metadata(format!("k{i}"), i as i64);
}
assert!(matches!(
validate(&command),
Err(ValidateError::TooLarge {
field: "metadata",
..
})
));
}
fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
(
RecordId::from_u128(7),
ConversationId::from_u128(11),
"test-agent".parse().expect("valid agent id"),
CorrelationId::from_u128(17),
)
}
}
#[cfg(all(test, feature = "cbor"))]
mod wire_tests {
use super::*;
use crate::framing::{decode_named, encode_named};
#[test]
fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
let mut valid = vec![0x40 | 20];
valid.extend_from_slice(&[0u8; 20]);
decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
for bad_len in [0u8, 19, 21, 23] {
let mut bytes = vec![0x40 | bad_len];
bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
assert!(
decode_named::<LogPosition>(&bytes).is_err(),
"a {bad_len}-byte locator must error, not panic"
);
}
}
#[test]
fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
let bytes = encode_named(&pos).expect("encodes");
let back: LogPosition = decode_named(&bytes).expect("decodes");
assert_eq!(back, pos);
}
#[test]
fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
let envelope = AgentEnvelope::command(
RecordId::from_u128(1),
ConversationId::from_u128(2),
"source-agent".parse().expect("valid agent id"),
CorrelationId::from_u128(4),
b"payload".to_vec(),
)
.with_target("target-agent".parse().expect("valid agent id"))
.with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
.with_idempotency_key("order-1".parse().expect("valid key"))
.with_deadline_micros(1_700_000_000_000_000)
.with_operation("chat")
.with_tool("search")
.with_metadata("customer_tier", "gold");
let bytes = encode_named(&envelope).expect("encodes");
let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
assert_eq!(back, envelope);
}
#[test]
fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
let envelope = AgentEnvelope::event(
RecordId::from_u128(1),
ConversationId::from_u128(2),
"source-agent".parse().expect("valid agent id"),
b"e".to_vec(),
)
.requiring(0b101);
let bytes = encode_named(&envelope).expect("encodes");
let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
assert_eq!(back.must_understand, 0b101);
assert_eq!(back.unmet_requirements(0b001), 0b100);
assert_eq!(back.unmet_requirements(0b111), 0);
let plain = AgentEnvelope::event(
RecordId::from_u128(1),
ConversationId::from_u128(2),
"source-agent".parse().expect("valid agent id"),
b"e".to_vec(),
);
assert_eq!(plain.unmet_requirements(features::NONE), 0);
let json = serde_json::to_string(&plain).expect("json");
assert!(
!json.contains("must_understand"),
"zero marker must be omitted: {json}"
);
}
#[test]
fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
let envelope = AgentEnvelope::event(
RecordId::from_u128(1),
ConversationId::from_u128(2),
"source-agent".parse().expect("valid agent id"),
b"e".to_vec(),
);
let bytes = encode_named(&envelope).expect("encodes");
assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
assert!(!back.last);
assert!(back.metadata.is_none());
}
#[test]
fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
assert_eq!(bytes.len(), 17);
assert_eq!(bytes[0], 0x50);
assert_eq!(bytes[16], 0x02);
assert_eq!(bytes[15], 0x01);
}
#[test]
fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
let bytes = encode_named(&TaskState::Completed).expect("encodes");
assert_eq!(bytes, vec![4]);
let unknown = encode_named(&42u8).expect("encodes a raw code");
let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
assert_eq!(back, TaskState::Unrecognized(42));
}
#[test]
fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
let body = AgentErrorBody {
code: AgentErrorCode::ToolFailure,
message: Some("search timed out".to_owned()),
retryable: true,
detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
};
let bytes = encode_named(&body).expect("encodes");
let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
assert_eq!(back, body);
}
#[test]
fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
let bytes = encode_named(&capsule).expect("encodes");
let back: BodyRef = decode_named(&bytes).expect("decodes");
assert_eq!(back, capsule);
assert!(back.encryption.is_none(), "absent encryption costs nothing");
back.validate().expect("decoded capsule validates");
}
#[test]
fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
let inner = AgentEnvelope::command(
RecordId::from_u128(9),
ConversationId::from_u128(8),
"source-agent".parse().expect("valid agent id"),
CorrelationId::from_u128(6),
b"poison".to_vec(),
);
let payload = encode_named(&inner).expect("inner encodes");
let capsule = AgentDeadLetter {
source: LogPosition::new(1, 2, 3, 99),
reason: DeadLetterReason::RetryExhausted,
attempts: 5,
detail: Some("handler kept failing".to_owned()),
payload: payload.clone(),
};
let bytes = encode_named(&capsule).expect("encodes");
let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
assert_eq!(back.payload, payload, "redrive needs the original bytes");
let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
assert_eq!(redrive, inner);
}
#[test]
fn given_a_run_lifecycle_status_when_validated_then_should_require_the_correlation() {
let base = AgentEnvelope::status(
RecordId::from_u128(1),
ConversationId::from_u128(2),
"runner".parse().expect("valid agent id"),
OPERATION_TASK,
)
.with_task_state(TaskState::Working)
.with_metadata(METADATA_RUN, "run-1");
assert!(
matches!(
validate(&base),
Err(ValidateError::Missing {
kind: AgentKind::Status,
field: "correlation"
})
),
"a task status without a correlation is rejected"
);
let correlated = base.with_correlation(CorrelationId::from_u128(2));
validate(&correlated)
.expect("the run-lifecycle status validates once it correlates on the run");
}
#[test]
fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
assert_eq!(TaskState::InputRequired.to_string(), "input-required");
assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
assert_eq!(
"input-required".parse::<TaskState>().expect("parses"),
TaskState::InputRequired
);
assert!("bogus".parse::<TaskState>().is_err());
}
}