1use crate::error::InvalidError;
19use crate::query::Value;
20use serde::de::{self, Visitor};
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use std::collections::BTreeMap;
23use std::fmt;
24use std::str::FromStr;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28#[non_exhaustive]
29pub enum IdParseError {
30 #[error("id must be 26 characters, got {got}")]
31 Length { got: usize },
32 #[error("id contains invalid character `{0}`")]
33 Char(char),
34 #[error("id overflows 128 bits")]
35 Overflow,
36}
37
38macro_rules! wire_id {
43 ($(#[$doc:meta])* $name:ident) => {
44 $(#[$doc])*
45 #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
46 pub struct $name(u128);
47
48 impl $name {
49 pub const fn from_u128(value: u128) -> Self {
51 Self(value)
52 }
53
54 pub const fn as_u128(self) -> u128 {
56 self.0
57 }
58
59 pub const fn to_bytes(self) -> [u8; 16] {
61 self.0.to_be_bytes()
62 }
63
64 pub const fn from_bytes(payload: [u8; 16]) -> Self {
66 Self(u128::from_be_bytes(payload))
67 }
68 }
69
70 impl fmt::Display for $name {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 let encoded = crockford_encode(self.0);
73 f.write_str(std::str::from_utf8(&encoded).expect("crockford output is ASCII"))
75 }
76 }
77
78 impl fmt::Debug for $name {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "{}({})", stringify!($name), self)
81 }
82 }
83
84 impl FromStr for $name {
85 type Err = IdParseError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 crockford_decode(s).map(Self)
89 }
90 }
91
92 impl From<u128> for $name {
93 fn from(value: u128) -> Self {
94 Self(value)
95 }
96 }
97
98 impl From<$name> for u128 {
99 fn from(value: $name) -> u128 {
100 value.0
101 }
102 }
103
104 impl Serialize for $name {
105 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106 serializer.serialize_bytes(&self.to_bytes())
107 }
108 }
109
110 impl<'de> Deserialize<'de> for $name {
111 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
112 struct BytesVisitor;
113
114 impl<'de> Visitor<'de> for BytesVisitor {
115 type Value = $name;
116
117 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str("16 big-endian id bytes")
119 }
120
121 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
122 let payload: [u8; 16] = v
123 .try_into()
124 .map_err(|_| E::invalid_length(v.len(), &self))?;
125 Ok($name::from_bytes(payload))
126 }
127 }
128
129 deserializer.deserialize_bytes(BytesVisitor)
130 }
131 }
132 };
133}
134
135pub(crate) use wire_id;
138
139wire_id!(
140 RecordId
147);
148wire_id!(
149 ConversationId
152);
153wire_id!(
154 CorrelationId
157);
158wire_id!(
159 ChannelId
162);
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
176pub struct LogPosition {
177 pub stream_id: u32,
178 pub topic_id: u32,
179 pub partition_id: u32,
180 pub offset: u64,
181}
182
183const LOG_POSITION_BYTES: usize = 20;
184
185impl LogPosition {
186 pub const fn new(stream_id: u32, topic_id: u32, partition_id: u32, offset: u64) -> Self {
188 Self {
189 stream_id,
190 topic_id,
191 partition_id,
192 offset,
193 }
194 }
195
196 pub fn to_bytes(self) -> [u8; LOG_POSITION_BYTES] {
198 let mut out = [0u8; LOG_POSITION_BYTES];
199 out[0..4].copy_from_slice(&self.stream_id.to_be_bytes());
200 out[4..8].copy_from_slice(&self.topic_id.to_be_bytes());
201 out[8..12].copy_from_slice(&self.partition_id.to_be_bytes());
202 out[12..20].copy_from_slice(&self.offset.to_be_bytes());
203 out
204 }
205
206 pub fn from_bytes(payload: [u8; LOG_POSITION_BYTES]) -> Self {
208 let u32_at = |start: usize| {
209 u32::from_be_bytes(payload[start..start + 4].try_into().expect("4-byte slice"))
210 };
211 Self {
212 stream_id: u32_at(0),
213 topic_id: u32_at(4),
214 partition_id: u32_at(8),
215 offset: u64::from_be_bytes(payload[12..20].try_into().expect("8-byte slice")),
216 }
217 }
218}
219
220impl Serialize for LogPosition {
224 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
225 serializer.serialize_bytes(&self.to_bytes())
226 }
227}
228
229impl<'de> Deserialize<'de> for LogPosition {
230 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
231 struct LocatorVisitor;
232
233 impl<'de> Visitor<'de> for LocatorVisitor {
234 type Value = LogPosition;
235
236 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.write_str("20 packed locator bytes")
238 }
239
240 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
241 let payload: [u8; LOG_POSITION_BYTES] = v
242 .try_into()
243 .map_err(|_| E::invalid_length(v.len(), &self))?;
244 Ok(LogPosition::from_bytes(payload))
245 }
246 }
247
248 deserializer.deserialize_bytes(LocatorVisitor)
249 }
250}
251
252#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
258#[serde(try_from = "String", into = "String")]
259pub struct IdempotencyKey(String);
260
261impl IdempotencyKey {
262 pub fn as_str(&self) -> &str {
264 &self.0
265 }
266}
267
268impl FromStr for IdempotencyKey {
269 type Err = InvalidError;
270
271 fn from_str(s: &str) -> Result<Self, Self::Err> {
272 s.to_owned().try_into()
273 }
274}
275
276impl TryFrom<String> for IdempotencyKey {
277 type Error = InvalidError;
278
279 fn try_from(value: String) -> Result<Self, Self::Error> {
280 if value.is_empty() {
281 return Err(InvalidError::new("idempotency key must not be empty"));
282 }
283 if value.len() > crate::limits::MAX_IDEMPOTENCY_KEY_BYTES {
284 return Err(InvalidError::new(format!(
285 "idempotency key is {}B, exceeds cap {}B",
286 value.len(),
287 crate::limits::MAX_IDEMPOTENCY_KEY_BYTES
288 )));
289 }
290 Ok(Self(value))
291 }
292}
293
294impl From<IdempotencyKey> for String {
295 fn from(value: IdempotencyKey) -> Self {
296 value.0
297 }
298}
299
300impl fmt::Display for IdempotencyKey {
301 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302 f.write_str(&self.0)
303 }
304}
305
306#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
316#[serde(try_from = "String", into = "String")]
317pub struct AgentId(String);
318
319impl AgentId {
320 pub fn as_str(&self) -> &str {
322 &self.0
323 }
324}
325
326impl FromStr for AgentId {
327 type Err = InvalidError;
328
329 fn from_str(s: &str) -> Result<Self, Self::Err> {
330 s.to_owned().try_into()
331 }
332}
333
334impl TryFrom<String> for AgentId {
335 type Error = InvalidError;
336
337 fn try_from(value: String) -> Result<Self, Self::Error> {
338 if value.is_empty() {
339 return Err(InvalidError::new("agent id must not be empty"));
340 }
341 if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
342 return Err(InvalidError::new(format!(
343 "agent id is {}B, exceeds cap {}B",
344 value.len(),
345 crate::limits::MAX_AGENT_STRING_BYTES
346 )));
347 }
348 if let Some(c) = value.chars().find(|c| c.is_control()) {
349 return Err(InvalidError::new(format!(
350 "agent id must not contain control characters (found {c:?})"
351 )));
352 }
353 Ok(Self(value))
354 }
355}
356
357impl TryFrom<&str> for AgentId {
358 type Error = InvalidError;
359
360 fn try_from(value: &str) -> Result<Self, Self::Error> {
361 value.to_owned().try_into()
362 }
363}
364
365impl From<AgentId> for String {
366 fn from(value: AgentId) -> Self {
367 value.0
368 }
369}
370
371impl fmt::Display for AgentId {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 f.write_str(&self.0)
374 }
375}
376
377#[derive(
381 Clone,
382 Copy,
383 Debug,
384 PartialEq,
385 Eq,
386 Hash,
387 Serialize,
388 Deserialize,
389 strum::Display,
390 strum::EnumString,
391)]
392#[serde(rename_all = "snake_case")]
393#[strum(serialize_all = "snake_case")]
394pub enum AgentKind {
395 Command,
398 Response,
400 Event,
402 Chunk,
404 Status,
407 Error,
409}
410
411#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
423#[serde(from = "u8", into = "u8")]
424pub enum TaskState {
425 Submitted,
426 Working,
427 InputRequired,
428 Completed,
429 Canceled,
430 Failed,
431 Rejected,
432 AuthRequired,
433 Unknown,
434 Unrecognized(u8),
436}
437
438impl TaskState {
439 pub const fn code(self) -> u8 {
441 match self {
442 TaskState::Submitted => 1,
443 TaskState::Working => 2,
444 TaskState::InputRequired => 3,
445 TaskState::Completed => 4,
446 TaskState::Canceled => 5,
447 TaskState::Failed => 6,
448 TaskState::Rejected => 7,
449 TaskState::AuthRequired => 8,
450 TaskState::Unknown => 9,
451 TaskState::Unrecognized(code) => code,
452 }
453 }
454
455 pub const fn from_code(code: u8) -> Self {
458 match code {
459 1 => TaskState::Submitted,
460 2 => TaskState::Working,
461 3 => TaskState::InputRequired,
462 4 => TaskState::Completed,
463 5 => TaskState::Canceled,
464 6 => TaskState::Failed,
465 7 => TaskState::Rejected,
466 8 => TaskState::AuthRequired,
467 9 => TaskState::Unknown,
468 other => TaskState::Unrecognized(other),
469 }
470 }
471
472 pub const fn is_terminal(self) -> bool {
475 matches!(
476 self,
477 TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
478 )
479 }
480}
481
482impl From<u8> for TaskState {
483 fn from(code: u8) -> Self {
484 Self::from_code(code)
485 }
486}
487
488impl From<TaskState> for u8 {
489 fn from(state: TaskState) -> u8 {
490 state.code()
491 }
492}
493
494impl fmt::Display for TaskState {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497 match self {
498 TaskState::Submitted => f.write_str("submitted"),
499 TaskState::Working => f.write_str("working"),
500 TaskState::InputRequired => f.write_str("input-required"),
501 TaskState::Completed => f.write_str("completed"),
502 TaskState::Canceled => f.write_str("canceled"),
503 TaskState::Failed => f.write_str("failed"),
504 TaskState::Rejected => f.write_str("rejected"),
505 TaskState::AuthRequired => f.write_str("auth-required"),
506 TaskState::Unknown => f.write_str("unknown"),
507 TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
508 }
509 }
510}
511
512impl FromStr for TaskState {
513 type Err = InvalidError;
514
515 fn from_str(s: &str) -> Result<Self, Self::Err> {
516 Ok(match s {
517 "submitted" => TaskState::Submitted,
518 "working" => TaskState::Working,
519 "input-required" => TaskState::InputRequired,
520 "completed" => TaskState::Completed,
521 "canceled" => TaskState::Canceled,
522 "failed" => TaskState::Failed,
523 "rejected" => TaskState::Rejected,
524 "auth-required" => TaskState::AuthRequired,
525 "unknown" => TaskState::Unknown,
526 other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
527 })
528 }
529}
530
531#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
538pub struct TokenUsage {
539 pub input_tokens: u64,
540 pub output_tokens: u64,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub reasoning_output_tokens: Option<u64>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub cache_read_input_tokens: Option<u64>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub cache_creation_input_tokens: Option<u64>,
547}
548
549#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
552#[serde(from = "u8", into = "u8")]
553pub enum AgentErrorCode {
554 InvalidRequest,
555 Unauthorized,
556 Unsupported,
557 DeadlineExceeded,
558 Cancelled,
559 ToolFailure,
560 Internal,
561 Unrecognized(u8),
563}
564
565impl AgentErrorCode {
566 pub const fn code(self) -> u8 {
568 match self {
569 AgentErrorCode::InvalidRequest => 1,
570 AgentErrorCode::Unauthorized => 2,
571 AgentErrorCode::Unsupported => 3,
572 AgentErrorCode::DeadlineExceeded => 4,
573 AgentErrorCode::Cancelled => 5,
574 AgentErrorCode::ToolFailure => 6,
575 AgentErrorCode::Internal => 7,
576 AgentErrorCode::Unrecognized(code) => code,
577 }
578 }
579
580 pub const fn from_code(code: u8) -> Self {
582 match code {
583 1 => AgentErrorCode::InvalidRequest,
584 2 => AgentErrorCode::Unauthorized,
585 3 => AgentErrorCode::Unsupported,
586 4 => AgentErrorCode::DeadlineExceeded,
587 5 => AgentErrorCode::Cancelled,
588 6 => AgentErrorCode::ToolFailure,
589 7 => AgentErrorCode::Internal,
590 other => AgentErrorCode::Unrecognized(other),
591 }
592 }
593}
594
595impl From<u8> for AgentErrorCode {
596 fn from(code: u8) -> Self {
597 Self::from_code(code)
598 }
599}
600
601impl From<AgentErrorCode> for u8 {
602 fn from(code: AgentErrorCode) -> u8 {
603 code.code()
604 }
605}
606
607#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
611pub struct AgentErrorBody {
612 pub code: AgentErrorCode,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub message: Option<String>,
615 #[serde(default)]
616 pub retryable: bool,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub detail: Option<BTreeMap<String, Value>>,
619}
620
621#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
623#[serde(from = "u8", into = "u8")]
624pub enum DeadLetterReason {
625 RetryExhausted,
626 Rejected,
627 DecodeFailed,
628 DeadlineExceeded,
629 Unrecognized(u8),
631}
632
633impl DeadLetterReason {
634 pub const fn code(self) -> u8 {
636 match self {
637 DeadLetterReason::RetryExhausted => 1,
638 DeadLetterReason::Rejected => 2,
639 DeadLetterReason::DecodeFailed => 3,
640 DeadLetterReason::DeadlineExceeded => 4,
641 DeadLetterReason::Unrecognized(code) => code,
642 }
643 }
644
645 pub const fn from_code(code: u8) -> Self {
647 match code {
648 1 => DeadLetterReason::RetryExhausted,
649 2 => DeadLetterReason::Rejected,
650 3 => DeadLetterReason::DecodeFailed,
651 4 => DeadLetterReason::DeadlineExceeded,
652 other => DeadLetterReason::Unrecognized(other),
653 }
654 }
655}
656
657impl From<u8> for DeadLetterReason {
658 fn from(code: u8) -> Self {
659 Self::from_code(code)
660 }
661}
662
663impl From<DeadLetterReason> for u8 {
664 fn from(reason: DeadLetterReason) -> u8 {
665 reason.code()
666 }
667}
668
669#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
679pub struct AgentDeadLetter {
680 pub source: LogPosition,
681 pub reason: DeadLetterReason,
682 pub attempts: u32,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub detail: Option<String>,
685 #[serde(with = "crate::encoding::bin_bytes")]
686 pub payload: Vec<u8>,
687}
688
689#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
692#[serde(from = "u8", into = "u8")]
693pub enum Health {
694 Healthy,
695 Degraded,
696 Unavailable,
697 Unrecognized(u8),
699}
700
701impl Health {
702 pub const fn code(self) -> u8 {
704 match self {
705 Health::Healthy => 1,
706 Health::Degraded => 2,
707 Health::Unavailable => 3,
708 Health::Unrecognized(code) => code,
709 }
710 }
711
712 pub const fn from_code(code: u8) -> Self {
715 match code {
716 1 => Health::Healthy,
717 2 => Health::Degraded,
718 3 => Health::Unavailable,
719 other => Health::Unrecognized(other),
720 }
721 }
722}
723
724impl From<u8> for Health {
725 fn from(code: u8) -> Self {
726 Self::from_code(code)
727 }
728}
729
730impl From<Health> for u8 {
731 fn from(health: Health) -> u8 {
732 health.code()
733 }
734}
735
736#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
744#[serde(rename_all = "snake_case")]
745pub enum ContentRef {
746 ContentType(crate::content::ContentType),
747 SchemaId(String),
748}
749
750#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
754pub struct CapabilityDescriptor {
755 pub skill_id: String,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub input: Option<ContentRef>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub output: Option<ContentRef>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub cost_class: Option<u8>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
766 pub latency_class: Option<u8>,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub max_concurrency: Option<u32>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub health: Option<Health>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub load: Option<u16>,
774}
775
776#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
784pub struct AgentCard {
785 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub name: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub version: Option<String>,
792 #[serde(default, skip_serializing_if = "Vec::is_empty")]
795 pub capabilities: Vec<CapabilityDescriptor>,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub ttl_micros: Option<u64>,
800}
801
802impl AgentCard {
803 pub fn validate(&self) -> Result<(), ValidateError> {
805 cap_str(self.name.as_deref(), "name")?;
806 cap_str(self.version.as_deref(), "version")?;
807 if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
808 return Err(ValidateError::TooLarge {
809 field: "capabilities",
810 size: self.capabilities.len(),
811 cap: crate::limits::MAX_CARD_CAPABILITIES,
812 });
813 }
814 for capability in &self.capabilities {
815 cap_str(Some(&capability.skill_id), "capability skill_id")?;
816 }
817 Ok(())
818 }
819}
820
821#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
839pub struct AgentPresence {
840 pub v: u32,
843 pub agent: AgentId,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub inbox: Option<String>,
853}
854
855impl AgentPresence {
856 pub fn new(agent: AgentId) -> Self {
859 Self {
860 v: crate::codes::PRESENCE_OP_VERSION,
861 agent,
862 inbox: None,
863 }
864 }
865
866 pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
868 self.inbox = Some(inbox.into());
869 self
870 }
871
872 pub fn validate(&self) -> Result<(), ValidateError> {
875 cap_str(self.inbox.as_deref(), "inbox")?;
876 Ok(())
877 }
878}
879
880#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
889pub struct BodyRef {
890 pub reference: String,
893 pub size_bytes: u64,
895 #[serde(with = "crate::encoding::bin_bytes")]
897 pub sha256: Vec<u8>,
898 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub encryption: Option<u8>,
903}
904
905const SHA256_BYTES: usize = 32;
906
907impl BodyRef {
908 pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
910 Self {
911 reference: reference.into(),
912 size_bytes,
913 sha256: sha256.to_vec(),
914 encryption: None,
915 }
916 }
917
918 pub fn validate(&self) -> Result<(), ValidateError> {
922 if self.reference.is_empty() {
923 return Err(ValidateError::Invalid {
924 field: "reference",
925 reason: "reference must not be empty".to_owned(),
926 });
927 }
928 if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
929 return Err(ValidateError::TooLarge {
930 field: "reference",
931 size: self.reference.len(),
932 cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
933 });
934 }
935 if self.sha256.len() != SHA256_BYTES {
936 return Err(ValidateError::Invalid {
937 field: "sha256",
938 reason: format!(
939 "digest must be {SHA256_BYTES} bytes, got {}",
940 self.sha256.len()
941 ),
942 });
943 }
944 Ok(())
945 }
946}
947
948#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
960pub struct Signature {
961 pub scheme: u8,
964 #[serde(with = "crate::encoding::bin_bytes")]
966 pub key_id: Vec<u8>,
967 #[serde(with = "crate::encoding::bin_bytes")]
970 pub bytes: Vec<u8>,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
977 pub context: Option<SignatureContext>,
978}
979
980#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
986pub struct SignatureContext {
987 #[serde(default, skip_serializing_if = "Option::is_none")]
989 pub content_type: Option<u8>,
990 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub agent_version: Option<u32>,
993}
994
995pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
997
998pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
1003
1004const ED25519_KEY_ID_BYTES: usize = 8;
1005const ED25519_SIGNATURE_BYTES: usize = 64;
1006
1007impl Signature {
1008 pub fn validate(&self) -> Result<(), ValidateError> {
1013 if self.scheme != SIGNATURE_SCHEME_ED25519 {
1014 return Ok(());
1015 }
1016 if self.key_id.len() != ED25519_KEY_ID_BYTES {
1017 return Err(ValidateError::Invalid {
1018 field: "key_id",
1019 reason: format!(
1020 "Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
1021 self.key_id.len()
1022 ),
1023 });
1024 }
1025 if self.bytes.len() != ED25519_SIGNATURE_BYTES {
1026 return Err(ValidateError::Invalid {
1027 field: "bytes",
1028 reason: format!(
1029 "Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
1030 self.bytes.len()
1031 ),
1032 });
1033 }
1034 Ok(())
1035 }
1036}
1037
1038#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1051pub struct AgentEnvelope {
1052 pub kind: AgentKind,
1053 #[serde(default, skip_serializing_if = "Option::is_none")]
1057 pub record: Option<RecordId>,
1058 pub conversation: ConversationId,
1060 pub source: AgentId,
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1066 pub target: Option<AgentId>,
1067 #[serde(default, skip_serializing_if = "Option::is_none")]
1070 pub cause: Option<RecordId>,
1071 #[serde(default, skip_serializing_if = "Option::is_none")]
1074 pub cause_at: Option<LogPosition>,
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub correlation: Option<CorrelationId>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub channel: Option<ChannelId>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub idempotency_key: Option<IdempotencyKey>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub deadline_micros: Option<u64>,
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1090 pub sequence: Option<u64>,
1091 #[serde(default, skip_serializing_if = "is_false")]
1095 pub last: bool,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1100 pub finish_reason: Option<String>,
1101 #[serde(default, skip_serializing_if = "Option::is_none")]
1102 pub task_state: Option<TaskState>,
1103 #[serde(default, skip_serializing_if = "Option::is_none")]
1106 pub operation: Option<String>,
1107 #[serde(default, skip_serializing_if = "Option::is_none")]
1109 pub tool: Option<String>,
1110 #[serde(default, skip_serializing_if = "Option::is_none")]
1111 pub usage: Option<TokenUsage>,
1112 #[serde(default, skip_serializing_if = "Option::is_none")]
1114 pub metadata: Option<BTreeMap<String, Value>>,
1115 #[serde(default, skip_serializing_if = "is_zero_u64")]
1127 pub must_understand: u64,
1128 #[serde(
1131 default,
1132 skip_serializing_if = "Vec::is_empty",
1133 with = "crate::encoding::bin_bytes"
1134 )]
1135 pub body: Vec<u8>,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1143 pub signature: Option<Signature>,
1144}
1145
1146fn is_false(value: &bool) -> bool {
1147 !*value
1148}
1149
1150fn is_zero_u64(value: &u64) -> bool {
1151 *value == 0
1152}
1153
1154pub mod features {
1161 pub const NONE: u64 = 0;
1165}
1166
1167impl AgentEnvelope {
1168 fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
1169 Self {
1170 kind,
1171 record: None,
1172 conversation,
1173 source,
1174 target: None,
1175 cause: None,
1176 cause_at: None,
1177 correlation: None,
1178 channel: None,
1179 idempotency_key: None,
1180 deadline_micros: None,
1181 sequence: None,
1182 last: false,
1183 finish_reason: None,
1184 task_state: None,
1185 operation: None,
1186 tool: None,
1187 usage: None,
1188 metadata: None,
1189 must_understand: 0,
1190 body: Vec::new(),
1191 signature: None,
1192 }
1193 }
1194
1195 #[must_use]
1200 pub fn requiring(mut self, bits: u64) -> Self {
1201 self.must_understand = bits;
1202 self
1203 }
1204
1205 pub fn unmet_requirements(&self, understood: u64) -> u64 {
1210 self.must_understand & !understood
1211 }
1212
1213 pub fn command(
1215 record: RecordId,
1216 conversation: ConversationId,
1217 source: AgentId,
1218 correlation: CorrelationId,
1219 body: Vec<u8>,
1220 ) -> Self {
1221 let mut envelope = Self::base(AgentKind::Command, conversation, source);
1222 envelope.record = Some(record);
1223 envelope.correlation = Some(correlation);
1224 envelope.body = body;
1225 envelope
1226 }
1227
1228 pub fn response(
1230 record: RecordId,
1231 conversation: ConversationId,
1232 source: AgentId,
1233 correlation: CorrelationId,
1234 body: Vec<u8>,
1235 ) -> Self {
1236 let mut envelope = Self::base(AgentKind::Response, conversation, source);
1237 envelope.record = Some(record);
1238 envelope.correlation = Some(correlation);
1239 envelope.body = body;
1240 envelope
1241 }
1242
1243 pub fn event(
1245 record: RecordId,
1246 conversation: ConversationId,
1247 source: AgentId,
1248 body: Vec<u8>,
1249 ) -> Self {
1250 let mut envelope = Self::base(AgentKind::Event, conversation, source);
1251 envelope.record = Some(record);
1252 envelope.body = body;
1253 envelope
1254 }
1255
1256 pub fn chunk(
1259 conversation: ConversationId,
1260 source: AgentId,
1261 correlation: CorrelationId,
1262 channel: ChannelId,
1263 sequence: u64,
1264 body: Vec<u8>,
1265 ) -> Self {
1266 let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
1267 envelope.correlation = Some(correlation);
1268 envelope.channel = Some(channel);
1269 envelope.sequence = Some(sequence);
1270 envelope.body = body;
1271 envelope
1272 }
1273
1274 pub fn status(
1278 record: RecordId,
1279 conversation: ConversationId,
1280 source: AgentId,
1281 operation: impl Into<String>,
1282 ) -> Self {
1283 let mut envelope = Self::base(AgentKind::Status, conversation, source);
1284 envelope.record = Some(record);
1285 envelope.operation = Some(operation.into());
1286 envelope
1287 }
1288
1289 pub fn error(
1292 record: RecordId,
1293 conversation: ConversationId,
1294 source: AgentId,
1295 correlation: CorrelationId,
1296 body: Vec<u8>,
1297 ) -> Self {
1298 let mut envelope = Self::base(AgentKind::Error, conversation, source);
1299 envelope.record = Some(record);
1300 envelope.correlation = Some(correlation);
1301 envelope.body = body;
1302 envelope
1303 }
1304
1305 pub fn with_target(mut self, target: AgentId) -> Self {
1307 self.target = Some(target);
1308 self
1309 }
1310
1311 pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
1315 self.cause = Some(cause);
1316 self.cause_at = cause_at;
1317 self
1318 }
1319
1320 pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
1324 self.correlation = Some(correlation);
1325 self
1326 }
1327
1328 pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
1330 self.idempotency_key = Some(key);
1331 self
1332 }
1333
1334 pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
1337 self.deadline_micros = Some(deadline_micros);
1338 self
1339 }
1340
1341 pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
1344 self.last = true;
1345 self.finish_reason = Some(finish_reason.into());
1346 self
1347 }
1348
1349 pub fn with_task_state(mut self, state: TaskState) -> Self {
1352 self.task_state = Some(state);
1353 self
1354 }
1355
1356 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
1358 self.operation = Some(operation.into());
1359 self
1360 }
1361
1362 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
1364 self.tool = Some(tool.into());
1365 self
1366 }
1367
1368 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
1370 self.usage = Some(usage);
1371 self
1372 }
1373
1374 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1376 self.metadata
1377 .get_or_insert_with(BTreeMap::new)
1378 .insert(key.into(), value.into());
1379 self
1380 }
1381
1382 pub fn with_signature(mut self, signature: Signature) -> Self {
1387 self.signature = Some(signature);
1388 self
1389 }
1390}
1391
1392#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1395#[non_exhaustive]
1396pub enum ValidateError {
1397 #[error("{kind} requires `{field}`")]
1398 Missing {
1399 kind: AgentKind,
1400 field: &'static str,
1401 },
1402 #[error("`{field}` is invalid on {kind}")]
1403 Forbidden {
1404 kind: AgentKind,
1405 field: &'static str,
1406 },
1407 #[error("`{field}` is {size}B, exceeds cap {cap}B")]
1408 TooLarge {
1409 field: &'static str,
1410 size: usize,
1411 cap: usize,
1412 },
1413 #[error("`{field}`: {reason}")]
1414 Invalid { field: &'static str, reason: String },
1415}
1416
1417pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
1442 use AgentKind::*;
1443 let kind = envelope.kind;
1444
1445 let require = |present: bool, field: &'static str| {
1446 if present {
1447 Ok(())
1448 } else {
1449 Err(ValidateError::Missing { kind, field })
1450 }
1451 };
1452 let forbid = |absent: bool, field: &'static str| {
1453 if absent {
1454 Ok(())
1455 } else {
1456 Err(ValidateError::Forbidden { kind, field })
1457 }
1458 };
1459
1460 if kind != Chunk {
1462 require(envelope.record.is_some(), "record")?;
1463 }
1464
1465 match kind {
1467 Command | Response | Chunk | Error => {
1468 require(envelope.correlation.is_some(), "correlation")?
1469 }
1470 Status => {
1471 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1472 require(envelope.correlation.is_some(), "correlation")?;
1473 }
1474 }
1475 Event => {}
1476 }
1477
1478 match kind {
1481 Chunk => {
1482 require(envelope.channel.is_some(), "channel")?;
1483 require(envelope.sequence.is_some(), "sequence")?;
1484 }
1485 Error => {
1486 if envelope.sequence.is_some() && envelope.channel.is_none() {
1487 return Err(ValidateError::Invalid {
1488 field: "sequence",
1489 reason: "sequence requires channel".to_owned(),
1490 });
1491 }
1492 }
1493 _ => {
1494 forbid(envelope.channel.is_none(), "channel")?;
1495 forbid(envelope.sequence.is_none(), "sequence")?;
1496 }
1497 }
1498
1499 if envelope.last && !matches!(kind, Chunk | Status) {
1502 return Err(ValidateError::Forbidden {
1503 kind,
1504 field: "last",
1505 });
1506 }
1507
1508 match kind {
1510 Response => {}
1511 Chunk => {
1512 if envelope.finish_reason.is_some() && !envelope.last {
1513 return Err(ValidateError::Invalid {
1514 field: "finish_reason",
1515 reason: "finish_reason rides only the terminal chunk".to_owned(),
1516 });
1517 }
1518 }
1519 _ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
1520 }
1521
1522 if matches!(kind, Chunk | Status | Error) {
1525 forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
1526 }
1527
1528 if matches!(kind, Response | Event | Status | Error) {
1531 forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
1532 }
1533 if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
1534 return Err(ValidateError::Invalid {
1535 field: "deadline_micros",
1536 reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
1537 });
1538 }
1539
1540 match kind {
1542 Status => {
1543 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1544 require(envelope.task_state.is_some(), "task_state")?;
1545 }
1546 }
1547 Response | Error => {}
1548 _ => forbid(envelope.task_state.is_none(), "task_state")?,
1549 }
1550
1551 match kind {
1557 Status => {
1558 require(envelope.operation.is_some(), "operation")?;
1559 if let Some(operation) = envelope.operation.as_deref()
1560 && !matches!(
1561 operation,
1562 OPERATION_TASK
1563 | OPERATION_CARD
1564 | OPERATION_PROGRESS
1565 | OPERATION_QUARANTINE
1566 | OPERATION_UNQUARANTINE
1567 )
1568 {
1569 return Err(ValidateError::Invalid {
1570 field: "operation",
1571 reason: format!(
1572 "status operation must be `{OPERATION_TASK}`, `{OPERATION_CARD}`, \
1573 `{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
1574 `{OPERATION_UNQUARANTINE}`, got `{operation}`"
1575 ),
1576 });
1577 }
1578 }
1579 Chunk => {
1580 if envelope.sequence == Some(0) {
1581 require(envelope.operation.is_some(), "operation")?;
1582 }
1583 if let Some(operation) = envelope.operation.as_deref() {
1584 if envelope.sequence != Some(0) {
1585 return Err(ValidateError::Invalid {
1586 field: "operation",
1587 reason: "the stream purpose rides the opening chunk (sequence 0)"
1588 .to_owned(),
1589 });
1590 }
1591 if !matches!(
1592 operation,
1593 OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
1594 ) {
1595 return Err(ValidateError::Invalid {
1596 field: "operation",
1597 reason: format!(
1598 "chunk-stream purpose must be `{OPERATION_CHAT}`, \
1599 `{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
1600 got `{operation}`"
1601 ),
1602 });
1603 }
1604 }
1605 }
1606 Command | Response | Event | Error => {}
1607 }
1608
1609 if kind == Status {
1611 forbid(envelope.tool.is_none(), "tool")?;
1612 }
1613
1614 match kind {
1616 Command => forbid(envelope.usage.is_none(), "usage")?,
1617 Chunk if envelope.usage.is_some() && !envelope.last => {
1618 return Err(ValidateError::Invalid {
1619 field: "usage",
1620 reason: "whole-stream accounting rides the terminal chunk".to_owned(),
1621 });
1622 }
1623 _ => {}
1624 }
1625
1626 match kind {
1628 Status => {}
1629 Chunk => {
1630 if envelope.body.is_empty() && !envelope.last {
1631 return Err(ValidateError::Missing {
1632 kind,
1633 field: "body",
1634 });
1635 }
1636 }
1637 _ => require(!envelope.body.is_empty(), "body")?,
1638 }
1639
1640 cap_str(envelope.operation.as_deref(), "operation")?;
1644 cap_str(envelope.tool.as_deref(), "tool")?;
1645 cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
1646 if let Some(metadata) = &envelope.metadata {
1647 if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
1648 return Err(ValidateError::TooLarge {
1649 field: "metadata",
1650 size: metadata.len(),
1651 cap: crate::limits::MAX_METADATA_ENTRIES,
1652 });
1653 }
1654 let mut total = 0usize;
1655 for (key, value) in metadata {
1656 if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
1657 return Err(ValidateError::TooLarge {
1658 field: "metadata key",
1659 size: key.len(),
1660 cap: crate::limits::MAX_METADATA_KEY_BYTES,
1661 });
1662 }
1663 let value_size = value_size(value);
1664 if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
1665 return Err(ValidateError::TooLarge {
1666 field: "metadata value",
1667 size: value_size,
1668 cap: crate::limits::MAX_METADATA_VALUE_BYTES,
1669 });
1670 }
1671 total += key.len() + value_size;
1672 }
1673 if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
1674 return Err(ValidateError::TooLarge {
1675 field: "metadata",
1676 size: total,
1677 cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
1678 });
1679 }
1680 }
1681 if let Some(signature) = &envelope.signature {
1684 signature.validate()?;
1685 }
1686 Ok(())
1687}
1688
1689pub const OPERATION_TASK: &str = "task";
1691pub const OPERATION_CARD: &str = "card";
1693pub const OPERATION_PROGRESS: &str = "progress";
1695pub const OPERATION_QUARANTINE: &str = "quarantine";
1700pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
1705
1706pub const OPERATION_CHAT: &str = "chat";
1713pub const OPERATION_REASONING: &str = "reasoning";
1715pub const OPERATION_TOOL_ARGS: &str = "tool_args";
1717
1718pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
1724pub const OPERATION_STATE_DELTA: &str = "state_delta";
1727
1728pub const METADATA_ROLE: &str = "role";
1735pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
1741pub const METADATA_RUN: &str = "run";
1747
1748pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
1752pub const METADATA_PURPOSE: &str = "purpose";
1755pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
1758pub const METADATA_TASK_CONTEXT: &str = "task_context";
1761pub const METADATA_SESSION_INTENT: &str = "session_intent";
1764
1765const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1769
1770pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
1771 let mut out = [0u8; 26];
1772 let mut v = value;
1773 for slot in out.iter_mut().rev() {
1774 *slot = CROCKFORD[(v & 0x1f) as usize];
1775 v >>= 5;
1776 }
1777 out
1778}
1779
1780pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
1781 let bytes = s.as_bytes();
1782 if bytes.len() != 26 {
1783 return Err(IdParseError::Length { got: bytes.len() });
1784 }
1785 let mut value: u128 = 0;
1786 for (i, byte) in bytes.iter().enumerate() {
1787 let digit = CROCKFORD
1788 .iter()
1789 .position(|c| *c == byte.to_ascii_uppercase())
1790 .ok_or(IdParseError::Char(*byte as char))?;
1791 if i == 0 && digit > 7 {
1794 return Err(IdParseError::Overflow);
1795 }
1796 value = (value << 5) | digit as u128;
1797 }
1798 Ok(value)
1799}
1800
1801fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
1802 if let Some(value) = value
1803 && value.len() > crate::limits::MAX_AGENT_STRING_BYTES
1804 {
1805 return Err(ValidateError::TooLarge {
1806 field,
1807 size: value.len(),
1808 cap: crate::limits::MAX_AGENT_STRING_BYTES,
1809 });
1810 }
1811 Ok(())
1812}
1813
1814fn value_size(value: &Value) -> usize {
1817 match value {
1818 Value::Str(s) => s.len(),
1819 Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
1820 _ => 9,
1821 }
1822}
1823
1824#[cfg(test)]
1825mod tests {
1826 use super::*;
1827
1828 #[test]
1829 fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
1830 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
1831 let text = id.to_string();
1832 assert_eq!(text.len(), 26);
1833 assert_eq!(text.parse::<RecordId>().expect("parses"), id);
1834 assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
1836 assert_eq!(
1837 RecordId::from_u128(0).to_string(),
1838 "00000000000000000000000000"
1839 );
1840 assert_eq!(
1841 RecordId::from_u128(u128::MAX).to_string(),
1842 "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
1843 );
1844 }
1845
1846 #[test]
1847 fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
1848 assert_eq!(
1849 "short".parse::<RecordId>(),
1850 Err(IdParseError::Length { got: 5 })
1851 );
1852 assert_eq!(
1853 "8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
1854 Err(IdParseError::Overflow)
1855 );
1856 assert!(matches!(
1857 "UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
1858 Err(IdParseError::Char('U'))
1859 ));
1860 }
1861
1862 #[test]
1863 fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
1864 for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
1865 assert_eq!(
1866 s.parse::<AgentId>()
1867 .expect("a printable agent id is valid")
1868 .as_str(),
1869 s
1870 );
1871 }
1872 assert!("".parse::<AgentId>().is_err());
1873 assert!("bad\nid".parse::<AgentId>().is_err());
1874 }
1875
1876 #[test]
1877 fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
1878 let expected = [
1879 (TaskState::Submitted, 1u8),
1880 (TaskState::Working, 2),
1881 (TaskState::InputRequired, 3),
1882 (TaskState::Completed, 4),
1883 (TaskState::Canceled, 5),
1884 (TaskState::Failed, 6),
1885 (TaskState::Rejected, 7),
1886 (TaskState::AuthRequired, 8),
1887 (TaskState::Unknown, 9),
1888 ];
1889 for (state, code) in expected {
1890 assert_eq!(state.code(), code);
1891 assert_eq!(TaskState::from_code(code), state);
1892 }
1893 let future = TaskState::from_code(42);
1895 assert_eq!(future, TaskState::Unrecognized(42));
1896 assert_eq!(future.code(), 42);
1897 assert!(!future.is_terminal());
1898 assert!(TaskState::Completed.is_terminal());
1899 assert!(!TaskState::Working.is_terminal());
1900 }
1901
1902 #[test]
1903 fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
1904 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
1905 assert_eq!(
1906 "auth-required".parse::<TaskState>().expect("parses"),
1907 TaskState::AuthRequired
1908 );
1909 assert!("nope".parse::<TaskState>().is_err());
1910 }
1911
1912 #[test]
1913 fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
1914 assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
1915 assert_eq!(AgentErrorCode::Internal.code(), 7);
1916 assert_eq!(
1917 AgentErrorCode::from_code(99),
1918 AgentErrorCode::Unrecognized(99)
1919 );
1920 assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
1921 assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
1922 assert_eq!(
1923 DeadLetterReason::from_code(77),
1924 DeadLetterReason::Unrecognized(77)
1925 );
1926 }
1927
1928 #[test]
1929 fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
1930 for code in 0u8..=u8::MAX {
1934 assert_eq!(TaskState::from_code(code).code(), code);
1935 assert_eq!(AgentErrorCode::from_code(code).code(), code);
1936 assert_eq!(DeadLetterReason::from_code(code).code(), code);
1937 }
1938 }
1939
1940 #[test]
1941 fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
1942 assert!("order-123-attempt-2".parse::<IdempotencyKey>().is_ok());
1943 assert!("".parse::<IdempotencyKey>().is_err());
1944 assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
1945 }
1946
1947 #[test]
1948 fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
1949 let (record, conversation, source, correlation) = ids();
1950 let command =
1951 AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
1952 validate(&command).expect("a well-formed command validates");
1953
1954 let mut missing = command.clone();
1956 missing.correlation = None;
1957 assert_eq!(
1958 validate(&missing),
1959 Err(ValidateError::Missing {
1960 kind: AgentKind::Command,
1961 field: "correlation"
1962 })
1963 );
1964
1965 let with_usage = command.clone().with_usage(TokenUsage::default());
1967 assert!(matches!(
1968 validate(&with_usage),
1969 Err(ValidateError::Forbidden { field: "usage", .. })
1970 ));
1971
1972 let mut with_channel = command;
1974 with_channel.channel = Some(ChannelId::from_u128(1));
1975 assert!(matches!(
1976 validate(&with_channel),
1977 Err(ValidateError::Forbidden {
1978 field: "channel",
1979 ..
1980 })
1981 ));
1982 }
1983
1984 #[test]
1985 fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
1986 let (_, conversation, source, correlation) = ids();
1987 let channel = ChannelId::from_u128(23);
1988 let chunk = AgentEnvelope::chunk(
1989 conversation,
1990 source.clone(),
1991 correlation,
1992 channel,
1993 0,
1994 b"tok".to_vec(),
1995 )
1996 .with_operation(OPERATION_CHAT);
1997 validate(&chunk).expect("a stream chunk validates");
1998
1999 let mut undeclared = chunk.clone();
2001 undeclared.operation = None;
2002 assert!(matches!(
2003 validate(&undeclared),
2004 Err(ValidateError::Missing {
2005 field: "operation",
2006 ..
2007 })
2008 ));
2009 let mut off_vocabulary = chunk.clone();
2010 off_vocabulary.operation = Some("telemetry".to_owned());
2011 assert!(matches!(
2012 validate(&off_vocabulary),
2013 Err(ValidateError::Invalid {
2014 field: "operation",
2015 ..
2016 })
2017 ));
2018
2019 let redeclared = AgentEnvelope::chunk(
2021 conversation,
2022 source.clone(),
2023 correlation,
2024 channel,
2025 3,
2026 b"tok".to_vec(),
2027 )
2028 .with_operation(OPERATION_REASONING);
2029 assert!(matches!(
2030 validate(&redeclared),
2031 Err(ValidateError::Invalid {
2032 field: "operation",
2033 ..
2034 })
2035 ));
2036
2037 let terminal = AgentEnvelope::chunk(
2039 conversation,
2040 source.clone(),
2041 correlation,
2042 channel,
2043 41,
2044 Vec::new(),
2045 )
2046 .terminal("stop")
2047 .with_usage(TokenUsage {
2048 input_tokens: 100,
2049 output_tokens: 42,
2050 ..Default::default()
2051 });
2052 validate(&terminal).expect("a terminal chunk validates");
2053
2054 let mut early_finish = chunk.clone();
2056 early_finish.finish_reason = Some("stop".to_owned());
2057 assert!(matches!(
2058 validate(&early_finish),
2059 Err(ValidateError::Invalid {
2060 field: "finish_reason",
2061 ..
2062 })
2063 ));
2064
2065 let empty = AgentEnvelope::chunk(
2067 conversation,
2068 source.clone(),
2069 correlation,
2070 channel,
2071 1,
2072 Vec::new(),
2073 );
2074 assert!(matches!(
2075 validate(&empty),
2076 Err(ValidateError::Missing { field: "body", .. })
2077 ));
2078
2079 let mut keyed = chunk.clone();
2081 keyed.idempotency_key = Some("k".parse().expect("valid key"));
2082 assert!(matches!(
2083 validate(&keyed),
2084 Err(ValidateError::Forbidden {
2085 field: "idempotency_key",
2086 ..
2087 })
2088 ));
2089
2090 let opening = chunk.with_deadline_micros(1);
2092 validate(&opening).expect("an opening chunk may declare the bound");
2093 let late = AgentEnvelope::chunk(
2094 conversation,
2095 source.clone(),
2096 correlation,
2097 channel,
2098 5,
2099 b"tok".to_vec(),
2100 )
2101 .with_deadline_micros(1);
2102 assert!(matches!(
2103 validate(&late),
2104 Err(ValidateError::Invalid {
2105 field: "deadline_micros",
2106 ..
2107 })
2108 ));
2109 }
2110
2111 #[test]
2112 fn given_status_signals_when_validated_then_task_updates_should_require_state() {
2113 let (record, conversation, source, correlation) = ids();
2114 let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
2116 validate(&card).expect("a card validates");
2117
2118 let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
2120 assert!(matches!(
2121 validate(&bare_task),
2122 Err(ValidateError::Missing {
2123 field: "correlation",
2124 ..
2125 })
2126 ));
2127 let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
2128 .with_correlation(correlation)
2129 .with_task_state(TaskState::Working);
2130 validate(&task).expect("a task update validates");
2131
2132 let off_vocabulary =
2134 AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
2135 assert!(matches!(
2136 validate(&off_vocabulary),
2137 Err(ValidateError::Invalid {
2138 field: "operation",
2139 ..
2140 })
2141 ));
2142 }
2143
2144 #[test]
2145 fn given_an_error_when_validated_then_last_should_be_forbidden() {
2146 let (record, conversation, source, correlation) = ids();
2147 let error =
2148 AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
2149 validate(&error).expect("an error validates");
2150
2151 let mut flagged = error.clone();
2153 flagged.last = true;
2154 assert!(matches!(
2155 validate(&flagged),
2156 Err(ValidateError::Forbidden { field: "last", .. })
2157 ));
2158
2159 let mut dangling = error;
2161 dangling.sequence = Some(3);
2162 assert!(matches!(
2163 validate(&dangling),
2164 Err(ValidateError::Invalid {
2165 field: "sequence",
2166 ..
2167 })
2168 ));
2169 }
2170
2171 fn descriptor(skill_id: &str) -> CapabilityDescriptor {
2172 CapabilityDescriptor {
2173 skill_id: skill_id.to_owned(),
2174 input: None,
2175 output: None,
2176 cost_class: None,
2177 latency_class: None,
2178 max_concurrency: None,
2179 health: None,
2180 load: None,
2181 }
2182 }
2183
2184 #[test]
2185 fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
2186 let card = AgentCard {
2187 name: Some("trip-planner".to_owned()),
2188 version: Some("1.4.2".to_owned()),
2189 capabilities: vec![
2190 CapabilityDescriptor {
2191 skill_id: "chat".to_owned(),
2192 input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2193 output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2194 cost_class: Some(2),
2195 latency_class: Some(1),
2196 max_concurrency: Some(8),
2197 health: Some(Health::Healthy),
2198 load: Some(250),
2199 },
2200 descriptor("search_flights"),
2201 ],
2202 ttl_micros: Some(30_000_000),
2203 };
2204 card.validate().expect("a well-formed card validates");
2205
2206 let mut crowded = card.clone();
2207 crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
2208 assert!(matches!(
2209 crowded.validate(),
2210 Err(ValidateError::TooLarge {
2211 field: "capabilities",
2212 ..
2213 })
2214 ));
2215
2216 let mut oversized = card;
2217 oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
2218 assert!(matches!(
2219 oversized.validate(),
2220 Err(ValidateError::TooLarge { field: "name", .. })
2221 ));
2222 }
2223
2224 #[cfg(feature = "cbor")]
2225 #[test]
2226 fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
2227 {
2228 use crate::framing::{decode_named, encode_named};
2229 let reference = ContentRef::SchemaId("json".to_owned());
2232 let bytes = encode_named(&reference).expect("encodes");
2233 let back: ContentRef = decode_named(&bytes).expect("decodes");
2234 assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
2235 let ct = ContentRef::ContentType(crate::content::ContentType::Json);
2237 let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
2238 assert_eq!(back, ct);
2239 }
2240
2241 #[test]
2242 fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
2243 let valid = Signature {
2244 scheme: SIGNATURE_SCHEME_ED25519,
2245 key_id: vec![1u8; 8],
2246 bytes: vec![2u8; 64],
2247 context: None,
2248 };
2249 valid
2250 .validate()
2251 .expect("a well-formed Ed25519 signature validates");
2252
2253 let mut short_key = valid.clone();
2254 short_key.key_id = vec![1u8; 4];
2255 assert!(matches!(
2256 short_key.validate(),
2257 Err(ValidateError::Invalid {
2258 field: "key_id",
2259 ..
2260 })
2261 ));
2262
2263 let mut short_signature = valid.clone();
2264 short_signature.bytes = vec![2u8; 32];
2265 assert!(matches!(
2266 short_signature.validate(),
2267 Err(ValidateError::Invalid { field: "bytes", .. })
2268 ));
2269
2270 let future = Signature {
2273 scheme: 42,
2274 key_id: vec![1u8; 3],
2275 bytes: vec![2u8; 99],
2276 context: None,
2277 };
2278 future.validate().expect("an unknown scheme flows through");
2279 }
2280
2281 #[test]
2282 fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
2283 let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
2284 valid.validate().expect("a well-formed reference validates");
2285
2286 let mut empty = valid.clone();
2287 empty.reference = String::new();
2288 assert!(matches!(
2289 empty.validate(),
2290 Err(ValidateError::Invalid {
2291 field: "reference",
2292 ..
2293 })
2294 ));
2295
2296 let mut oversized = valid.clone();
2297 oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
2298 assert!(matches!(
2299 oversized.validate(),
2300 Err(ValidateError::TooLarge {
2301 field: "reference",
2302 ..
2303 })
2304 ));
2305
2306 let mut truncated = valid;
2307 truncated.sha256 = vec![7u8; 16];
2308 assert!(matches!(
2309 truncated.validate(),
2310 Err(ValidateError::Invalid {
2311 field: "sha256",
2312 ..
2313 })
2314 ));
2315 }
2316
2317 #[test]
2318 fn given_oversized_metadata_when_validated_then_should_reject() {
2319 let (record, conversation, source, correlation) = ids();
2320 let mut command =
2321 AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
2322 for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
2323 command = command.with_metadata(format!("k{i}"), i as i64);
2324 }
2325 assert!(matches!(
2326 validate(&command),
2327 Err(ValidateError::TooLarge {
2328 field: "metadata",
2329 ..
2330 })
2331 ));
2332 }
2333
2334 fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
2335 (
2336 RecordId::from_u128(7),
2337 ConversationId::from_u128(11),
2338 "test-agent".parse().expect("valid agent id"),
2339 CorrelationId::from_u128(17),
2340 )
2341 }
2342}
2343
2344#[cfg(all(test, feature = "cbor"))]
2345mod wire_tests {
2346 use super::*;
2347 use crate::framing::{decode_named, encode_named};
2348
2349 #[test]
2350 fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
2351 let mut valid = vec![0x40 | 20];
2354 valid.extend_from_slice(&[0u8; 20]);
2355 decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
2356
2357 for bad_len in [0u8, 19, 21, 23] {
2358 let mut bytes = vec![0x40 | bad_len];
2359 bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
2360 assert!(
2361 decode_named::<LogPosition>(&bytes).is_err(),
2362 "a {bad_len}-byte locator must error, not panic"
2363 );
2364 }
2365 }
2366
2367 #[test]
2368 fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
2369 let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
2370 let bytes = encode_named(&pos).expect("encodes");
2371 let back: LogPosition = decode_named(&bytes).expect("decodes");
2372 assert_eq!(back, pos);
2373 }
2374
2375 #[test]
2376 fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
2377 let envelope = AgentEnvelope::command(
2378 RecordId::from_u128(1),
2379 ConversationId::from_u128(2),
2380 "source-agent".parse().expect("valid agent id"),
2381 CorrelationId::from_u128(4),
2382 b"payload".to_vec(),
2383 )
2384 .with_target("target-agent".parse().expect("valid agent id"))
2385 .with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
2386 .with_idempotency_key("order-1".parse().expect("valid key"))
2387 .with_deadline_micros(1_700_000_000_000_000)
2388 .with_operation("chat")
2389 .with_tool("search")
2390 .with_metadata("customer_tier", "gold");
2391 let bytes = encode_named(&envelope).expect("encodes");
2392 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2393 assert_eq!(back, envelope);
2394 }
2395
2396 #[test]
2397 fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
2398 let envelope = AgentEnvelope::event(
2402 RecordId::from_u128(1),
2403 ConversationId::from_u128(2),
2404 "source-agent".parse().expect("valid agent id"),
2405 b"e".to_vec(),
2406 )
2407 .requiring(0b101);
2408 let bytes = encode_named(&envelope).expect("encodes");
2409 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2410 assert_eq!(back.must_understand, 0b101);
2411 assert_eq!(back.unmet_requirements(0b001), 0b100);
2413 assert_eq!(back.unmet_requirements(0b111), 0);
2415 let plain = AgentEnvelope::event(
2417 RecordId::from_u128(1),
2418 ConversationId::from_u128(2),
2419 "source-agent".parse().expect("valid agent id"),
2420 b"e".to_vec(),
2421 );
2422 assert_eq!(plain.unmet_requirements(features::NONE), 0);
2423 let json = serde_json::to_string(&plain).expect("json");
2424 assert!(
2425 !json.contains("must_understand"),
2426 "zero marker must be omitted: {json}"
2427 );
2428 }
2429
2430 #[test]
2431 fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
2432 let envelope = AgentEnvelope::event(
2435 RecordId::from_u128(1),
2436 ConversationId::from_u128(2),
2437 "source-agent".parse().expect("valid agent id"),
2438 b"e".to_vec(),
2439 );
2440 let bytes = encode_named(&envelope).expect("encodes");
2441 assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
2444 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2445 assert!(!back.last);
2446 assert!(back.metadata.is_none());
2447 }
2448
2449 #[test]
2450 fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
2451 let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
2452 assert_eq!(bytes.len(), 17);
2455 assert_eq!(bytes[0], 0x50);
2456 assert_eq!(bytes[16], 0x02);
2457 assert_eq!(bytes[15], 0x01);
2458 }
2459
2460 #[test]
2461 fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
2462 let bytes = encode_named(&TaskState::Completed).expect("encodes");
2463 assert_eq!(bytes, vec![4]);
2464 let unknown = encode_named(&42u8).expect("encodes a raw code");
2465 let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
2466 assert_eq!(back, TaskState::Unrecognized(42));
2467 }
2468
2469 #[test]
2470 fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
2471 let body = AgentErrorBody {
2472 code: AgentErrorCode::ToolFailure,
2473 message: Some("search timed out".to_owned()),
2474 retryable: true,
2475 detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
2476 };
2477 let bytes = encode_named(&body).expect("encodes");
2478 let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
2479 assert_eq!(back, body);
2480 }
2481
2482 #[test]
2483 fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
2484 let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
2485 let bytes = encode_named(&capsule).expect("encodes");
2486 let back: BodyRef = decode_named(&bytes).expect("decodes");
2487 assert_eq!(back, capsule);
2488 assert!(back.encryption.is_none(), "absent encryption costs nothing");
2489 back.validate().expect("decoded capsule validates");
2490 }
2491
2492 #[test]
2493 fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
2494 let inner = AgentEnvelope::command(
2495 RecordId::from_u128(9),
2496 ConversationId::from_u128(8),
2497 "source-agent".parse().expect("valid agent id"),
2498 CorrelationId::from_u128(6),
2499 b"poison".to_vec(),
2500 );
2501 let payload = encode_named(&inner).expect("inner encodes");
2502 let capsule = AgentDeadLetter {
2503 source: LogPosition::new(1, 2, 3, 99),
2504 reason: DeadLetterReason::RetryExhausted,
2505 attempts: 5,
2506 detail: Some("handler kept failing".to_owned()),
2507 payload: payload.clone(),
2508 };
2509 let bytes = encode_named(&capsule).expect("encodes");
2510 let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
2511 assert_eq!(back.payload, payload, "redrive needs the original bytes");
2512 let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
2513 assert_eq!(redrive, inner);
2514 }
2515 #[test]
2516 fn given_a_run_lifecycle_status_when_validated_then_should_require_the_correlation() {
2517 let base = AgentEnvelope::status(
2521 RecordId::from_u128(1),
2522 ConversationId::from_u128(2),
2523 "runner".parse().expect("valid agent id"),
2524 OPERATION_TASK,
2525 )
2526 .with_task_state(TaskState::Working)
2527 .with_metadata(METADATA_RUN, "run-1");
2528 assert!(
2529 matches!(
2530 validate(&base),
2531 Err(ValidateError::Missing {
2532 kind: AgentKind::Status,
2533 field: "correlation"
2534 })
2535 ),
2536 "a task status without a correlation is rejected"
2537 );
2538 let correlated = base.with_correlation(CorrelationId::from_u128(2));
2539 validate(&correlated)
2540 .expect("the run-lifecycle status validates once it correlates on the run");
2541 }
2542
2543 #[test]
2544 fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
2545 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2546 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
2547 assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
2548 assert_eq!(
2549 "input-required".parse::<TaskState>().expect("parses"),
2550 TaskState::InputRequired
2551 );
2552 assert!("bogus".parse::<TaskState>().is_err());
2553 }
2554}