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 std::fmt::Display for $name {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 let encoded = $crate::agent::crockford_encode(self.0);
73 f.write_str(std::str::from_utf8(&encoded).expect("crockford output is ASCII"))
75 }
76 }
77
78 impl std::fmt::Debug for $name {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{}({})", stringify!($name), self)
81 }
82 }
83
84 impl std::str::FromStr for $name {
85 type Err = $crate::agent::IdParseError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 $crate::agent::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 serde::Serialize for $name {
105 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106 if serializer.is_human_readable() {
107 serializer.serialize_str(&self.to_string())
108 } else {
109 serializer.serialize_bytes(&self.to_bytes())
110 }
111 }
112 }
113
114 impl<'de> serde::Deserialize<'de> for $name {
115 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 if deserializer.is_human_readable() {
117 struct TextVisitor;
118
119 impl<'de> serde::de::Visitor<'de> for TextVisitor {
120 type Value = $name;
121
122 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.write_str("a 26-character Crockford base32 id or 16 id bytes")
124 }
125
126 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
127 value.parse().map_err(E::custom)
128 }
129
130 fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
131 let payload: [u8; 16] = value
132 .try_into()
133 .map_err(|_| E::invalid_length(value.len(), &self))?;
134 Ok($name::from_bytes(payload))
135 }
136
137 fn visit_byte_buf<E: serde::de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
138 self.visit_bytes(&value)
139 }
140 }
141
142 return deserializer.deserialize_any(TextVisitor);
146 }
147
148 struct BytesVisitor;
149
150 impl<'de> serde::de::Visitor<'de> for BytesVisitor {
151 type Value = $name;
152
153 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str("16 big-endian id bytes")
155 }
156
157 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
158 let payload: [u8; 16] = v
159 .try_into()
160 .map_err(|_| E::invalid_length(v.len(), &self))?;
161 Ok($name::from_bytes(payload))
162 }
163
164 fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
165 self.visit_bytes(&v)
166 }
167
168 fn visit_seq<A: serde::de::SeqAccess<'de>>(
169 self,
170 mut sequence: A,
171 ) -> Result<Self::Value, A::Error> {
172 let mut payload = [0_u8; 16];
173 for (index, byte) in payload.iter_mut().enumerate() {
174 *byte = sequence
175 .next_element()?
176 .ok_or_else(|| serde::de::Error::invalid_length(index, &self))?;
177 }
178 if sequence.next_element::<u8>()?.is_some() {
179 return Err(serde::de::Error::invalid_length(17, &self));
180 }
181 Ok($name::from_bytes(payload))
182 }
183 }
184
185 deserializer.deserialize_bytes(BytesVisitor)
186 }
187 }
188 };
189}
190
191pub(crate) use wire_id;
194
195wire_id!(
196 RecordId
203);
204wire_id!(
205 ConversationId
208);
209wire_id!(
210 CorrelationId
213);
214wire_id!(
215 ChannelId
218);
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
232pub struct LogPosition {
233 pub stream_id: u32,
234 pub topic_id: u32,
235 pub partition_id: u32,
236 pub offset: u64,
237}
238
239const LOG_POSITION_BYTES: usize = 20;
240
241impl LogPosition {
242 pub const fn new(stream_id: u32, topic_id: u32, partition_id: u32, offset: u64) -> Self {
244 Self {
245 stream_id,
246 topic_id,
247 partition_id,
248 offset,
249 }
250 }
251
252 pub fn to_bytes(self) -> [u8; LOG_POSITION_BYTES] {
254 let mut out = [0u8; LOG_POSITION_BYTES];
255 out[0..4].copy_from_slice(&self.stream_id.to_be_bytes());
256 out[4..8].copy_from_slice(&self.topic_id.to_be_bytes());
257 out[8..12].copy_from_slice(&self.partition_id.to_be_bytes());
258 out[12..20].copy_from_slice(&self.offset.to_be_bytes());
259 out
260 }
261
262 pub fn from_bytes(payload: [u8; LOG_POSITION_BYTES]) -> Self {
264 let u32_at = |start: usize| {
265 u32::from_be_bytes(payload[start..start + 4].try_into().expect("4-byte slice"))
266 };
267 Self {
268 stream_id: u32_at(0),
269 topic_id: u32_at(4),
270 partition_id: u32_at(8),
271 offset: u64::from_be_bytes(payload[12..20].try_into().expect("8-byte slice")),
272 }
273 }
274}
275
276impl Serialize for LogPosition {
280 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
281 serializer.serialize_bytes(&self.to_bytes())
282 }
283}
284
285impl<'de> Deserialize<'de> for LogPosition {
286 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
287 struct LocatorVisitor;
288
289 impl<'de> Visitor<'de> for LocatorVisitor {
290 type Value = LogPosition;
291
292 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.write_str("20 packed locator bytes")
294 }
295
296 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
297 let payload: [u8; LOG_POSITION_BYTES] = v
298 .try_into()
299 .map_err(|_| E::invalid_length(v.len(), &self))?;
300 Ok(LogPosition::from_bytes(payload))
301 }
302
303 fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
304 self.visit_bytes(&v)
305 }
306
307 fn visit_seq<A: de::SeqAccess<'de>>(
308 self,
309 mut sequence: A,
310 ) -> Result<Self::Value, A::Error> {
311 let mut payload = [0_u8; LOG_POSITION_BYTES];
312 for (index, byte) in payload.iter_mut().enumerate() {
313 *byte = sequence
314 .next_element()?
315 .ok_or_else(|| de::Error::invalid_length(index, &self))?;
316 }
317 if sequence.next_element::<u8>()?.is_some() {
318 return Err(de::Error::invalid_length(LOG_POSITION_BYTES + 1, &self));
319 }
320 Ok(LogPosition::from_bytes(payload))
321 }
322 }
323
324 deserializer.deserialize_bytes(LocatorVisitor)
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[serde(try_from = "String", into = "String")]
335pub struct IdempotencyKey(String);
336
337impl IdempotencyKey {
338 pub fn as_str(&self) -> &str {
340 &self.0
341 }
342}
343
344impl FromStr for IdempotencyKey {
345 type Err = InvalidError;
346
347 fn from_str(s: &str) -> Result<Self, Self::Err> {
348 s.to_owned().try_into()
349 }
350}
351
352impl TryFrom<String> for IdempotencyKey {
353 type Error = InvalidError;
354
355 fn try_from(value: String) -> Result<Self, Self::Error> {
356 if value.is_empty() {
357 return Err(InvalidError::new("idempotency key must not be empty"));
358 }
359 if value.len() > crate::limits::MAX_IDEMPOTENCY_KEY_BYTES {
360 return Err(InvalidError::new(format!(
361 "idempotency key is {}B, exceeds cap {}B",
362 value.len(),
363 crate::limits::MAX_IDEMPOTENCY_KEY_BYTES
364 )));
365 }
366 Ok(Self(value))
367 }
368}
369
370impl From<IdempotencyKey> for String {
371 fn from(value: IdempotencyKey) -> Self {
372 value.0
373 }
374}
375
376impl fmt::Display for IdempotencyKey {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.write_str(&self.0)
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
392#[serde(try_from = "String", into = "String")]
393pub struct AgentId(String);
394
395impl AgentId {
396 pub fn as_str(&self) -> &str {
398 &self.0
399 }
400}
401
402impl FromStr for AgentId {
403 type Err = InvalidError;
404
405 fn from_str(s: &str) -> Result<Self, Self::Err> {
406 s.to_owned().try_into()
407 }
408}
409
410impl TryFrom<String> for AgentId {
411 type Error = InvalidError;
412
413 fn try_from(value: String) -> Result<Self, Self::Error> {
414 if value.is_empty() {
415 return Err(InvalidError::new("agent id must not be empty"));
416 }
417 if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
418 return Err(InvalidError::new(format!(
419 "agent id is {}B, exceeds cap {}B",
420 value.len(),
421 crate::limits::MAX_AGENT_STRING_BYTES
422 )));
423 }
424 if let Some(c) = value.chars().find(|c| c.is_control()) {
425 return Err(InvalidError::new(format!(
426 "agent id must not contain control characters (found {c:?})"
427 )));
428 }
429 Ok(Self(value))
430 }
431}
432
433impl TryFrom<&str> for AgentId {
434 type Error = InvalidError;
435
436 fn try_from(value: &str) -> Result<Self, Self::Error> {
437 value.to_owned().try_into()
438 }
439}
440
441impl From<AgentId> for String {
442 fn from(value: AgentId) -> Self {
443 value.0
444 }
445}
446
447impl fmt::Display for AgentId {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 f.write_str(&self.0)
450 }
451}
452
453#[derive(
457 Clone,
458 Copy,
459 Debug,
460 PartialEq,
461 Eq,
462 Hash,
463 Serialize,
464 Deserialize,
465 strum::Display,
466 strum::EnumString,
467)]
468#[serde(rename_all = "snake_case")]
469#[strum(serialize_all = "snake_case")]
470pub enum AgentKind {
471 Command,
474 Response,
476 Event,
478 Chunk,
480 Status,
483 Error,
485}
486
487#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
499#[serde(from = "u8", into = "u8")]
500pub enum TaskState {
501 Submitted,
502 Working,
503 InputRequired,
504 Completed,
505 Canceled,
506 Failed,
507 Rejected,
508 AuthRequired,
509 Unknown,
510 Unrecognized(u8),
512}
513
514impl TaskState {
515 pub const fn code(self) -> u8 {
517 match self {
518 TaskState::Submitted => 1,
519 TaskState::Working => 2,
520 TaskState::InputRequired => 3,
521 TaskState::Completed => 4,
522 TaskState::Canceled => 5,
523 TaskState::Failed => 6,
524 TaskState::Rejected => 7,
525 TaskState::AuthRequired => 8,
526 TaskState::Unknown => 9,
527 TaskState::Unrecognized(code) => code,
528 }
529 }
530
531 pub const fn from_code(code: u8) -> Self {
534 match code {
535 1 => TaskState::Submitted,
536 2 => TaskState::Working,
537 3 => TaskState::InputRequired,
538 4 => TaskState::Completed,
539 5 => TaskState::Canceled,
540 6 => TaskState::Failed,
541 7 => TaskState::Rejected,
542 8 => TaskState::AuthRequired,
543 9 => TaskState::Unknown,
544 other => TaskState::Unrecognized(other),
545 }
546 }
547
548 pub const fn is_terminal(self) -> bool {
551 matches!(
552 self,
553 TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
554 )
555 }
556}
557
558impl From<u8> for TaskState {
559 fn from(code: u8) -> Self {
560 Self::from_code(code)
561 }
562}
563
564impl From<TaskState> for u8 {
565 fn from(state: TaskState) -> u8 {
566 state.code()
567 }
568}
569
570impl fmt::Display for TaskState {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 match self {
574 TaskState::Submitted => f.write_str("submitted"),
575 TaskState::Working => f.write_str("working"),
576 TaskState::InputRequired => f.write_str("input-required"),
577 TaskState::Completed => f.write_str("completed"),
578 TaskState::Canceled => f.write_str("canceled"),
579 TaskState::Failed => f.write_str("failed"),
580 TaskState::Rejected => f.write_str("rejected"),
581 TaskState::AuthRequired => f.write_str("auth-required"),
582 TaskState::Unknown => f.write_str("unknown"),
583 TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
584 }
585 }
586}
587
588impl FromStr for TaskState {
589 type Err = InvalidError;
590
591 fn from_str(s: &str) -> Result<Self, Self::Err> {
592 Ok(match s {
593 "submitted" => TaskState::Submitted,
594 "working" => TaskState::Working,
595 "input-required" => TaskState::InputRequired,
596 "completed" => TaskState::Completed,
597 "canceled" => TaskState::Canceled,
598 "failed" => TaskState::Failed,
599 "rejected" => TaskState::Rejected,
600 "auth-required" => TaskState::AuthRequired,
601 "unknown" => TaskState::Unknown,
602 other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
603 })
604 }
605}
606
607#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
614pub struct TokenUsage {
615 pub input_tokens: u64,
616 pub output_tokens: u64,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub reasoning_output_tokens: Option<u64>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub cache_read_input_tokens: Option<u64>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub cache_creation_input_tokens: Option<u64>,
623}
624
625#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
628#[serde(from = "u8", into = "u8")]
629pub enum AgentErrorCode {
630 InvalidRequest,
631 Unauthorized,
632 Unsupported,
633 DeadlineExceeded,
634 Cancelled,
635 ToolFailure,
636 Internal,
637 Unrecognized(u8),
639}
640
641impl AgentErrorCode {
642 pub const fn code(self) -> u8 {
644 match self {
645 AgentErrorCode::InvalidRequest => 1,
646 AgentErrorCode::Unauthorized => 2,
647 AgentErrorCode::Unsupported => 3,
648 AgentErrorCode::DeadlineExceeded => 4,
649 AgentErrorCode::Cancelled => 5,
650 AgentErrorCode::ToolFailure => 6,
651 AgentErrorCode::Internal => 7,
652 AgentErrorCode::Unrecognized(code) => code,
653 }
654 }
655
656 pub const fn from_code(code: u8) -> Self {
658 match code {
659 1 => AgentErrorCode::InvalidRequest,
660 2 => AgentErrorCode::Unauthorized,
661 3 => AgentErrorCode::Unsupported,
662 4 => AgentErrorCode::DeadlineExceeded,
663 5 => AgentErrorCode::Cancelled,
664 6 => AgentErrorCode::ToolFailure,
665 7 => AgentErrorCode::Internal,
666 other => AgentErrorCode::Unrecognized(other),
667 }
668 }
669}
670
671impl From<u8> for AgentErrorCode {
672 fn from(code: u8) -> Self {
673 Self::from_code(code)
674 }
675}
676
677impl From<AgentErrorCode> for u8 {
678 fn from(code: AgentErrorCode) -> u8 {
679 code.code()
680 }
681}
682
683#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
687pub struct AgentErrorBody {
688 pub code: AgentErrorCode,
689 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub message: Option<String>,
691 #[serde(default)]
692 pub retryable: bool,
693 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub detail: Option<BTreeMap<String, Value>>,
695}
696
697#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
699#[serde(from = "u8", into = "u8")]
700pub enum DeadLetterReason {
701 RetryExhausted,
702 Rejected,
703 DecodeFailed,
704 DeadlineExceeded,
705 Unrecognized(u8),
707}
708
709impl DeadLetterReason {
710 pub const fn code(self) -> u8 {
712 match self {
713 DeadLetterReason::RetryExhausted => 1,
714 DeadLetterReason::Rejected => 2,
715 DeadLetterReason::DecodeFailed => 3,
716 DeadLetterReason::DeadlineExceeded => 4,
717 DeadLetterReason::Unrecognized(code) => code,
718 }
719 }
720
721 pub const fn from_code(code: u8) -> Self {
723 match code {
724 1 => DeadLetterReason::RetryExhausted,
725 2 => DeadLetterReason::Rejected,
726 3 => DeadLetterReason::DecodeFailed,
727 4 => DeadLetterReason::DeadlineExceeded,
728 other => DeadLetterReason::Unrecognized(other),
729 }
730 }
731}
732
733impl From<u8> for DeadLetterReason {
734 fn from(code: u8) -> Self {
735 Self::from_code(code)
736 }
737}
738
739impl From<DeadLetterReason> for u8 {
740 fn from(reason: DeadLetterReason) -> u8 {
741 reason.code()
742 }
743}
744
745#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
755pub struct AgentDeadLetter {
756 pub source: LogPosition,
757 pub reason: DeadLetterReason,
758 pub attempts: u32,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub detail: Option<String>,
761 #[serde(with = "crate::encoding::bin_bytes")]
762 pub payload: Vec<u8>,
763}
764
765#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
768#[serde(from = "u8", into = "u8")]
769pub enum Health {
770 Healthy,
771 Degraded,
772 Unavailable,
773 Unrecognized(u8),
775}
776
777impl Health {
778 pub const fn code(self) -> u8 {
780 match self {
781 Health::Healthy => 1,
782 Health::Degraded => 2,
783 Health::Unavailable => 3,
784 Health::Unrecognized(code) => code,
785 }
786 }
787
788 pub const fn from_code(code: u8) -> Self {
791 match code {
792 1 => Health::Healthy,
793 2 => Health::Degraded,
794 3 => Health::Unavailable,
795 other => Health::Unrecognized(other),
796 }
797 }
798}
799
800impl From<u8> for Health {
801 fn from(code: u8) -> Self {
802 Self::from_code(code)
803 }
804}
805
806impl From<Health> for u8 {
807 fn from(health: Health) -> u8 {
808 health.code()
809 }
810}
811
812#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
820#[serde(rename_all = "snake_case")]
821pub enum ContentRef {
822 ContentType(crate::content::ContentType),
823 SchemaId(String),
824}
825
826#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
830pub struct CapabilityDescriptor {
831 pub skill_id: String,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub input: Option<ContentRef>,
835 #[serde(default, skip_serializing_if = "Option::is_none")]
836 pub output: Option<ContentRef>,
837 #[serde(default, skip_serializing_if = "Option::is_none")]
839 pub cost_class: Option<u8>,
840 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub latency_class: Option<u8>,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
844 pub max_concurrency: Option<u32>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub health: Option<Health>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
849 pub load: Option<u16>,
850}
851
852#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
860pub struct AgentCard {
861 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub name: Option<String>,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
867 pub version: Option<String>,
868 #[serde(default, skip_serializing_if = "Vec::is_empty")]
871 pub capabilities: Vec<CapabilityDescriptor>,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub ttl_micros: Option<u64>,
876}
877
878impl AgentCard {
879 pub fn validate(&self) -> Result<(), ValidateError> {
881 cap_str(self.name.as_deref(), "name")?;
882 cap_str(self.version.as_deref(), "version")?;
883 if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
884 return Err(ValidateError::TooLarge {
885 field: "capabilities",
886 size: self.capabilities.len(),
887 cap: crate::limits::MAX_CARD_CAPABILITIES,
888 });
889 }
890 for capability in &self.capabilities {
891 cap_str(Some(&capability.skill_id), "capability skill_id")?;
892 }
893 Ok(())
894 }
895}
896
897#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
915pub struct AgentPresence {
916 pub v: u32,
919 pub agent: AgentId,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
928 pub inbox: Option<String>,
929}
930
931impl AgentPresence {
932 pub fn new(agent: AgentId) -> Self {
935 Self {
936 v: crate::codes::PRESENCE_OP_VERSION,
937 agent,
938 inbox: None,
939 }
940 }
941
942 pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
944 self.inbox = Some(inbox.into());
945 self
946 }
947
948 pub fn validate(&self) -> Result<(), ValidateError> {
951 cap_str(self.inbox.as_deref(), "inbox")?;
952 Ok(())
953 }
954}
955
956#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
965pub struct BodyRef {
966 pub reference: String,
969 pub size_bytes: u64,
971 #[serde(with = "crate::encoding::bin_bytes")]
973 pub sha256: Vec<u8>,
974 #[serde(default, skip_serializing_if = "Option::is_none")]
978 pub encryption: Option<u8>,
979}
980
981const SHA256_BYTES: usize = 32;
982
983impl BodyRef {
984 pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
986 Self {
987 reference: reference.into(),
988 size_bytes,
989 sha256: sha256.to_vec(),
990 encryption: None,
991 }
992 }
993
994 pub fn validate(&self) -> Result<(), ValidateError> {
998 if self.reference.is_empty() {
999 return Err(ValidateError::Invalid {
1000 field: "reference",
1001 reason: "reference must not be empty".to_owned(),
1002 });
1003 }
1004 if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
1005 return Err(ValidateError::TooLarge {
1006 field: "reference",
1007 size: self.reference.len(),
1008 cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
1009 });
1010 }
1011 if self.sha256.len() != SHA256_BYTES {
1012 return Err(ValidateError::Invalid {
1013 field: "sha256",
1014 reason: format!(
1015 "digest must be {SHA256_BYTES} bytes, got {}",
1016 self.sha256.len()
1017 ),
1018 });
1019 }
1020 Ok(())
1021 }
1022}
1023
1024#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1036pub struct Signature {
1037 pub scheme: u8,
1040 #[serde(with = "crate::encoding::bin_bytes")]
1042 pub key_id: Vec<u8>,
1043 #[serde(with = "crate::encoding::bin_bytes")]
1046 pub bytes: Vec<u8>,
1047 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub context: Option<SignatureContext>,
1054}
1055
1056#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1062pub struct SignatureContext {
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub content_type: Option<u8>,
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1068 pub agent_version: Option<u32>,
1069}
1070
1071pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
1073
1074pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
1079
1080const ED25519_KEY_ID_BYTES: usize = 8;
1081const ED25519_SIGNATURE_BYTES: usize = 64;
1082
1083impl Signature {
1084 pub fn validate(&self) -> Result<(), ValidateError> {
1089 if self.scheme != SIGNATURE_SCHEME_ED25519 {
1090 return Ok(());
1091 }
1092 if self.key_id.len() != ED25519_KEY_ID_BYTES {
1093 return Err(ValidateError::Invalid {
1094 field: "key_id",
1095 reason: format!(
1096 "Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
1097 self.key_id.len()
1098 ),
1099 });
1100 }
1101 if self.bytes.len() != ED25519_SIGNATURE_BYTES {
1102 return Err(ValidateError::Invalid {
1103 field: "bytes",
1104 reason: format!(
1105 "Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
1106 self.bytes.len()
1107 ),
1108 });
1109 }
1110 Ok(())
1111 }
1112}
1113
1114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1127pub struct AgentEnvelope {
1128 pub kind: AgentKind,
1129 #[serde(default, skip_serializing_if = "Option::is_none")]
1133 pub record: Option<RecordId>,
1134 pub conversation: ConversationId,
1136 pub source: AgentId,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub target: Option<AgentId>,
1143 #[serde(default, skip_serializing_if = "Option::is_none")]
1146 pub cause: Option<RecordId>,
1147 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub cause_at: Option<LogPosition>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub correlation: Option<CorrelationId>,
1154 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub channel: Option<ChannelId>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1158 pub idempotency_key: Option<IdempotencyKey>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub deadline_micros: Option<u64>,
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1166 pub sequence: Option<u64>,
1167 #[serde(default, skip_serializing_if = "is_false")]
1171 pub last: bool,
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub finish_reason: Option<String>,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub task_state: Option<TaskState>,
1179 #[serde(default, skip_serializing_if = "Option::is_none")]
1182 pub operation: Option<String>,
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub tool: Option<String>,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub usage: Option<TokenUsage>,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1190 pub metadata: Option<BTreeMap<String, Value>>,
1191 #[serde(default, skip_serializing_if = "is_zero_u64")]
1203 pub must_understand: u64,
1204 #[serde(
1207 default,
1208 skip_serializing_if = "Vec::is_empty",
1209 with = "crate::encoding::bin_bytes"
1210 )]
1211 pub body: Vec<u8>,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub signature: Option<Signature>,
1220}
1221
1222fn is_false(value: &bool) -> bool {
1223 !*value
1224}
1225
1226fn is_zero_u64(value: &u64) -> bool {
1227 *value == 0
1228}
1229
1230pub mod features {
1237 pub const NONE: u64 = 0;
1241}
1242
1243impl AgentEnvelope {
1244 fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
1245 Self {
1246 kind,
1247 record: None,
1248 conversation,
1249 source,
1250 target: None,
1251 cause: None,
1252 cause_at: None,
1253 correlation: None,
1254 channel: None,
1255 idempotency_key: None,
1256 deadline_micros: None,
1257 sequence: None,
1258 last: false,
1259 finish_reason: None,
1260 task_state: None,
1261 operation: None,
1262 tool: None,
1263 usage: None,
1264 metadata: None,
1265 must_understand: 0,
1266 body: Vec::new(),
1267 signature: None,
1268 }
1269 }
1270
1271 #[must_use]
1276 pub fn requiring(mut self, bits: u64) -> Self {
1277 self.must_understand = bits;
1278 self
1279 }
1280
1281 pub fn unmet_requirements(&self, understood: u64) -> u64 {
1286 self.must_understand & !understood
1287 }
1288
1289 pub fn command(
1291 record: RecordId,
1292 conversation: ConversationId,
1293 source: AgentId,
1294 correlation: CorrelationId,
1295 body: Vec<u8>,
1296 ) -> Self {
1297 let mut envelope = Self::base(AgentKind::Command, conversation, source);
1298 envelope.record = Some(record);
1299 envelope.correlation = Some(correlation);
1300 envelope.body = body;
1301 envelope
1302 }
1303
1304 pub fn response(
1306 record: RecordId,
1307 conversation: ConversationId,
1308 source: AgentId,
1309 correlation: CorrelationId,
1310 body: Vec<u8>,
1311 ) -> Self {
1312 let mut envelope = Self::base(AgentKind::Response, conversation, source);
1313 envelope.record = Some(record);
1314 envelope.correlation = Some(correlation);
1315 envelope.body = body;
1316 envelope
1317 }
1318
1319 pub fn event(
1321 record: RecordId,
1322 conversation: ConversationId,
1323 source: AgentId,
1324 body: Vec<u8>,
1325 ) -> Self {
1326 let mut envelope = Self::base(AgentKind::Event, conversation, source);
1327 envelope.record = Some(record);
1328 envelope.body = body;
1329 envelope
1330 }
1331
1332 pub fn chunk(
1335 conversation: ConversationId,
1336 source: AgentId,
1337 correlation: CorrelationId,
1338 channel: ChannelId,
1339 sequence: u64,
1340 body: Vec<u8>,
1341 ) -> Self {
1342 let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
1343 envelope.correlation = Some(correlation);
1344 envelope.channel = Some(channel);
1345 envelope.sequence = Some(sequence);
1346 envelope.body = body;
1347 envelope
1348 }
1349
1350 pub fn status(
1354 record: RecordId,
1355 conversation: ConversationId,
1356 source: AgentId,
1357 operation: impl Into<String>,
1358 ) -> Self {
1359 let mut envelope = Self::base(AgentKind::Status, conversation, source);
1360 envelope.record = Some(record);
1361 envelope.operation = Some(operation.into());
1362 envelope
1363 }
1364
1365 pub fn error(
1368 record: RecordId,
1369 conversation: ConversationId,
1370 source: AgentId,
1371 correlation: CorrelationId,
1372 body: Vec<u8>,
1373 ) -> Self {
1374 let mut envelope = Self::base(AgentKind::Error, conversation, source);
1375 envelope.record = Some(record);
1376 envelope.correlation = Some(correlation);
1377 envelope.body = body;
1378 envelope
1379 }
1380
1381 pub fn with_target(mut self, target: AgentId) -> Self {
1383 self.target = Some(target);
1384 self
1385 }
1386
1387 pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
1391 self.cause = Some(cause);
1392 self.cause_at = cause_at;
1393 self
1394 }
1395
1396 pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
1400 self.correlation = Some(correlation);
1401 self
1402 }
1403
1404 pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
1406 self.idempotency_key = Some(key);
1407 self
1408 }
1409
1410 pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
1413 self.deadline_micros = Some(deadline_micros);
1414 self
1415 }
1416
1417 pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
1420 self.last = true;
1421 self.finish_reason = Some(finish_reason.into());
1422 self
1423 }
1424
1425 pub fn with_task_state(mut self, state: TaskState) -> Self {
1428 self.task_state = Some(state);
1429 self
1430 }
1431
1432 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
1434 self.operation = Some(operation.into());
1435 self
1436 }
1437
1438 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
1440 self.tool = Some(tool.into());
1441 self
1442 }
1443
1444 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
1446 self.usage = Some(usage);
1447 self
1448 }
1449
1450 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1452 self.metadata
1453 .get_or_insert_with(BTreeMap::new)
1454 .insert(key.into(), value.into());
1455 self
1456 }
1457
1458 pub fn with_signature(mut self, signature: Signature) -> Self {
1463 self.signature = Some(signature);
1464 self
1465 }
1466}
1467
1468#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1471#[non_exhaustive]
1472pub enum ValidateError {
1473 #[error("{kind} requires `{field}`")]
1474 Missing {
1475 kind: AgentKind,
1476 field: &'static str,
1477 },
1478 #[error("`{field}` is invalid on {kind}")]
1479 Forbidden {
1480 kind: AgentKind,
1481 field: &'static str,
1482 },
1483 #[error("`{field}` is {size}B, exceeds cap {cap}B")]
1484 TooLarge {
1485 field: &'static str,
1486 size: usize,
1487 cap: usize,
1488 },
1489 #[error("`{field}`: {reason}")]
1490 Invalid { field: &'static str, reason: String },
1491}
1492
1493pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
1518 use AgentKind::*;
1519 let kind = envelope.kind;
1520
1521 let require = |present: bool, field: &'static str| {
1522 if present {
1523 Ok(())
1524 } else {
1525 Err(ValidateError::Missing { kind, field })
1526 }
1527 };
1528 let forbid = |absent: bool, field: &'static str| {
1529 if absent {
1530 Ok(())
1531 } else {
1532 Err(ValidateError::Forbidden { kind, field })
1533 }
1534 };
1535
1536 if kind != Chunk {
1538 require(envelope.record.is_some(), "record")?;
1539 }
1540
1541 match kind {
1543 Command | Response | Chunk | Error => {
1544 require(envelope.correlation.is_some(), "correlation")?
1545 }
1546 Status => {
1547 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1548 require(envelope.correlation.is_some(), "correlation")?;
1549 }
1550 }
1551 Event => {}
1552 }
1553
1554 match kind {
1557 Chunk => {
1558 require(envelope.channel.is_some(), "channel")?;
1559 require(envelope.sequence.is_some(), "sequence")?;
1560 }
1561 Error => {
1562 if envelope.sequence.is_some() && envelope.channel.is_none() {
1563 return Err(ValidateError::Invalid {
1564 field: "sequence",
1565 reason: "sequence requires channel".to_owned(),
1566 });
1567 }
1568 }
1569 _ => {
1570 forbid(envelope.channel.is_none(), "channel")?;
1571 forbid(envelope.sequence.is_none(), "sequence")?;
1572 }
1573 }
1574
1575 if envelope.last && !matches!(kind, Chunk | Status) {
1578 return Err(ValidateError::Forbidden {
1579 kind,
1580 field: "last",
1581 });
1582 }
1583
1584 match kind {
1586 Response => {}
1587 Chunk => {
1588 if envelope.finish_reason.is_some() && !envelope.last {
1589 return Err(ValidateError::Invalid {
1590 field: "finish_reason",
1591 reason: "finish_reason rides only the terminal chunk".to_owned(),
1592 });
1593 }
1594 }
1595 _ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
1596 }
1597
1598 if matches!(kind, Chunk | Status | Error) {
1601 forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
1602 }
1603
1604 if matches!(kind, Response | Event | Status | Error) {
1607 forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
1608 }
1609 if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
1610 return Err(ValidateError::Invalid {
1611 field: "deadline_micros",
1612 reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
1613 });
1614 }
1615
1616 match kind {
1618 Status => {
1619 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1620 require(envelope.task_state.is_some(), "task_state")?;
1621 }
1622 }
1623 Response | Error => {}
1624 _ => forbid(envelope.task_state.is_none(), "task_state")?,
1625 }
1626
1627 match kind {
1633 Status => {
1634 require(envelope.operation.is_some(), "operation")?;
1635 if let Some(operation) = envelope.operation.as_deref()
1636 && !matches!(
1637 operation,
1638 OPERATION_TASK
1639 | OPERATION_CARD
1640 | OPERATION_PROGRESS
1641 | OPERATION_QUARANTINE
1642 | OPERATION_UNQUARANTINE
1643 )
1644 {
1645 return Err(ValidateError::Invalid {
1646 field: "operation",
1647 reason: format!(
1648 "status operation must be `{OPERATION_TASK}`, `{OPERATION_CARD}`, \
1649 `{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
1650 `{OPERATION_UNQUARANTINE}`, got `{operation}`"
1651 ),
1652 });
1653 }
1654 }
1655 Chunk => {
1656 if envelope.sequence == Some(0) {
1657 require(envelope.operation.is_some(), "operation")?;
1658 }
1659 if let Some(operation) = envelope.operation.as_deref() {
1660 if envelope.sequence != Some(0) {
1661 return Err(ValidateError::Invalid {
1662 field: "operation",
1663 reason: "the stream purpose rides the opening chunk (sequence 0)"
1664 .to_owned(),
1665 });
1666 }
1667 if !matches!(
1668 operation,
1669 OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
1670 ) {
1671 return Err(ValidateError::Invalid {
1672 field: "operation",
1673 reason: format!(
1674 "chunk-stream purpose must be `{OPERATION_CHAT}`, \
1675 `{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
1676 got `{operation}`"
1677 ),
1678 });
1679 }
1680 }
1681 }
1682 Command | Response | Event | Error => {}
1683 }
1684
1685 if kind == Status {
1687 forbid(envelope.tool.is_none(), "tool")?;
1688 }
1689
1690 match kind {
1692 Command => forbid(envelope.usage.is_none(), "usage")?,
1693 Chunk if envelope.usage.is_some() && !envelope.last => {
1694 return Err(ValidateError::Invalid {
1695 field: "usage",
1696 reason: "whole-stream accounting rides the terminal chunk".to_owned(),
1697 });
1698 }
1699 _ => {}
1700 }
1701
1702 match kind {
1704 Status => {}
1705 Chunk => {
1706 if envelope.body.is_empty() && !envelope.last {
1707 return Err(ValidateError::Missing {
1708 kind,
1709 field: "body",
1710 });
1711 }
1712 }
1713 _ => require(!envelope.body.is_empty(), "body")?,
1714 }
1715
1716 cap_str(envelope.operation.as_deref(), "operation")?;
1720 cap_str(envelope.tool.as_deref(), "tool")?;
1721 cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
1722 if let Some(metadata) = &envelope.metadata {
1723 if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
1724 return Err(ValidateError::TooLarge {
1725 field: "metadata",
1726 size: metadata.len(),
1727 cap: crate::limits::MAX_METADATA_ENTRIES,
1728 });
1729 }
1730 let mut total = 0usize;
1731 for (key, value) in metadata {
1732 if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
1733 return Err(ValidateError::TooLarge {
1734 field: "metadata key",
1735 size: key.len(),
1736 cap: crate::limits::MAX_METADATA_KEY_BYTES,
1737 });
1738 }
1739 let value_size = value_size(value);
1740 if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
1741 return Err(ValidateError::TooLarge {
1742 field: "metadata value",
1743 size: value_size,
1744 cap: crate::limits::MAX_METADATA_VALUE_BYTES,
1745 });
1746 }
1747 total += key.len() + value_size;
1748 }
1749 if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
1750 return Err(ValidateError::TooLarge {
1751 field: "metadata",
1752 size: total,
1753 cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
1754 });
1755 }
1756 }
1757 if let Some(signature) = &envelope.signature {
1760 signature.validate()?;
1761 }
1762 Ok(())
1763}
1764
1765pub const OPERATION_TASK: &str = "task";
1767pub const OPERATION_CARD: &str = "card";
1769pub const OPERATION_PROGRESS: &str = "progress";
1771pub const OPERATION_QUARANTINE: &str = "quarantine";
1776pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
1781
1782pub const OPERATION_CHAT: &str = "chat";
1789pub const OPERATION_REASONING: &str = "reasoning";
1791pub const OPERATION_TOOL_ARGS: &str = "tool_args";
1793
1794pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
1800pub const OPERATION_STATE_DELTA: &str = "state_delta";
1803
1804pub const METADATA_ROLE: &str = "role";
1811pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
1817pub const METADATA_RUN: &str = "run";
1823
1824pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
1828pub const METADATA_PURPOSE: &str = "purpose";
1831pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
1834pub const METADATA_TASK_CONTEXT: &str = "task_context";
1837pub const METADATA_SESSION_INTENT: &str = "session_intent";
1840
1841const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
1845
1846pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
1847 let mut out = [0u8; 26];
1848 let mut v = value;
1849 for slot in out.iter_mut().rev() {
1850 *slot = CROCKFORD[(v & 0x1f) as usize];
1851 v >>= 5;
1852 }
1853 out
1854}
1855
1856pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
1857 let bytes = s.as_bytes();
1858 if bytes.len() != 26 {
1859 return Err(IdParseError::Length { got: bytes.len() });
1860 }
1861 let mut value: u128 = 0;
1862 for (i, byte) in bytes.iter().enumerate() {
1863 let digit = CROCKFORD
1864 .iter()
1865 .position(|c| *c == byte.to_ascii_uppercase())
1866 .ok_or(IdParseError::Char(*byte as char))?;
1867 if i == 0 && digit > 7 {
1870 return Err(IdParseError::Overflow);
1871 }
1872 value = (value << 5) | digit as u128;
1873 }
1874 Ok(value)
1875}
1876
1877fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
1878 if let Some(value) = value
1879 && value.len() > crate::limits::MAX_AGENT_STRING_BYTES
1880 {
1881 return Err(ValidateError::TooLarge {
1882 field,
1883 size: value.len(),
1884 cap: crate::limits::MAX_AGENT_STRING_BYTES,
1885 });
1886 }
1887 Ok(())
1888}
1889
1890fn value_size(value: &Value) -> usize {
1893 match value {
1894 Value::Str(s) => s.len(),
1895 Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
1896 _ => 9,
1897 }
1898}
1899
1900#[cfg(test)]
1901mod tests {
1902 use super::*;
1903
1904 #[test]
1905 fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
1906 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
1907 let text = id.to_string();
1908 assert_eq!(text.len(), 26);
1909 assert_eq!(text.parse::<RecordId>().expect("parses"), id);
1910 assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
1912 assert_eq!(
1913 RecordId::from_u128(0).to_string(),
1914 "00000000000000000000000000"
1915 );
1916 assert_eq!(
1917 RecordId::from_u128(u128::MAX).to_string(),
1918 "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
1919 );
1920 }
1921
1922 #[test]
1923 fn given_an_id_in_json_when_encoded_then_should_use_readable_text() {
1924 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
1925 let json = serde_json::to_string(&id).expect("serializes");
1926 assert_eq!(json, format!("\"{id}\""));
1927 assert_eq!(
1928 serde_json::from_str::<RecordId>(&json).expect("deserializes"),
1929 id
1930 );
1931 let byte_array = serde_json::to_string(&[0_u8; 16]).expect("serializes byte array");
1932 assert!(serde_json::from_str::<RecordId>(&byte_array).is_err());
1933 }
1934
1935 #[cfg(feature = "cbor")]
1936 #[test]
1937 fn given_an_id_in_named_cbor_when_encoded_then_should_use_fixed_width_bytes() {
1938 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
1939 let encoded = crate::framing::encode_named(&id).expect("serializes");
1940 let decoded = crate::framing::decode_named::<RecordId>(&encoded).expect("deserializes");
1941 assert_eq!(decoded, id);
1942 }
1943
1944 #[test]
1945 fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
1946 assert_eq!(
1947 "short".parse::<RecordId>(),
1948 Err(IdParseError::Length { got: 5 })
1949 );
1950 assert_eq!(
1951 "8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
1952 Err(IdParseError::Overflow)
1953 );
1954 assert!(matches!(
1955 "UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
1956 Err(IdParseError::Char('U'))
1957 ));
1958 }
1959
1960 #[test]
1961 fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
1962 for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
1963 assert_eq!(
1964 s.parse::<AgentId>()
1965 .expect("a printable agent id is valid")
1966 .as_str(),
1967 s
1968 );
1969 }
1970 assert!("".parse::<AgentId>().is_err());
1971 assert!("bad\nid".parse::<AgentId>().is_err());
1972 }
1973
1974 #[test]
1975 fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
1976 let expected = [
1977 (TaskState::Submitted, 1u8),
1978 (TaskState::Working, 2),
1979 (TaskState::InputRequired, 3),
1980 (TaskState::Completed, 4),
1981 (TaskState::Canceled, 5),
1982 (TaskState::Failed, 6),
1983 (TaskState::Rejected, 7),
1984 (TaskState::AuthRequired, 8),
1985 (TaskState::Unknown, 9),
1986 ];
1987 for (state, code) in expected {
1988 assert_eq!(state.code(), code);
1989 assert_eq!(TaskState::from_code(code), state);
1990 }
1991 let future = TaskState::from_code(42);
1993 assert_eq!(future, TaskState::Unrecognized(42));
1994 assert_eq!(future.code(), 42);
1995 assert!(!future.is_terminal());
1996 assert!(TaskState::Completed.is_terminal());
1997 assert!(!TaskState::Working.is_terminal());
1998 }
1999
2000 #[test]
2001 fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
2002 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2003 assert_eq!(
2004 "auth-required".parse::<TaskState>().expect("parses"),
2005 TaskState::AuthRequired
2006 );
2007 assert!("nope".parse::<TaskState>().is_err());
2008 }
2009
2010 #[test]
2011 fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
2012 assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
2013 assert_eq!(AgentErrorCode::Internal.code(), 7);
2014 assert_eq!(
2015 AgentErrorCode::from_code(99),
2016 AgentErrorCode::Unrecognized(99)
2017 );
2018 assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
2019 assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
2020 assert_eq!(
2021 DeadLetterReason::from_code(77),
2022 DeadLetterReason::Unrecognized(77)
2023 );
2024 }
2025
2026 #[test]
2027 fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
2028 for code in 0u8..=u8::MAX {
2032 assert_eq!(TaskState::from_code(code).code(), code);
2033 assert_eq!(AgentErrorCode::from_code(code).code(), code);
2034 assert_eq!(DeadLetterReason::from_code(code).code(), code);
2035 }
2036 }
2037
2038 #[test]
2039 fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
2040 assert!("job-123-attempt-2".parse::<IdempotencyKey>().is_ok());
2041 assert!("".parse::<IdempotencyKey>().is_err());
2042 assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
2043 }
2044
2045 #[test]
2046 fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
2047 let (record, conversation, source, correlation) = ids();
2048 let command =
2049 AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
2050 validate(&command).expect("a well-formed command validates");
2051
2052 let mut missing = command.clone();
2054 missing.correlation = None;
2055 assert_eq!(
2056 validate(&missing),
2057 Err(ValidateError::Missing {
2058 kind: AgentKind::Command,
2059 field: "correlation"
2060 })
2061 );
2062
2063 let with_usage = command.clone().with_usage(TokenUsage::default());
2065 assert!(matches!(
2066 validate(&with_usage),
2067 Err(ValidateError::Forbidden { field: "usage", .. })
2068 ));
2069
2070 let mut with_channel = command;
2072 with_channel.channel = Some(ChannelId::from_u128(1));
2073 assert!(matches!(
2074 validate(&with_channel),
2075 Err(ValidateError::Forbidden {
2076 field: "channel",
2077 ..
2078 })
2079 ));
2080 }
2081
2082 #[test]
2083 fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
2084 let (_, conversation, source, correlation) = ids();
2085 let channel = ChannelId::from_u128(23);
2086 let chunk = AgentEnvelope::chunk(
2087 conversation,
2088 source.clone(),
2089 correlation,
2090 channel,
2091 0,
2092 b"tok".to_vec(),
2093 )
2094 .with_operation(OPERATION_CHAT);
2095 validate(&chunk).expect("a stream chunk validates");
2096
2097 let mut undeclared = chunk.clone();
2099 undeclared.operation = None;
2100 assert!(matches!(
2101 validate(&undeclared),
2102 Err(ValidateError::Missing {
2103 field: "operation",
2104 ..
2105 })
2106 ));
2107 let mut off_vocabulary = chunk.clone();
2108 off_vocabulary.operation = Some("telemetry".to_owned());
2109 assert!(matches!(
2110 validate(&off_vocabulary),
2111 Err(ValidateError::Invalid {
2112 field: "operation",
2113 ..
2114 })
2115 ));
2116
2117 let redeclared = AgentEnvelope::chunk(
2119 conversation,
2120 source.clone(),
2121 correlation,
2122 channel,
2123 3,
2124 b"tok".to_vec(),
2125 )
2126 .with_operation(OPERATION_REASONING);
2127 assert!(matches!(
2128 validate(&redeclared),
2129 Err(ValidateError::Invalid {
2130 field: "operation",
2131 ..
2132 })
2133 ));
2134
2135 let terminal = AgentEnvelope::chunk(
2137 conversation,
2138 source.clone(),
2139 correlation,
2140 channel,
2141 41,
2142 Vec::new(),
2143 )
2144 .terminal("stop")
2145 .with_usage(TokenUsage {
2146 input_tokens: 100,
2147 output_tokens: 42,
2148 ..Default::default()
2149 });
2150 validate(&terminal).expect("a terminal chunk validates");
2151
2152 let mut early_finish = chunk.clone();
2154 early_finish.finish_reason = Some("stop".to_owned());
2155 assert!(matches!(
2156 validate(&early_finish),
2157 Err(ValidateError::Invalid {
2158 field: "finish_reason",
2159 ..
2160 })
2161 ));
2162
2163 let empty = AgentEnvelope::chunk(
2165 conversation,
2166 source.clone(),
2167 correlation,
2168 channel,
2169 1,
2170 Vec::new(),
2171 );
2172 assert!(matches!(
2173 validate(&empty),
2174 Err(ValidateError::Missing { field: "body", .. })
2175 ));
2176
2177 let mut keyed = chunk.clone();
2179 keyed.idempotency_key = Some("k".parse().expect("valid key"));
2180 assert!(matches!(
2181 validate(&keyed),
2182 Err(ValidateError::Forbidden {
2183 field: "idempotency_key",
2184 ..
2185 })
2186 ));
2187
2188 let opening = chunk.with_deadline_micros(1);
2190 validate(&opening).expect("an opening chunk may declare the bound");
2191 let late = AgentEnvelope::chunk(
2192 conversation,
2193 source.clone(),
2194 correlation,
2195 channel,
2196 5,
2197 b"tok".to_vec(),
2198 )
2199 .with_deadline_micros(1);
2200 assert!(matches!(
2201 validate(&late),
2202 Err(ValidateError::Invalid {
2203 field: "deadline_micros",
2204 ..
2205 })
2206 ));
2207 }
2208
2209 #[test]
2210 fn given_status_signals_when_validated_then_task_updates_should_require_state() {
2211 let (record, conversation, source, correlation) = ids();
2212 let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
2214 validate(&card).expect("a card validates");
2215
2216 let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
2218 assert!(matches!(
2219 validate(&bare_task),
2220 Err(ValidateError::Missing {
2221 field: "correlation",
2222 ..
2223 })
2224 ));
2225 let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
2226 .with_correlation(correlation)
2227 .with_task_state(TaskState::Working);
2228 validate(&task).expect("a task update validates");
2229
2230 let off_vocabulary =
2232 AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
2233 assert!(matches!(
2234 validate(&off_vocabulary),
2235 Err(ValidateError::Invalid {
2236 field: "operation",
2237 ..
2238 })
2239 ));
2240 }
2241
2242 #[test]
2243 fn given_an_error_when_validated_then_last_should_be_forbidden() {
2244 let (record, conversation, source, correlation) = ids();
2245 let error =
2246 AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
2247 validate(&error).expect("an error validates");
2248
2249 let mut flagged = error.clone();
2251 flagged.last = true;
2252 assert!(matches!(
2253 validate(&flagged),
2254 Err(ValidateError::Forbidden { field: "last", .. })
2255 ));
2256
2257 let mut dangling = error;
2259 dangling.sequence = Some(3);
2260 assert!(matches!(
2261 validate(&dangling),
2262 Err(ValidateError::Invalid {
2263 field: "sequence",
2264 ..
2265 })
2266 ));
2267 }
2268
2269 fn descriptor(skill_id: &str) -> CapabilityDescriptor {
2270 CapabilityDescriptor {
2271 skill_id: skill_id.to_owned(),
2272 input: None,
2273 output: None,
2274 cost_class: None,
2275 latency_class: None,
2276 max_concurrency: None,
2277 health: None,
2278 load: None,
2279 }
2280 }
2281
2282 #[test]
2283 fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
2284 let card = AgentCard {
2285 name: Some("rollout-planner".to_owned()),
2286 version: Some("1.4.2".to_owned()),
2287 capabilities: vec![
2288 CapabilityDescriptor {
2289 skill_id: "chat".to_owned(),
2290 input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2291 output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2292 cost_class: Some(2),
2293 latency_class: Some(1),
2294 max_concurrency: Some(8),
2295 health: Some(Health::Healthy),
2296 load: Some(250),
2297 },
2298 descriptor("plan_rollout"),
2299 ],
2300 ttl_micros: Some(30_000_000),
2301 };
2302 card.validate().expect("a well-formed card validates");
2303
2304 let mut crowded = card.clone();
2305 crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
2306 assert!(matches!(
2307 crowded.validate(),
2308 Err(ValidateError::TooLarge {
2309 field: "capabilities",
2310 ..
2311 })
2312 ));
2313
2314 let mut oversized = card;
2315 oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
2316 assert!(matches!(
2317 oversized.validate(),
2318 Err(ValidateError::TooLarge { field: "name", .. })
2319 ));
2320 }
2321
2322 #[cfg(feature = "cbor")]
2323 #[test]
2324 fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
2325 {
2326 use crate::framing::{decode_named, encode_named};
2327 let reference = ContentRef::SchemaId("json".to_owned());
2330 let bytes = encode_named(&reference).expect("encodes");
2331 let back: ContentRef = decode_named(&bytes).expect("decodes");
2332 assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
2333 let ct = ContentRef::ContentType(crate::content::ContentType::Json);
2335 let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
2336 assert_eq!(back, ct);
2337 }
2338
2339 #[test]
2340 fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
2341 let valid = Signature {
2342 scheme: SIGNATURE_SCHEME_ED25519,
2343 key_id: vec![1u8; 8],
2344 bytes: vec![2u8; 64],
2345 context: None,
2346 };
2347 valid
2348 .validate()
2349 .expect("a well-formed Ed25519 signature validates");
2350
2351 let mut short_key = valid.clone();
2352 short_key.key_id = vec![1u8; 4];
2353 assert!(matches!(
2354 short_key.validate(),
2355 Err(ValidateError::Invalid {
2356 field: "key_id",
2357 ..
2358 })
2359 ));
2360
2361 let mut short_signature = valid.clone();
2362 short_signature.bytes = vec![2u8; 32];
2363 assert!(matches!(
2364 short_signature.validate(),
2365 Err(ValidateError::Invalid { field: "bytes", .. })
2366 ));
2367
2368 let future = Signature {
2371 scheme: 42,
2372 key_id: vec![1u8; 3],
2373 bytes: vec![2u8; 99],
2374 context: None,
2375 };
2376 future.validate().expect("an unknown scheme flows through");
2377 }
2378
2379 #[test]
2380 fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
2381 let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
2382 valid.validate().expect("a well-formed reference validates");
2383
2384 let mut empty = valid.clone();
2385 empty.reference = String::new();
2386 assert!(matches!(
2387 empty.validate(),
2388 Err(ValidateError::Invalid {
2389 field: "reference",
2390 ..
2391 })
2392 ));
2393
2394 let mut oversized = valid.clone();
2395 oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
2396 assert!(matches!(
2397 oversized.validate(),
2398 Err(ValidateError::TooLarge {
2399 field: "reference",
2400 ..
2401 })
2402 ));
2403
2404 let mut truncated = valid;
2405 truncated.sha256 = vec![7u8; 16];
2406 assert!(matches!(
2407 truncated.validate(),
2408 Err(ValidateError::Invalid {
2409 field: "sha256",
2410 ..
2411 })
2412 ));
2413 }
2414
2415 #[test]
2416 fn given_oversized_metadata_when_validated_then_should_reject() {
2417 let (record, conversation, source, correlation) = ids();
2418 let mut command =
2419 AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
2420 for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
2421 command = command.with_metadata(format!("k{i}"), i as i64);
2422 }
2423 assert!(matches!(
2424 validate(&command),
2425 Err(ValidateError::TooLarge {
2426 field: "metadata",
2427 ..
2428 })
2429 ));
2430 }
2431
2432 fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
2433 (
2434 RecordId::from_u128(7),
2435 ConversationId::from_u128(11),
2436 "test-agent".parse().expect("valid agent id"),
2437 CorrelationId::from_u128(17),
2438 )
2439 }
2440}
2441
2442#[cfg(all(test, feature = "cbor"))]
2443mod wire_tests {
2444 use super::*;
2445 use crate::framing::{decode_named, encode_named};
2446
2447 #[test]
2448 fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
2449 let mut valid = vec![0x40 | 20];
2452 valid.extend_from_slice(&[0u8; 20]);
2453 decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
2454
2455 for bad_len in [0u8, 19, 21, 23] {
2456 let mut bytes = vec![0x40 | bad_len];
2457 bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
2458 assert!(
2459 decode_named::<LogPosition>(&bytes).is_err(),
2460 "a {bad_len}-byte locator must error, not panic"
2461 );
2462 }
2463 }
2464
2465 #[test]
2466 fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
2467 let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
2468 let bytes = encode_named(&pos).expect("encodes");
2469 let back: LogPosition = decode_named(&bytes).expect("decodes");
2470 assert_eq!(back, pos);
2471 }
2472
2473 #[test]
2474 fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
2475 let envelope = AgentEnvelope::command(
2476 RecordId::from_u128(1),
2477 ConversationId::from_u128(2),
2478 "source-agent".parse().expect("valid agent id"),
2479 CorrelationId::from_u128(4),
2480 b"payload".to_vec(),
2481 )
2482 .with_target("target-agent".parse().expect("valid agent id"))
2483 .with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
2484 .with_idempotency_key("job-1".parse().expect("valid key"))
2485 .with_deadline_micros(1_700_000_000_000_000)
2486 .with_operation("chat")
2487 .with_tool("search")
2488 .with_metadata("pool", "eu-west");
2489 let bytes = encode_named(&envelope).expect("encodes");
2490 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2491 assert_eq!(back, envelope);
2492 }
2493
2494 #[test]
2495 fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
2496 let envelope = AgentEnvelope::event(
2500 RecordId::from_u128(1),
2501 ConversationId::from_u128(2),
2502 "source-agent".parse().expect("valid agent id"),
2503 b"e".to_vec(),
2504 )
2505 .requiring(0b101);
2506 let bytes = encode_named(&envelope).expect("encodes");
2507 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2508 assert_eq!(back.must_understand, 0b101);
2509 assert_eq!(back.unmet_requirements(0b001), 0b100);
2511 assert_eq!(back.unmet_requirements(0b111), 0);
2513 let plain = AgentEnvelope::event(
2515 RecordId::from_u128(1),
2516 ConversationId::from_u128(2),
2517 "source-agent".parse().expect("valid agent id"),
2518 b"e".to_vec(),
2519 );
2520 assert_eq!(plain.unmet_requirements(features::NONE), 0);
2521 let json = serde_json::to_string(&plain).expect("json");
2522 assert!(
2523 !json.contains("must_understand"),
2524 "zero marker must be omitted: {json}"
2525 );
2526 }
2527
2528 #[test]
2529 fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
2530 let envelope = AgentEnvelope::event(
2533 RecordId::from_u128(1),
2534 ConversationId::from_u128(2),
2535 "source-agent".parse().expect("valid agent id"),
2536 b"e".to_vec(),
2537 );
2538 let bytes = encode_named(&envelope).expect("encodes");
2539 assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
2542 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
2543 assert!(!back.last);
2544 assert!(back.metadata.is_none());
2545 }
2546
2547 #[test]
2548 fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
2549 let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
2550 assert_eq!(bytes.len(), 17);
2553 assert_eq!(bytes[0], 0x50);
2554 assert_eq!(bytes[16], 0x02);
2555 assert_eq!(bytes[15], 0x01);
2556 }
2557
2558 #[test]
2559 fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
2560 let bytes = encode_named(&TaskState::Completed).expect("encodes");
2561 assert_eq!(bytes, vec![4]);
2562 let unknown = encode_named(&42u8).expect("encodes a raw code");
2563 let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
2564 assert_eq!(back, TaskState::Unrecognized(42));
2565 }
2566
2567 #[test]
2568 fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
2569 let body = AgentErrorBody {
2570 code: AgentErrorCode::ToolFailure,
2571 message: Some("search timed out".to_owned()),
2572 retryable: true,
2573 detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
2574 };
2575 let bytes = encode_named(&body).expect("encodes");
2576 let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
2577 assert_eq!(back, body);
2578 }
2579
2580 #[test]
2581 fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
2582 let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
2583 let bytes = encode_named(&capsule).expect("encodes");
2584 let back: BodyRef = decode_named(&bytes).expect("decodes");
2585 assert_eq!(back, capsule);
2586 assert!(back.encryption.is_none(), "absent encryption costs nothing");
2587 back.validate().expect("decoded capsule validates");
2588 }
2589
2590 #[test]
2591 fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
2592 let inner = AgentEnvelope::command(
2593 RecordId::from_u128(9),
2594 ConversationId::from_u128(8),
2595 "source-agent".parse().expect("valid agent id"),
2596 CorrelationId::from_u128(6),
2597 b"poison".to_vec(),
2598 );
2599 let payload = encode_named(&inner).expect("inner encodes");
2600 let capsule = AgentDeadLetter {
2601 source: LogPosition::new(1, 2, 3, 99),
2602 reason: DeadLetterReason::RetryExhausted,
2603 attempts: 5,
2604 detail: Some("handler kept failing".to_owned()),
2605 payload: payload.clone(),
2606 };
2607 let bytes = encode_named(&capsule).expect("encodes");
2608 let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
2609 assert_eq!(back.payload, payload, "redrive needs the original bytes");
2610 let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
2611 assert_eq!(redrive, inner);
2612 }
2613 #[test]
2614 fn given_a_run_lifecycle_status_when_validated_then_should_require_the_correlation() {
2615 let base = AgentEnvelope::status(
2619 RecordId::from_u128(1),
2620 ConversationId::from_u128(2),
2621 "runner".parse().expect("valid agent id"),
2622 OPERATION_TASK,
2623 )
2624 .with_task_state(TaskState::Working)
2625 .with_metadata(METADATA_RUN, "run-1");
2626 assert!(
2627 matches!(
2628 validate(&base),
2629 Err(ValidateError::Missing {
2630 kind: AgentKind::Status,
2631 field: "correlation"
2632 })
2633 ),
2634 "a task status without a correlation is rejected"
2635 );
2636 let correlated = base.with_correlation(CorrelationId::from_u128(2));
2637 validate(&correlated)
2638 .expect("the run-lifecycle status validates once it correlates on the run");
2639 }
2640
2641 #[test]
2642 fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
2643 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2644 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
2645 assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
2646 assert_eq!(
2647 "input-required".parse::<TaskState>().expect("parses"),
2648 TaskState::InputRequired
2649 );
2650 assert!("bogus".parse::<TaskState>().is_err());
2651 }
2652}