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 #[cfg(feature = "schemars")]
105 impl schemars::JsonSchema for $name {
106 fn schema_name() -> std::borrow::Cow<'static, str> {
107 stringify!($name).into()
108 }
109
110 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
111 schemars::json_schema!({
112 "description": "A 128-bit id as 26-character Crockford base32 text",
113 "type": "string",
114 "minLength": 26,
115 "maxLength": 26
116 })
117 }
118 }
119
120 impl serde::Serialize for $name {
121 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
122 if serializer.is_human_readable() {
123 serializer.serialize_str(&self.to_string())
124 } else {
125 serializer.serialize_bytes(&self.to_bytes())
126 }
127 }
128 }
129
130 impl<'de> serde::Deserialize<'de> for $name {
131 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132 if deserializer.is_human_readable() {
133 struct TextVisitor;
134
135 impl<'de> serde::de::Visitor<'de> for TextVisitor {
136 type Value = $name;
137
138 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.write_str("a 26-character Crockford base32 id or 16 id bytes")
140 }
141
142 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
143 value.parse().map_err(E::custom)
144 }
145
146 fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
147 let payload: [u8; 16] = value
148 .try_into()
149 .map_err(|_| E::invalid_length(value.len(), &self))?;
150 Ok($name::from_bytes(payload))
151 }
152
153 fn visit_byte_buf<E: serde::de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
154 self.visit_bytes(&value)
155 }
156 }
157
158 return deserializer.deserialize_any(TextVisitor);
162 }
163
164 struct BytesVisitor;
165
166 impl<'de> serde::de::Visitor<'de> for BytesVisitor {
167 type Value = $name;
168
169 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 f.write_str("16 big-endian id bytes")
171 }
172
173 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
174 let payload: [u8; 16] = v
175 .try_into()
176 .map_err(|_| E::invalid_length(v.len(), &self))?;
177 Ok($name::from_bytes(payload))
178 }
179
180 fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
181 self.visit_bytes(&v)
182 }
183
184 fn visit_seq<A: serde::de::SeqAccess<'de>>(
185 self,
186 mut sequence: A,
187 ) -> Result<Self::Value, A::Error> {
188 let mut payload = [0_u8; 16];
189 for (index, byte) in payload.iter_mut().enumerate() {
190 *byte = sequence
191 .next_element()?
192 .ok_or_else(|| serde::de::Error::invalid_length(index, &self))?;
193 }
194 if sequence.next_element::<u8>()?.is_some() {
195 return Err(serde::de::Error::invalid_length(17, &self));
196 }
197 Ok($name::from_bytes(payload))
198 }
199 }
200
201 deserializer.deserialize_bytes(BytesVisitor)
202 }
203 }
204 };
205}
206
207pub(crate) use wire_id;
210
211wire_id!(
212 RecordId
219);
220wire_id!(
221 ConversationId
224);
225wire_id!(
226 CorrelationId
229);
230wire_id!(
231 ChannelId
234);
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
248pub struct LogPosition {
249 pub stream_id: u32,
250 pub topic_id: u32,
251 pub partition_id: u32,
252 pub offset: u64,
253}
254
255const LOG_POSITION_BYTES: usize = 20;
256
257impl LogPosition {
258 pub const fn new(stream_id: u32, topic_id: u32, partition_id: u32, offset: u64) -> Self {
260 Self {
261 stream_id,
262 topic_id,
263 partition_id,
264 offset,
265 }
266 }
267
268 pub fn to_bytes(self) -> [u8; LOG_POSITION_BYTES] {
270 let mut out = [0u8; LOG_POSITION_BYTES];
271 out[0..4].copy_from_slice(&self.stream_id.to_be_bytes());
272 out[4..8].copy_from_slice(&self.topic_id.to_be_bytes());
273 out[8..12].copy_from_slice(&self.partition_id.to_be_bytes());
274 out[12..20].copy_from_slice(&self.offset.to_be_bytes());
275 out
276 }
277
278 pub fn from_bytes(payload: [u8; LOG_POSITION_BYTES]) -> Self {
280 let u32_at = |start: usize| {
281 u32::from_be_bytes(payload[start..start + 4].try_into().expect("4-byte slice"))
282 };
283 Self {
284 stream_id: u32_at(0),
285 topic_id: u32_at(4),
286 partition_id: u32_at(8),
287 offset: u64::from_be_bytes(payload[12..20].try_into().expect("8-byte slice")),
288 }
289 }
290}
291
292#[cfg(feature = "schemars")]
296impl schemars::JsonSchema for LogPosition {
297 fn schema_name() -> std::borrow::Cow<'static, str> {
298 "LogPosition".into()
299 }
300
301 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
302 crate::encoding::fixed_bytes_schema(generator, LOG_POSITION_BYTES)
303 }
304}
305
306impl Serialize for LogPosition {
307 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
308 serializer.serialize_bytes(&self.to_bytes())
309 }
310}
311
312impl<'de> Deserialize<'de> for LogPosition {
313 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314 struct LocatorVisitor;
315
316 impl<'de> Visitor<'de> for LocatorVisitor {
317 type Value = LogPosition;
318
319 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 f.write_str("20 packed locator bytes")
321 }
322
323 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
324 let payload: [u8; LOG_POSITION_BYTES] = v
325 .try_into()
326 .map_err(|_| E::invalid_length(v.len(), &self))?;
327 Ok(LogPosition::from_bytes(payload))
328 }
329
330 fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
331 self.visit_bytes(&v)
332 }
333
334 fn visit_seq<A: de::SeqAccess<'de>>(
335 self,
336 mut sequence: A,
337 ) -> Result<Self::Value, A::Error> {
338 let mut payload = [0_u8; LOG_POSITION_BYTES];
339 for (index, byte) in payload.iter_mut().enumerate() {
340 *byte = sequence
341 .next_element()?
342 .ok_or_else(|| de::Error::invalid_length(index, &self))?;
343 }
344 if sequence.next_element::<u8>()?.is_some() {
345 return Err(de::Error::invalid_length(LOG_POSITION_BYTES + 1, &self));
346 }
347 Ok(LogPosition::from_bytes(payload))
348 }
349 }
350
351 deserializer.deserialize_bytes(LocatorVisitor)
352 }
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
361#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
362#[serde(try_from = "String", into = "String")]
363pub struct IdempotencyKey(String);
364
365impl IdempotencyKey {
366 pub fn as_str(&self) -> &str {
368 &self.0
369 }
370}
371
372impl FromStr for IdempotencyKey {
373 type Err = InvalidError;
374
375 fn from_str(s: &str) -> Result<Self, Self::Err> {
376 s.to_owned().try_into()
377 }
378}
379
380impl TryFrom<String> for IdempotencyKey {
381 type Error = InvalidError;
382
383 fn try_from(value: String) -> Result<Self, Self::Error> {
384 if value.is_empty() {
385 return Err(InvalidError::new("idempotency key must not be empty"));
386 }
387 if value.len() > crate::limits::MAX_IDEMPOTENCY_KEY_BYTES {
388 return Err(InvalidError::new(format!(
389 "idempotency key is {}B, exceeds cap {}B",
390 value.len(),
391 crate::limits::MAX_IDEMPOTENCY_KEY_BYTES
392 )));
393 }
394 if value.chars().any(char::is_control) {
395 return Err(InvalidError::new(
396 "idempotency key contains a control character",
397 ));
398 }
399 Ok(Self(value))
400 }
401}
402
403impl From<IdempotencyKey> for String {
404 fn from(value: IdempotencyKey) -> Self {
405 value.0
406 }
407}
408
409impl fmt::Display for IdempotencyKey {
410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411 f.write_str(&self.0)
412 }
413}
414
415#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
425#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
426#[serde(try_from = "String", into = "String")]
427pub struct AgentId(String);
428
429impl AgentId {
430 pub fn as_str(&self) -> &str {
432 &self.0
433 }
434}
435
436impl FromStr for AgentId {
437 type Err = InvalidError;
438
439 fn from_str(s: &str) -> Result<Self, Self::Err> {
440 s.to_owned().try_into()
441 }
442}
443
444impl TryFrom<String> for AgentId {
445 type Error = InvalidError;
446
447 fn try_from(value: String) -> Result<Self, Self::Error> {
448 if value.is_empty() {
449 return Err(InvalidError::new("agent id must not be empty"));
450 }
451 if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
452 return Err(InvalidError::new(format!(
453 "agent id is {}B, exceeds cap {}B",
454 value.len(),
455 crate::limits::MAX_AGENT_STRING_BYTES
456 )));
457 }
458 if let Some(c) = value.chars().find(|c| c.is_control()) {
459 return Err(InvalidError::new(format!(
460 "agent id must not contain control characters (found {c:?})"
461 )));
462 }
463 Ok(Self(value))
464 }
465}
466
467impl TryFrom<&str> for AgentId {
468 type Error = InvalidError;
469
470 fn try_from(value: &str) -> Result<Self, Self::Error> {
471 value.to_owned().try_into()
472 }
473}
474
475impl From<AgentId> for String {
476 fn from(value: AgentId) -> Self {
477 value.0
478 }
479}
480
481impl fmt::Display for AgentId {
482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 f.write_str(&self.0)
484 }
485}
486
487#[derive(
491 Clone,
492 Copy,
493 Debug,
494 PartialEq,
495 Eq,
496 Hash,
497 Serialize,
498 Deserialize,
499 strum::Display,
500 strum::EnumString,
501)]
502#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
503#[serde(rename_all = "snake_case")]
504#[strum(serialize_all = "snake_case")]
505pub enum AgentKind {
506 Command,
509 Response,
511 Event,
513 Chunk,
515 Status,
518 Error,
520}
521
522#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
534#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
535#[serde(from = "u8", into = "u8")]
536pub enum TaskState {
537 Submitted,
538 Working,
539 InputRequired,
540 Completed,
541 Canceled,
542 Failed,
543 Rejected,
544 AuthRequired,
545 Unknown,
546 Paused,
548 Unrecognized(u8),
550}
551
552impl TaskState {
553 pub const fn code(self) -> u8 {
555 match self {
556 TaskState::Submitted => 1,
557 TaskState::Working => 2,
558 TaskState::InputRequired => 3,
559 TaskState::Completed => 4,
560 TaskState::Canceled => 5,
561 TaskState::Failed => 6,
562 TaskState::Rejected => 7,
563 TaskState::AuthRequired => 8,
564 TaskState::Unknown => 9,
565 TaskState::Paused => 10,
566 TaskState::Unrecognized(code) => code,
567 }
568 }
569
570 pub const fn from_code(code: u8) -> Self {
573 match code {
574 1 => TaskState::Submitted,
575 2 => TaskState::Working,
576 3 => TaskState::InputRequired,
577 4 => TaskState::Completed,
578 5 => TaskState::Canceled,
579 6 => TaskState::Failed,
580 7 => TaskState::Rejected,
581 8 => TaskState::AuthRequired,
582 9 => TaskState::Unknown,
583 10 => TaskState::Paused,
584 other => TaskState::Unrecognized(other),
585 }
586 }
587
588 pub const fn is_terminal(self) -> bool {
591 matches!(
592 self,
593 TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
594 )
595 }
596}
597
598impl From<u8> for TaskState {
599 fn from(code: u8) -> Self {
600 Self::from_code(code)
601 }
602}
603
604impl From<TaskState> for u8 {
605 fn from(state: TaskState) -> u8 {
606 state.code()
607 }
608}
609
610impl fmt::Display for TaskState {
611 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 match self {
614 TaskState::Submitted => f.write_str("submitted"),
615 TaskState::Working => f.write_str("working"),
616 TaskState::InputRequired => f.write_str("input-required"),
617 TaskState::Completed => f.write_str("completed"),
618 TaskState::Canceled => f.write_str("canceled"),
619 TaskState::Failed => f.write_str("failed"),
620 TaskState::Rejected => f.write_str("rejected"),
621 TaskState::AuthRequired => f.write_str("auth-required"),
622 TaskState::Unknown => f.write_str("unknown"),
623 TaskState::Paused => f.write_str("paused"),
624 TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
625 }
626 }
627}
628
629impl FromStr for TaskState {
630 type Err = InvalidError;
631
632 fn from_str(s: &str) -> Result<Self, Self::Err> {
633 Ok(match s {
634 "submitted" => TaskState::Submitted,
635 "working" => TaskState::Working,
636 "input-required" => TaskState::InputRequired,
637 "completed" => TaskState::Completed,
638 "canceled" => TaskState::Canceled,
639 "failed" => TaskState::Failed,
640 "rejected" => TaskState::Rejected,
641 "auth-required" => TaskState::AuthRequired,
642 "unknown" => TaskState::Unknown,
643 "paused" => TaskState::Paused,
644 other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
645 })
646 }
647}
648
649#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
651#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
652#[serde(rename_all = "snake_case")]
653pub enum SessionStatus {
654 Submitted,
655 Active,
656 Paused,
657 Completed,
658 Failed,
659 Canceled,
660 #[serde(other)]
661 Unrecognized,
662}
663
664#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
666#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
667pub struct SessionRef {
668 pub stream: String,
669 pub session: ConversationId,
670}
671
672#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
674#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
675pub struct SdkInfo {
676 pub language: String,
677 pub version: String,
678}
679
680#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
682#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
683pub struct Budget {
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub tokens: Option<u64>,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub cost_micros: Option<u64>,
688}
689
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
692#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
693pub struct SessionStart {
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub label: Option<String>,
696 #[serde(default, skip_serializing_if = "Option::is_none")]
697 pub namespace: Option<String>,
698 pub agent: AgentId,
699 pub sdk: SdkInfo,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub parent: Option<ConversationId>,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub root: Option<ConversationId>,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub idle_timeout_micros: Option<u64>,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub budget: Option<Budget>,
708 #[serde(default, skip_serializing_if = "Vec::is_empty")]
709 pub tags: Vec<String>,
710}
711
712#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
714#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
715pub struct SessionTransition {
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub actor: Option<AgentId>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub acknowledges: Option<LogPosition>,
720}
721
722#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
727#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
728pub struct SessionPauseRequest {
729 #[serde(default, skip_serializing_if = "Vec::is_empty")]
730 pub participants: Vec<AgentId>,
731}
732
733#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
739#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
740pub struct SessionParking {
741 pub source: crate::graph::SourceRef,
742 pub role: AgentId,
743 pub request: LogPosition,
744}
745
746impl crate::validate::Validate for SessionParking {
747 fn validate(&self) -> Result<(), InvalidError> {
748 match &self.source {
749 crate::graph::SourceRef::Message {
750 generation: Some(_),
751 ..
752 } => Ok(()),
753 crate::graph::SourceRef::Message { .. } => Err(InvalidError::new(
754 "a parked record must name its source topic generation",
755 )),
756 _ => Err(InvalidError::new(
757 "a parked record must point at a log record",
758 )),
759 }
760 }
761}
762
763#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
765#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
766pub struct SessionEnd {
767 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub reason: Option<String>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub error: Option<AgentErrorBody>,
771}
772
773pub const fn estimate_tokens(bytes: usize) -> u64 {
776 bytes.div_ceil(4) as u64
777}
778
779pub const METADATA_REQUEST_MODEL: &str = "gen_ai.request.model";
781pub const METADATA_RESPONSE_MODEL: &str = "gen_ai.response.model";
783pub const METADATA_PROVIDER_NAME: &str = "gen_ai.provider.name";
785pub const METADATA_SUBMITTED: &str = "submitted";
789pub const METADATA_RECORDED: &str = "recorded";
793pub const METADATA_DURATION_MICROS: &str = "duration_micros";
796
797#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
799#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
800pub struct ContextManifest {
801 pub policy: String,
802 pub policy_version: String,
803 pub fragments: Vec<Fragment>,
804 pub tokens: u64,
805 pub bytes: u64,
806 pub frontier: Vec<(u32, u32, u64)>,
807 #[serde(default, skip_serializing_if = "Option::is_none")]
808 pub correlation: Option<CorrelationId>,
809}
810
811#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
813#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
814pub enum Fragment {
815 Message {
816 at: crate::graph::SourceRef,
817 tokens: u32,
818 bytes: u32,
819 },
820 Memory {
821 id: String,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 version: Option<u64>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 digest: Option<crate::schema::Digest32>,
826 tokens: u32,
827 bytes: u32,
828 },
829 Kv {
830 namespace: String,
831 key: String,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 version: Option<u64>,
834 #[serde(default, skip_serializing_if = "Option::is_none")]
835 digest: Option<crate::schema::Digest32>,
836 tokens: u32,
837 bytes: u32,
838 },
839 State {
840 key: String,
841 revision: u64,
842 tokens: u32,
843 bytes: u32,
844 },
845 Summary {
846 at: crate::graph::SourceRef,
847 tokens: u32,
848 bytes: u32,
849 },
850}
851
852#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
854#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
855pub struct ContextCompaction {
856 pub summary_at: crate::graph::SourceRef,
857 pub covered: Vec<(u32, u32, u64, u64)>,
858 pub summarizer: crate::graph::ProducerInfo,
859}
860
861#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
863#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
864pub struct ContextRetrieval {
865 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub query: Option<String>,
867 pub items: Vec<(String, f32)>,
868}
869
870pub use json_patch::PatchOperation;
872
873#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
875#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
876pub struct StateDelta {
877 pub base_revision: u64,
878 #[cfg_attr(
879 feature = "schemars",
880 schemars(schema_with = "crate::encoding::json_patch_schema")
881 )]
882 pub patch: Vec<json_patch::PatchOperation>,
883 pub op_id: String,
884 #[serde(default, skip_serializing_if = "Option::is_none")]
886 pub base_op_id: Option<String>,
887}
888
889#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
891#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
892pub struct StateSnapshot {
893 pub base_revision: u64,
894 pub document: serde_json::Value,
895 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub base_op_id: Option<String>,
898}
899
900impl crate::validate::Validate for ContextManifest {
901 fn validate(&self) -> Result<(), InvalidError> {
902 if self.fragments.len() > crate::limits::MAX_MANIFEST_FRAGMENTS {
903 return Err(InvalidError::new("context manifest has too many fragments"));
904 }
905 for fragment in &self.fragments {
906 let digest = match fragment {
907 Fragment::Memory { digest, .. } | Fragment::Kv { digest, .. } => digest.as_ref(),
908 _ => None,
909 };
910 if let Some(digest) = digest {
911 crate::validate::Validate::validate(digest)?;
912 }
913 }
914 Ok(())
915 }
916}
917
918impl crate::validate::Validate for StateDelta {
919 fn validate(&self) -> Result<(), InvalidError> {
920 if self.patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
921 return Err(InvalidError::new("state patch has too many operations"));
922 }
923 if self.op_id.is_empty() {
924 return Err(InvalidError::new(
925 "state patch operation id must not be empty",
926 ));
927 }
928 let bytes = serde_json::to_vec(&self.patch)
929 .map_err(|error| InvalidError::new(error.to_string()))?;
930 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
931 return Err(InvalidError::new(
932 "state patch exceeds the document byte cap",
933 ));
934 }
935 let patch = serde_json::to_value(&self.patch)
936 .map_err(|error| InvalidError::new(error.to_string()))?;
937 validate_portable_json(&patch)?;
938 Ok(())
939 }
940}
941
942impl crate::validate::Validate for StateSnapshot {
943 fn validate(&self) -> Result<(), InvalidError> {
944 let bytes = serde_json::to_vec(&self.document)
945 .map_err(|error| InvalidError::new(error.to_string()))?;
946 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
947 return Err(InvalidError::new("state document exceeds its byte cap"));
948 }
949 validate_portable_json(&self.document)?;
950 Ok(())
951 }
952}
953
954const MAX_EXACT_JSON_INTEGER: i128 = 9_007_199_254_740_991;
955
956fn validate_portable_json(value: &serde_json::Value) -> Result<(), InvalidError> {
957 match value {
958 serde_json::Value::Number(number) => {
959 let exact = if let Some(integer) = number.as_i64() {
960 i128::from(integer).abs() <= MAX_EXACT_JSON_INTEGER
961 } else if let Some(integer) = number.as_u64() {
962 i128::from(integer) <= MAX_EXACT_JSON_INTEGER
963 } else {
964 number.as_f64().is_some_and(|float| {
965 float.is_finite()
966 && (float.fract() != 0.0 || float.abs() <= MAX_EXACT_JSON_INTEGER as f64)
967 })
968 };
969 if !exact {
970 return Err(InvalidError::new(
971 "state JSON integer exceeds the exact number range",
972 ));
973 }
974 }
975 serde_json::Value::Array(items) => {
976 for item in items {
977 validate_portable_json(item)?;
978 }
979 }
980 serde_json::Value::Object(fields) => {
981 for item in fields.values() {
982 validate_portable_json(item)?;
983 }
984 }
985 _ => {}
986 }
987 Ok(())
988}
989
990pub fn apply_json_patch(
992 document: &serde_json::Value,
993 patch: &[json_patch::PatchOperation],
994) -> Result<serde_json::Value, InvalidError> {
995 if patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
996 return Err(InvalidError::new("state patch has too many operations"));
997 }
998 let patch_bytes =
999 serde_json::to_vec(patch).map_err(|error| InvalidError::new(error.to_string()))?;
1000 if patch_bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
1001 return Err(InvalidError::new(
1002 "state patch exceeds the document byte cap",
1003 ));
1004 }
1005 validate_portable_json(document)?;
1006 let patch_value =
1007 serde_json::to_value(patch).map_err(|error| InvalidError::new(error.to_string()))?;
1008 validate_portable_json(&patch_value)?;
1009 let mut result = document.clone();
1010 for (index, operation) in patch.iter().enumerate() {
1011 if let json_patch::PatchOperation::Test(test) = operation {
1013 let kind = match result.pointer(test.path.as_str()) {
1014 Some(target) if equal_json(target, &test.value) => continue,
1015 Some(_) => "value did not match",
1016 None => "path is invalid",
1017 };
1018 return Err(InvalidError::new(format!(
1019 "operation '/{index}' failed at path '{}': {kind}",
1020 test.path
1021 )));
1022 }
1023 json_patch::patch(&mut result, std::slice::from_ref(operation)).map_err(|mut error| {
1024 error.operation = index;
1025 InvalidError::new(error.to_string())
1026 })?;
1027 }
1028 crate::validate::Validate::validate(&StateSnapshot {
1029 base_revision: 0,
1030 document: result.clone(),
1031 base_op_id: None,
1032 })?;
1033 Ok(result)
1034}
1035
1036pub fn equal_json(left: &serde_json::Value, right: &serde_json::Value) -> bool {
1038 match (left, right) {
1039 (serde_json::Value::Number(left), serde_json::Value::Number(right)) => {
1040 equal_json_number(left, right)
1041 }
1042 (serde_json::Value::Array(left), serde_json::Value::Array(right)) => {
1043 left.len() == right.len()
1044 && left
1045 .iter()
1046 .zip(right)
1047 .all(|(left, right)| equal_json(left, right))
1048 }
1049 (serde_json::Value::Object(left), serde_json::Value::Object(right)) => {
1050 left.len() == right.len()
1051 && left.iter().all(|(key, value)| {
1052 right.get(key).is_some_and(|other| equal_json(value, other))
1053 })
1054 }
1055 _ => left == right,
1056 }
1057}
1058
1059fn equal_json_number(left: &serde_json::Number, right: &serde_json::Number) -> bool {
1061 let integer = |number: &serde_json::Number| {
1062 number
1063 .as_i64()
1064 .map(i128::from)
1065 .or_else(|| number.as_u64().map(i128::from))
1066 };
1067 let whole = |number: &serde_json::Number| {
1068 number
1069 .as_f64()
1070 .filter(|float| float.fract() == 0.0)
1071 .map(|float| float as i128)
1072 };
1073 match (integer(left), integer(right)) {
1074 (Some(left), Some(right)) => left == right,
1075 (Some(integer), None) => whole(right) == Some(integer),
1076 (None, Some(integer)) => whole(left) == Some(integer),
1077 (None, None) => left.as_f64() == right.as_f64(),
1078 }
1079}
1080
1081impl crate::validate::Validate for SessionStart {
1082 fn validate(&self) -> Result<(), InvalidError> {
1083 if let Some(label) = &self.label
1084 && (label.len() > crate::limits::MAX_SESSION_LABEL_BYTES
1085 || label.chars().any(char::is_control))
1086 {
1087 return Err(InvalidError::new(
1088 "session label exceeds its cap or contains a control character",
1089 ));
1090 }
1091 if let Some(namespace) = &self.namespace {
1092 crate::kv::validate_namespace(namespace)?;
1093 }
1094 if self.root.is_some() && self.parent.is_none() {
1095 return Err(InvalidError::new("session root requires a parent"));
1096 }
1097 if self.tags.len() > crate::limits::MAX_MEMORY_TAGS {
1098 return Err(InvalidError::new("session has too many tags"));
1099 }
1100 if self
1101 .tags
1102 .iter()
1103 .any(|tag| tag.len() > crate::limits::MAX_MEMORY_TAG_BYTES)
1104 {
1105 return Err(InvalidError::new("session tag exceeds its byte cap"));
1106 }
1107 Ok(())
1108 }
1109}
1110
1111#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1118#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1119pub struct TokenUsage {
1120 pub input_tokens: u64,
1121 pub output_tokens: u64,
1122 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub reasoning_output_tokens: Option<u64>,
1124 #[serde(default, skip_serializing_if = "Option::is_none")]
1125 pub cache_read_input_tokens: Option<u64>,
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub cache_creation_input_tokens: Option<u64>,
1128 #[serde(default, skip_serializing_if = "Option::is_none")]
1130 pub cost_micros: Option<u64>,
1131}
1132
1133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1136#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1137#[serde(from = "u8", into = "u8")]
1138pub enum AgentErrorCode {
1139 InvalidRequest,
1140 Unauthorized,
1141 Unsupported,
1142 DeadlineExceeded,
1143 Cancelled,
1144 ToolFailure,
1145 Internal,
1146 Unrecognized(u8),
1148}
1149
1150impl AgentErrorCode {
1151 pub const fn code(self) -> u8 {
1153 match self {
1154 AgentErrorCode::InvalidRequest => 1,
1155 AgentErrorCode::Unauthorized => 2,
1156 AgentErrorCode::Unsupported => 3,
1157 AgentErrorCode::DeadlineExceeded => 4,
1158 AgentErrorCode::Cancelled => 5,
1159 AgentErrorCode::ToolFailure => 6,
1160 AgentErrorCode::Internal => 7,
1161 AgentErrorCode::Unrecognized(code) => code,
1162 }
1163 }
1164
1165 pub const fn from_code(code: u8) -> Self {
1167 match code {
1168 1 => AgentErrorCode::InvalidRequest,
1169 2 => AgentErrorCode::Unauthorized,
1170 3 => AgentErrorCode::Unsupported,
1171 4 => AgentErrorCode::DeadlineExceeded,
1172 5 => AgentErrorCode::Cancelled,
1173 6 => AgentErrorCode::ToolFailure,
1174 7 => AgentErrorCode::Internal,
1175 other => AgentErrorCode::Unrecognized(other),
1176 }
1177 }
1178}
1179
1180impl From<u8> for AgentErrorCode {
1181 fn from(code: u8) -> Self {
1182 Self::from_code(code)
1183 }
1184}
1185
1186impl From<AgentErrorCode> for u8 {
1187 fn from(code: AgentErrorCode) -> u8 {
1188 code.code()
1189 }
1190}
1191
1192#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1196#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1197pub struct AgentErrorBody {
1198 pub code: AgentErrorCode,
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub message: Option<String>,
1201 #[serde(default)]
1202 pub retryable: bool,
1203 #[serde(default, skip_serializing_if = "Option::is_none")]
1204 pub detail: Option<BTreeMap<String, Value>>,
1205}
1206
1207#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1210#[serde(from = "u8", into = "u8")]
1211pub enum DeadLetterReason {
1212 RetryExhausted,
1213 Rejected,
1214 DecodeFailed,
1215 DeadlineExceeded,
1216 Unrecognized(u8),
1218}
1219
1220impl DeadLetterReason {
1221 pub const fn code(self) -> u8 {
1223 match self {
1224 DeadLetterReason::RetryExhausted => 1,
1225 DeadLetterReason::Rejected => 2,
1226 DeadLetterReason::DecodeFailed => 3,
1227 DeadLetterReason::DeadlineExceeded => 4,
1228 DeadLetterReason::Unrecognized(code) => code,
1229 }
1230 }
1231
1232 pub const fn from_code(code: u8) -> Self {
1234 match code {
1235 1 => DeadLetterReason::RetryExhausted,
1236 2 => DeadLetterReason::Rejected,
1237 3 => DeadLetterReason::DecodeFailed,
1238 4 => DeadLetterReason::DeadlineExceeded,
1239 other => DeadLetterReason::Unrecognized(other),
1240 }
1241 }
1242}
1243
1244impl From<u8> for DeadLetterReason {
1245 fn from(code: u8) -> Self {
1246 Self::from_code(code)
1247 }
1248}
1249
1250impl From<DeadLetterReason> for u8 {
1251 fn from(reason: DeadLetterReason) -> u8 {
1252 reason.code()
1253 }
1254}
1255
1256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1266#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1267pub struct AgentDeadLetter {
1268 pub source: LogPosition,
1269 pub reason: DeadLetterReason,
1270 pub attempts: u32,
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub detail: Option<String>,
1273 #[cfg_attr(
1274 feature = "schemars",
1275 schemars(schema_with = "crate::encoding::bytes_schema")
1276 )]
1277 #[serde(with = "crate::encoding::bin_bytes")]
1278 pub payload: Vec<u8>,
1279}
1280
1281#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1284#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1285#[serde(from = "u8", into = "u8")]
1286pub enum Health {
1287 Healthy,
1288 Degraded,
1289 Unavailable,
1290 Unrecognized(u8),
1292}
1293
1294impl Health {
1295 pub const fn code(self) -> u8 {
1297 match self {
1298 Health::Healthy => 1,
1299 Health::Degraded => 2,
1300 Health::Unavailable => 3,
1301 Health::Unrecognized(code) => code,
1302 }
1303 }
1304
1305 pub const fn from_code(code: u8) -> Self {
1308 match code {
1309 1 => Health::Healthy,
1310 2 => Health::Degraded,
1311 3 => Health::Unavailable,
1312 other => Health::Unrecognized(other),
1313 }
1314 }
1315}
1316
1317impl From<u8> for Health {
1318 fn from(code: u8) -> Self {
1319 Self::from_code(code)
1320 }
1321}
1322
1323impl From<Health> for u8 {
1324 fn from(health: Health) -> u8 {
1325 health.code()
1326 }
1327}
1328
1329#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1337#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1338#[serde(rename_all = "snake_case")]
1339pub enum ContentRef {
1340 ContentType(crate::content::ContentType),
1341 SchemaId(String),
1342}
1343
1344#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1348#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1349pub struct CapabilityDescriptor {
1350 pub skill_id: String,
1352 #[serde(default, skip_serializing_if = "Option::is_none")]
1353 pub input: Option<ContentRef>,
1354 #[serde(default, skip_serializing_if = "Option::is_none")]
1355 pub output: Option<ContentRef>,
1356 #[serde(default, skip_serializing_if = "Option::is_none")]
1358 pub cost_class: Option<u8>,
1359 #[serde(default, skip_serializing_if = "Option::is_none")]
1361 pub latency_class: Option<u8>,
1362 #[serde(default, skip_serializing_if = "Option::is_none")]
1363 pub max_concurrency: Option<u32>,
1364 #[serde(default, skip_serializing_if = "Option::is_none")]
1365 pub health: Option<Health>,
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1368 pub load: Option<u16>,
1369}
1370
1371#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1379#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1380pub struct AgentCard {
1381 #[serde(default, skip_serializing_if = "Option::is_none")]
1384 pub name: Option<String>,
1385 #[serde(default, skip_serializing_if = "Option::is_none")]
1387 pub version: Option<String>,
1388 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1391 pub capabilities: Vec<CapabilityDescriptor>,
1392 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub ttl_micros: Option<u64>,
1396 #[serde(default, skip_serializing_if = "Option::is_none")]
1399 pub inbox: Option<String>,
1400}
1401
1402impl AgentCard {
1403 pub fn validate(&self) -> Result<(), ValidateError> {
1405 cap_str(self.name.as_deref(), "name")?;
1406 cap_str(self.version.as_deref(), "version")?;
1407 cap_str(self.inbox.as_deref(), "inbox")?;
1408 if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
1409 return Err(ValidateError::TooLarge {
1410 field: "capabilities",
1411 size: self.capabilities.len(),
1412 cap: crate::limits::MAX_CARD_CAPABILITIES,
1413 });
1414 }
1415 for capability in &self.capabilities {
1416 cap_str(Some(&capability.skill_id), "capability skill_id")?;
1417 }
1418 Ok(())
1419 }
1420}
1421
1422#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1440#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1441pub struct AgentPresence {
1442 pub v: u32,
1445 pub agent: AgentId,
1450 #[serde(default, skip_serializing_if = "Option::is_none")]
1454 pub inbox: Option<String>,
1455}
1456
1457impl AgentPresence {
1458 pub fn new(agent: AgentId) -> Self {
1461 Self {
1462 v: crate::codes::PRESENCE_OP_VERSION,
1463 agent,
1464 inbox: None,
1465 }
1466 }
1467
1468 pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
1470 self.inbox = Some(inbox.into());
1471 self
1472 }
1473
1474 pub fn validate(&self) -> Result<(), ValidateError> {
1477 cap_str(self.inbox.as_deref(), "inbox")?;
1478 Ok(())
1479 }
1480}
1481
1482#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1491#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1492pub struct BodyRef {
1493 pub reference: String,
1496 pub size_bytes: u64,
1498 #[cfg_attr(
1500 feature = "schemars",
1501 schemars(schema_with = "crate::encoding::bytes_schema")
1502 )]
1503 #[serde(with = "crate::encoding::bin_bytes")]
1504 pub sha256: Vec<u8>,
1505 #[serde(default, skip_serializing_if = "Option::is_none")]
1509 pub encryption: Option<u8>,
1510}
1511
1512const SHA256_BYTES: usize = 32;
1513
1514impl BodyRef {
1515 pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
1517 Self {
1518 reference: reference.into(),
1519 size_bytes,
1520 sha256: sha256.to_vec(),
1521 encryption: None,
1522 }
1523 }
1524
1525 pub fn validate(&self) -> Result<(), ValidateError> {
1529 if self.reference.is_empty() {
1530 return Err(ValidateError::Invalid {
1531 field: "reference",
1532 reason: "reference must not be empty".to_owned(),
1533 });
1534 }
1535 if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
1536 return Err(ValidateError::TooLarge {
1537 field: "reference",
1538 size: self.reference.len(),
1539 cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
1540 });
1541 }
1542 if self.sha256.len() != SHA256_BYTES {
1543 return Err(ValidateError::Invalid {
1544 field: "sha256",
1545 reason: format!(
1546 "digest must be {SHA256_BYTES} bytes, got {}",
1547 self.sha256.len()
1548 ),
1549 });
1550 }
1551 Ok(())
1552 }
1553}
1554
1555#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1567#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1568pub struct Signature {
1569 pub scheme: u8,
1572 #[cfg_attr(
1574 feature = "schemars",
1575 schemars(schema_with = "crate::encoding::bytes_schema")
1576 )]
1577 #[serde(with = "crate::encoding::bin_bytes")]
1578 pub key_id: Vec<u8>,
1579 #[cfg_attr(
1582 feature = "schemars",
1583 schemars(schema_with = "crate::encoding::bytes_schema")
1584 )]
1585 #[serde(with = "crate::encoding::bin_bytes")]
1586 pub bytes: Vec<u8>,
1587 #[serde(default, skip_serializing_if = "Option::is_none")]
1593 pub context: Option<SignatureContext>,
1594}
1595
1596#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1602#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1603pub struct SignatureContext {
1604 #[serde(default, skip_serializing_if = "Option::is_none")]
1606 pub content_type: Option<u8>,
1607 #[serde(default, skip_serializing_if = "Option::is_none")]
1609 pub agent_version: Option<u32>,
1610}
1611
1612pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
1614
1615pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
1620
1621const ED25519_KEY_ID_BYTES: usize = 8;
1622const ED25519_SIGNATURE_BYTES: usize = 64;
1623
1624impl Signature {
1625 pub fn validate(&self) -> Result<(), ValidateError> {
1630 if self.scheme != SIGNATURE_SCHEME_ED25519 {
1631 return Ok(());
1632 }
1633 if self.key_id.len() != ED25519_KEY_ID_BYTES {
1634 return Err(ValidateError::Invalid {
1635 field: "key_id",
1636 reason: format!(
1637 "Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
1638 self.key_id.len()
1639 ),
1640 });
1641 }
1642 if self.bytes.len() != ED25519_SIGNATURE_BYTES {
1643 return Err(ValidateError::Invalid {
1644 field: "bytes",
1645 reason: format!(
1646 "Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
1647 self.bytes.len()
1648 ),
1649 });
1650 }
1651 Ok(())
1652 }
1653}
1654
1655#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1668#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1669pub struct AgentEnvelope {
1670 pub kind: AgentKind,
1671 #[serde(default, skip_serializing_if = "Option::is_none")]
1675 pub record: Option<RecordId>,
1676 pub conversation: ConversationId,
1678 #[serde(default, skip_serializing_if = "Option::is_none")]
1680 pub parent: Option<ConversationId>,
1681 #[serde(default, skip_serializing_if = "Option::is_none")]
1683 pub root: Option<ConversationId>,
1684 pub source: AgentId,
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1690 pub target: Option<AgentId>,
1691 #[serde(default, skip_serializing_if = "Option::is_none")]
1694 pub cause: Option<RecordId>,
1695 #[serde(default, skip_serializing_if = "Option::is_none")]
1698 pub cause_at: Option<LogPosition>,
1699 #[serde(default, skip_serializing_if = "Option::is_none")]
1701 pub correlation: Option<CorrelationId>,
1702 #[serde(default, skip_serializing_if = "Option::is_none")]
1704 pub channel: Option<ChannelId>,
1705 #[serde(default, skip_serializing_if = "Option::is_none")]
1706 pub idempotency_key: Option<IdempotencyKey>,
1707 #[serde(default, skip_serializing_if = "Option::is_none")]
1711 pub deadline_micros: Option<u64>,
1712 #[serde(default, skip_serializing_if = "Option::is_none")]
1714 pub sequence: Option<u64>,
1715 #[serde(default, skip_serializing_if = "is_false")]
1719 pub last: bool,
1720 #[serde(default, skip_serializing_if = "Option::is_none")]
1724 pub finish_reason: Option<String>,
1725 #[serde(default, skip_serializing_if = "Option::is_none")]
1726 pub task_state: Option<TaskState>,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1730 pub operation: Option<String>,
1731 #[serde(default, skip_serializing_if = "Option::is_none")]
1733 pub tool: Option<String>,
1734 #[serde(default, skip_serializing_if = "Option::is_none")]
1735 pub usage: Option<TokenUsage>,
1736 #[serde(default, skip_serializing_if = "Option::is_none")]
1738 pub metadata: Option<BTreeMap<String, Value>>,
1739 #[serde(default, skip_serializing_if = "is_zero_u64")]
1751 pub must_understand: u64,
1752 #[cfg_attr(
1755 feature = "schemars",
1756 schemars(schema_with = "crate::encoding::bytes_schema")
1757 )]
1758 #[serde(
1759 default,
1760 skip_serializing_if = "Vec::is_empty",
1761 with = "crate::encoding::bin_bytes"
1762 )]
1763 pub body: Vec<u8>,
1764 #[serde(default, skip_serializing_if = "Option::is_none")]
1771 pub signature: Option<Signature>,
1772}
1773
1774fn is_false(value: &bool) -> bool {
1775 !*value
1776}
1777
1778fn is_zero_u64(value: &u64) -> bool {
1779 *value == 0
1780}
1781
1782pub mod features {
1789 pub const NONE: u64 = 0;
1793}
1794
1795impl AgentEnvelope {
1796 fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
1797 Self {
1798 kind,
1799 record: None,
1800 conversation,
1801 parent: None,
1802 root: None,
1803 source,
1804 target: None,
1805 cause: None,
1806 cause_at: None,
1807 correlation: None,
1808 channel: None,
1809 idempotency_key: None,
1810 deadline_micros: None,
1811 sequence: None,
1812 last: false,
1813 finish_reason: None,
1814 task_state: None,
1815 operation: None,
1816 tool: None,
1817 usage: None,
1818 metadata: None,
1819 must_understand: 0,
1820 body: Vec::new(),
1821 signature: None,
1822 }
1823 }
1824
1825 #[must_use]
1830 pub fn requiring(mut self, bits: u64) -> Self {
1831 self.must_understand = bits;
1832 self
1833 }
1834
1835 pub fn unmet_requirements(&self, understood: u64) -> u64 {
1840 self.must_understand & !understood
1841 }
1842
1843 pub fn command(
1845 record: RecordId,
1846 conversation: ConversationId,
1847 source: AgentId,
1848 correlation: CorrelationId,
1849 body: Vec<u8>,
1850 ) -> Self {
1851 let mut envelope = Self::base(AgentKind::Command, conversation, source);
1852 envelope.record = Some(record);
1853 envelope.correlation = Some(correlation);
1854 envelope.body = body;
1855 envelope
1856 }
1857
1858 pub fn response(
1860 record: RecordId,
1861 conversation: ConversationId,
1862 source: AgentId,
1863 correlation: CorrelationId,
1864 body: Vec<u8>,
1865 ) -> Self {
1866 let mut envelope = Self::base(AgentKind::Response, conversation, source);
1867 envelope.record = Some(record);
1868 envelope.correlation = Some(correlation);
1869 envelope.body = body;
1870 envelope
1871 }
1872
1873 pub fn event(
1875 record: RecordId,
1876 conversation: ConversationId,
1877 source: AgentId,
1878 body: Vec<u8>,
1879 ) -> Self {
1880 let mut envelope = Self::base(AgentKind::Event, conversation, source);
1881 envelope.record = Some(record);
1882 envelope.body = body;
1883 envelope
1884 }
1885
1886 pub fn chunk(
1889 conversation: ConversationId,
1890 source: AgentId,
1891 correlation: CorrelationId,
1892 channel: ChannelId,
1893 sequence: u64,
1894 body: Vec<u8>,
1895 ) -> Self {
1896 let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
1897 envelope.correlation = Some(correlation);
1898 envelope.channel = Some(channel);
1899 envelope.sequence = Some(sequence);
1900 envelope.body = body;
1901 envelope
1902 }
1903
1904 pub fn status(
1908 record: RecordId,
1909 conversation: ConversationId,
1910 source: AgentId,
1911 operation: impl Into<String>,
1912 ) -> Self {
1913 let mut envelope = Self::base(AgentKind::Status, conversation, source);
1914 envelope.record = Some(record);
1915 envelope.operation = Some(operation.into());
1916 envelope
1917 }
1918
1919 pub fn error(
1922 record: RecordId,
1923 conversation: ConversationId,
1924 source: AgentId,
1925 correlation: CorrelationId,
1926 body: Vec<u8>,
1927 ) -> Self {
1928 let mut envelope = Self::base(AgentKind::Error, conversation, source);
1929 envelope.record = Some(record);
1930 envelope.correlation = Some(correlation);
1931 envelope.body = body;
1932 envelope
1933 }
1934
1935 pub fn with_target(mut self, target: AgentId) -> Self {
1937 self.target = Some(target);
1938 self
1939 }
1940
1941 pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
1945 self.cause = Some(cause);
1946 self.cause_at = cause_at;
1947 self
1948 }
1949
1950 pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
1954 self.correlation = Some(correlation);
1955 self
1956 }
1957
1958 pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
1960 self.idempotency_key = Some(key);
1961 self
1962 }
1963
1964 pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
1967 self.deadline_micros = Some(deadline_micros);
1968 self
1969 }
1970
1971 pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
1974 self.last = true;
1975 self.finish_reason = Some(finish_reason.into());
1976 self
1977 }
1978
1979 pub fn with_task_state(mut self, state: TaskState) -> Self {
1982 self.task_state = Some(state);
1983 self
1984 }
1985
1986 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
1988 self.operation = Some(operation.into());
1989 self
1990 }
1991
1992 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
1994 self.tool = Some(tool.into());
1995 self
1996 }
1997
1998 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
2000 self.usage = Some(usage);
2001 self
2002 }
2003
2004 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
2006 self.metadata
2007 .get_or_insert_with(BTreeMap::new)
2008 .insert(key.into(), value.into());
2009 self
2010 }
2011
2012 pub fn with_signature(mut self, signature: Signature) -> Self {
2017 self.signature = Some(signature);
2018 self
2019 }
2020}
2021
2022#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2025#[non_exhaustive]
2026pub enum ValidateError {
2027 #[error("{kind} requires `{field}`")]
2028 Missing {
2029 kind: AgentKind,
2030 field: &'static str,
2031 },
2032 #[error("`{field}` is invalid on {kind}")]
2033 Forbidden {
2034 kind: AgentKind,
2035 field: &'static str,
2036 },
2037 #[error("`{field}` is {size}B, exceeds cap {cap}B")]
2038 TooLarge {
2039 field: &'static str,
2040 size: usize,
2041 cap: usize,
2042 },
2043 #[error("`{field}`: {reason}")]
2044 Invalid { field: &'static str, reason: String },
2045}
2046
2047pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
2072 use AgentKind::*;
2073 let kind = envelope.kind;
2074
2075 let require = |present: bool, field: &'static str| {
2076 if present {
2077 Ok(())
2078 } else {
2079 Err(ValidateError::Missing { kind, field })
2080 }
2081 };
2082 let forbid = |absent: bool, field: &'static str| {
2083 if absent {
2084 Ok(())
2085 } else {
2086 Err(ValidateError::Forbidden { kind, field })
2087 }
2088 };
2089
2090 if kind != Chunk {
2092 require(envelope.record.is_some(), "record")?;
2093 }
2094
2095 if envelope.root.is_some() && envelope.parent.is_none() {
2096 return Err(ValidateError::Invalid {
2097 field: "root",
2098 reason: "root requires parent".to_owned(),
2099 });
2100 }
2101 if envelope.parent == Some(envelope.conversation)
2102 || envelope.root == Some(envelope.conversation)
2103 {
2104 return Err(ValidateError::Invalid {
2105 field: "parent",
2106 reason: "parent and root must differ from conversation".to_owned(),
2107 });
2108 }
2109
2110 match kind {
2112 Command | Response | Chunk | Error => {
2113 require(envelope.correlation.is_some(), "correlation")?
2114 }
2115 Status => {
2116 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
2117 require(envelope.correlation.is_some(), "correlation")?;
2118 }
2119 }
2120 Event => {}
2121 }
2122
2123 match kind {
2126 Chunk => {
2127 require(envelope.channel.is_some(), "channel")?;
2128 require(envelope.sequence.is_some(), "sequence")?;
2129 }
2130 Error => {
2131 if envelope.sequence.is_some() && envelope.channel.is_none() {
2132 return Err(ValidateError::Invalid {
2133 field: "sequence",
2134 reason: "sequence requires channel".to_owned(),
2135 });
2136 }
2137 }
2138 _ => {
2139 forbid(envelope.channel.is_none(), "channel")?;
2140 forbid(envelope.sequence.is_none(), "sequence")?;
2141 }
2142 }
2143
2144 if envelope.last && !matches!(kind, Chunk | Status) {
2147 return Err(ValidateError::Forbidden {
2148 kind,
2149 field: "last",
2150 });
2151 }
2152
2153 match kind {
2155 Response => {}
2156 Chunk => {
2157 if envelope.finish_reason.is_some() && !envelope.last {
2158 return Err(ValidateError::Invalid {
2159 field: "finish_reason",
2160 reason: "finish_reason rides only the terminal chunk".to_owned(),
2161 });
2162 }
2163 }
2164 _ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
2165 }
2166
2167 if matches!(kind, Chunk | Status | Error) {
2170 forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
2171 }
2172
2173 let registry_fact = kind == Status
2177 && matches!(
2178 envelope.operation.as_deref(),
2179 Some(OPERATION_QUARANTINE | OPERATION_UNQUARANTINE)
2180 );
2181 if registry_fact {
2182 require(envelope.deadline_micros.is_some(), "deadline_micros")?;
2183 } else if matches!(kind, Response | Event | Status | Error) {
2184 forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
2185 }
2186 if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
2187 return Err(ValidateError::Invalid {
2188 field: "deadline_micros",
2189 reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
2190 });
2191 }
2192
2193 match kind {
2195 Status => {
2196 if matches!(
2197 envelope.operation.as_deref(),
2198 Some(OPERATION_TASK | OPERATION_SESSION)
2199 ) {
2200 require(envelope.task_state.is_some(), "task_state")?;
2201 }
2202 }
2203 Response | Error => {}
2204 _ => forbid(envelope.task_state.is_none(), "task_state")?,
2205 }
2206
2207 match kind {
2213 Status => {
2214 require(envelope.operation.is_some(), "operation")?;
2215 if let Some(operation) = envelope.operation.as_deref()
2216 && !matches!(
2217 operation,
2218 OPERATION_TASK
2219 | OPERATION_SESSION
2220 | OPERATION_CARD
2221 | OPERATION_PROGRESS
2222 | OPERATION_QUARANTINE
2223 | OPERATION_UNQUARANTINE
2224 )
2225 {
2226 return Err(ValidateError::Invalid {
2227 field: "operation",
2228 reason: format!(
2229 "status operation must be `{OPERATION_TASK}`, `{OPERATION_SESSION}`, `{OPERATION_CARD}`, \
2230 `{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
2231 `{OPERATION_UNQUARANTINE}`, got `{operation}`"
2232 ),
2233 });
2234 }
2235 }
2236 Chunk => {
2237 if envelope.sequence == Some(0) {
2238 require(envelope.operation.is_some(), "operation")?;
2239 }
2240 if let Some(operation) = envelope.operation.as_deref() {
2241 if envelope.sequence != Some(0) {
2242 return Err(ValidateError::Invalid {
2243 field: "operation",
2244 reason: "the stream purpose rides the opening chunk (sequence 0)"
2245 .to_owned(),
2246 });
2247 }
2248 if !matches!(
2249 operation,
2250 OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
2251 ) {
2252 return Err(ValidateError::Invalid {
2253 field: "operation",
2254 reason: format!(
2255 "chunk-stream purpose must be `{OPERATION_CHAT}`, \
2256 `{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
2257 got `{operation}`"
2258 ),
2259 });
2260 }
2261 }
2262 }
2263 Command | Response | Event | Error => {}
2264 }
2265
2266 if kind == Status {
2268 forbid(envelope.tool.is_none(), "tool")?;
2269 }
2270
2271 match kind {
2273 Command => forbid(envelope.usage.is_none(), "usage")?,
2274 Chunk if envelope.usage.is_some() && !envelope.last => {
2275 return Err(ValidateError::Invalid {
2276 field: "usage",
2277 reason: "whole-stream accounting rides the terminal chunk".to_owned(),
2278 });
2279 }
2280 _ => {}
2281 }
2282
2283 match kind {
2285 Status if envelope.operation.as_deref() == Some(OPERATION_SESSION) => {
2286 require(!envelope.body.is_empty(), "body")?;
2287 let state = envelope
2288 .task_state
2289 .expect("session state was required above");
2290 if envelope.last != state.is_terminal() {
2291 return Err(ValidateError::Invalid {
2292 field: "last",
2293 reason: "session last must match terminal task state".to_owned(),
2294 });
2295 }
2296 #[cfg(feature = "cbor")]
2298 {
2299 let invalid_body = |reason: String| ValidateError::Invalid {
2300 field: "body",
2301 reason,
2302 };
2303 let validate_start = |start: &SessionStart| {
2304 crate::validate::Validate::validate(start)
2305 .map_err(|error| invalid_body(error.to_string()))?;
2306 if start.parent != envelope.parent || start.root != envelope.root {
2307 return Err(invalid_body(
2308 "session ancestry must match envelope parent and root".to_owned(),
2309 ));
2310 }
2311 Ok(())
2312 };
2313 match state {
2314 TaskState::Submitted => {
2315 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2316 .map_err(|error| invalid_body(error.to_string()))?;
2317 validate_start(&start)?;
2318 }
2319 TaskState::Working => {
2320 let body: ciborium::value::Value =
2321 crate::framing::decode_named(&envelope.body)
2322 .map_err(|error| invalid_body(error.to_string()))?;
2323 let is_start = matches!(
2324 body,
2325 ciborium::value::Value::Map(ref entries)
2326 if entries.iter().any(|(key, _)| {
2327 matches!(key, ciborium::value::Value::Text(name) if name == "agent" || name == "sdk")
2328 })
2329 );
2330 if is_start {
2331 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2332 .map_err(|error| invalid_body(error.to_string()))?;
2333 validate_start(&start)?;
2334 } else {
2335 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2336 .map_err(|error| invalid_body(error.to_string()))?;
2337 }
2338 }
2339 state if state.is_terminal() => {
2340 crate::framing::decode_named::<SessionEnd>(&envelope.body)
2341 .map_err(|error| invalid_body(error.to_string()))?;
2342 }
2343 _ => {
2344 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2345 .map_err(|error| invalid_body(error.to_string()))?;
2346 }
2347 }
2348 }
2349 }
2350 Status => {}
2351 Chunk => {
2352 if envelope.body.is_empty() && !envelope.last {
2353 return Err(ValidateError::Missing {
2354 kind,
2355 field: "body",
2356 });
2357 }
2358 }
2359 _ => require(!envelope.body.is_empty(), "body")?,
2360 }
2361
2362 cap_str(envelope.operation.as_deref(), "operation")?;
2366 cap_str(envelope.tool.as_deref(), "tool")?;
2367 cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
2368 if let Some(metadata) = &envelope.metadata {
2369 if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
2370 return Err(ValidateError::TooLarge {
2371 field: "metadata",
2372 size: metadata.len(),
2373 cap: crate::limits::MAX_METADATA_ENTRIES,
2374 });
2375 }
2376 let mut total = 0usize;
2377 for (key, value) in metadata {
2378 if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
2379 return Err(ValidateError::TooLarge {
2380 field: "metadata key",
2381 size: key.len(),
2382 cap: crate::limits::MAX_METADATA_KEY_BYTES,
2383 });
2384 }
2385 let value_size = value_size(value);
2386 if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
2387 return Err(ValidateError::TooLarge {
2388 field: "metadata value",
2389 size: value_size,
2390 cap: crate::limits::MAX_METADATA_VALUE_BYTES,
2391 });
2392 }
2393 total += key.len() + value_size;
2394 }
2395 if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
2396 return Err(ValidateError::TooLarge {
2397 field: "metadata",
2398 size: total,
2399 cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
2400 });
2401 }
2402 }
2403 if let Some(signature) = &envelope.signature {
2406 signature.validate()?;
2407 }
2408 Ok(())
2409}
2410
2411pub const OPERATION_TASK: &str = "task";
2413pub const OPERATION_SESSION: &str = "session";
2415pub const OPERATION_CARD: &str = "card";
2417pub const OPERATION_PROGRESS: &str = "progress";
2419pub const OPERATION_QUARANTINE: &str = "quarantine";
2424pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
2429
2430pub const OPERATION_CHAT: &str = "chat";
2437pub const OPERATION_REASONING: &str = "reasoning";
2439pub const OPERATION_TOOL_ARGS: &str = "tool_args";
2441
2442pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
2448pub const OPERATION_STATE_DELTA: &str = "state_delta";
2451pub const OPERATION_SESSION_PARKED: &str = "session_parked";
2454pub const OPERATION_SESSION_UNPARKED: &str = "session_unparked";
2457
2458pub const METADATA_ROLE: &str = "role";
2465pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
2471pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
2475pub const METADATA_PURPOSE: &str = "purpose";
2478pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
2481pub const METADATA_TASK_CONTEXT: &str = "task_context";
2484pub const METADATA_SESSION_INTENT: &str = "session_intent";
2487
2488const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2492
2493pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
2494 let mut out = [0u8; 26];
2495 let mut v = value;
2496 for slot in out.iter_mut().rev() {
2497 *slot = CROCKFORD[(v & 0x1f) as usize];
2498 v >>= 5;
2499 }
2500 out
2501}
2502
2503pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
2504 let bytes = s.as_bytes();
2505 if bytes.len() != 26 {
2506 return Err(IdParseError::Length { got: bytes.len() });
2507 }
2508 let mut value: u128 = 0;
2509 for (i, byte) in bytes.iter().enumerate() {
2510 let digit = CROCKFORD
2511 .iter()
2512 .position(|c| *c == byte.to_ascii_uppercase())
2513 .ok_or(IdParseError::Char(*byte as char))?;
2514 if i == 0 && digit > 7 {
2517 return Err(IdParseError::Overflow);
2518 }
2519 value = (value << 5) | digit as u128;
2520 }
2521 Ok(value)
2522}
2523
2524fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
2525 if let Some(value) = value
2526 && value.len() > crate::limits::MAX_AGENT_STRING_BYTES
2527 {
2528 return Err(ValidateError::TooLarge {
2529 field,
2530 size: value.len(),
2531 cap: crate::limits::MAX_AGENT_STRING_BYTES,
2532 });
2533 }
2534 Ok(())
2535}
2536
2537fn value_size(value: &Value) -> usize {
2540 match value {
2541 Value::Str(s) => s.len(),
2542 Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
2543 _ => 9,
2544 }
2545}
2546
2547#[cfg(test)]
2548mod tests {
2549 use super::*;
2550 use crate::validate::Validate;
2551
2552 fn session_start() -> SessionStart {
2553 SessionStart {
2554 label: Some("Research".to_owned()),
2555 namespace: Some("agents".to_owned()),
2556 agent: "planner".parse().expect("valid agent id"),
2557 sdk: SdkInfo {
2558 language: "rust".to_owned(),
2559 version: "0.8.0".to_owned(),
2560 },
2561 parent: None,
2562 root: None,
2563 idle_timeout_micros: Some(300_000_000),
2564 budget: Some(Budget {
2565 tokens: Some(1000),
2566 cost_micros: None,
2567 }),
2568 tags: vec!["demo".to_owned()],
2569 }
2570 }
2571
2572 #[test]
2573 fn given_session_status_when_decoded_then_should_keep_known_and_unknown_values() {
2574 assert_eq!(
2575 serde_json::from_str::<SessionStatus>("\"paused\"").expect("paused"),
2576 SessionStatus::Paused
2577 );
2578 assert_eq!(
2579 serde_json::from_str::<SessionStatus>("\"future\"").expect("unknown"),
2580 SessionStatus::Unrecognized
2581 );
2582 }
2583
2584 #[test]
2585 fn given_session_start_when_validated_then_should_enforce_label_and_tag_caps() {
2586 let mut start = session_start();
2587 start.validate().expect("valid start");
2588 start.label = Some("bad\nlabel".to_owned());
2589 assert!(start.validate().is_err());
2590 start.label = Some("x".repeat(crate::limits::MAX_SESSION_LABEL_BYTES + 1));
2591 assert!(start.validate().is_err());
2592 start.label = None;
2593 start.tags = vec!["tag".to_owned(); crate::limits::MAX_MEMORY_TAGS + 1];
2594 assert!(start.validate().is_err());
2595 }
2596
2597 #[cfg(feature = "cbor")]
2598 #[test]
2599 fn given_session_status_bodies_when_validated_then_should_match_the_lifecycle() {
2600 let mut status = AgentEnvelope::status(
2601 RecordId::from_u128(1),
2602 ConversationId::from_u128(2),
2603 "planner".parse().expect("valid agent id"),
2604 OPERATION_SESSION,
2605 );
2606 status.task_state = Some(TaskState::Submitted);
2607 status.body = crate::framing::encode_named(&session_start()).expect("start encodes");
2608 validate(&status).expect("submitted start");
2609 status.parent = Some(ConversationId::from_u128(8));
2610 assert!(
2611 validate(&status).is_err(),
2612 "start and envelope ancestry must match"
2613 );
2614 status.parent = None;
2615
2616 status.task_state = Some(TaskState::Paused);
2617 status.body = crate::framing::encode_named(&SessionTransition::default())
2618 .expect("transition encodes");
2619 validate(&status).expect("paused transition");
2620
2621 status.task_state = Some(TaskState::Completed);
2622 status.body = crate::framing::encode_named(&SessionEnd::default()).expect("end encodes");
2623 assert!(validate(&status).is_err(), "terminal status needs last");
2624 status.last = true;
2625 validate(&status).expect("completed end");
2626 }
2627
2628 #[cfg(feature = "cbor")]
2629 #[test]
2630 fn given_a_parked_record_when_encoded_then_should_round_trip_and_require_a_generation() {
2631 use crate::validate::Validate;
2632 let source = |generation| crate::graph::SourceRef::Message {
2633 stream: 1,
2634 topic: 2,
2635 partition: 3,
2636 offset: 4,
2637 generation,
2638 conversation: Some(ConversationId::from_u128(5).to_string()),
2639 };
2640 let parking = SessionParking {
2641 source: source(Some(6)),
2642 role: "worker".parse().expect("valid agent id"),
2643 request: LogPosition::new(1, 7, 0, 8),
2644 };
2645 parking.validate().expect("a complete parking is valid");
2646 let bytes = crate::framing::encode_named(&parking).expect("parking encodes");
2647 let decoded: SessionParking = crate::framing::decode_named(&bytes).expect("decodes");
2648 assert_eq!(decoded, parking);
2649 let unproven = SessionParking {
2650 source: source(None),
2651 ..parking.clone()
2652 };
2653 assert!(unproven.validate().is_err());
2654 let not_a_record = SessionParking {
2655 source: crate::graph::SourceRef::Memory { id: "m".to_owned() },
2656 ..parking
2657 };
2658 assert!(not_a_record.validate().is_err());
2659 }
2660
2661 #[test]
2662 fn given_a_pause_request_body_when_decoded_then_should_read_an_empty_body_as_no_participants() {
2663 let empty: SessionPauseRequest = serde_json::from_slice(b"{}").expect("empty decodes");
2664 assert!(empty.participants.is_empty());
2665 let named = SessionPauseRequest {
2666 participants: vec!["worker".parse().expect("valid agent id")],
2667 };
2668 let json = serde_json::to_string(&named).expect("request encodes");
2669 assert_eq!(json, r#"{"participants":["worker"]}"#);
2670 assert_eq!(
2671 serde_json::from_str::<SessionPauseRequest>(&json).expect("decodes"),
2672 named
2673 );
2674 }
2675
2676 #[test]
2677 fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
2678 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2679 let text = id.to_string();
2680 assert_eq!(text.len(), 26);
2681 assert_eq!(text.parse::<RecordId>().expect("parses"), id);
2682 assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
2684 assert_eq!(
2685 RecordId::from_u128(0).to_string(),
2686 "00000000000000000000000000"
2687 );
2688 assert_eq!(
2689 RecordId::from_u128(u128::MAX).to_string(),
2690 "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
2691 );
2692 }
2693
2694 #[test]
2695 fn given_an_id_in_json_when_encoded_then_should_use_readable_text() {
2696 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2697 let json = serde_json::to_string(&id).expect("serializes");
2698 assert_eq!(json, format!("\"{id}\""));
2699 assert_eq!(
2700 serde_json::from_str::<RecordId>(&json).expect("deserializes"),
2701 id
2702 );
2703 let byte_array = serde_json::to_string(&[0_u8; 16]).expect("serializes byte array");
2704 assert!(serde_json::from_str::<RecordId>(&byte_array).is_err());
2705 }
2706
2707 #[cfg(feature = "cbor")]
2708 #[test]
2709 fn given_an_id_in_named_cbor_when_encoded_then_should_use_fixed_width_bytes() {
2710 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2711 let encoded = crate::framing::encode_named(&id).expect("serializes");
2712 let decoded = crate::framing::decode_named::<RecordId>(&encoded).expect("deserializes");
2713 assert_eq!(decoded, id);
2714 }
2715
2716 #[test]
2717 fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
2718 assert_eq!(
2719 "short".parse::<RecordId>(),
2720 Err(IdParseError::Length { got: 5 })
2721 );
2722 assert_eq!(
2723 "8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
2724 Err(IdParseError::Overflow)
2725 );
2726 assert!(matches!(
2727 "UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
2728 Err(IdParseError::Char('U'))
2729 ));
2730 }
2731
2732 #[test]
2733 fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
2734 for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
2735 assert_eq!(
2736 s.parse::<AgentId>()
2737 .expect("a printable agent id is valid")
2738 .as_str(),
2739 s
2740 );
2741 }
2742 assert!("".parse::<AgentId>().is_err());
2743 assert!("bad\nid".parse::<AgentId>().is_err());
2744 }
2745
2746 #[test]
2747 fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
2748 let expected = [
2749 (TaskState::Submitted, 1u8),
2750 (TaskState::Working, 2),
2751 (TaskState::InputRequired, 3),
2752 (TaskState::Completed, 4),
2753 (TaskState::Canceled, 5),
2754 (TaskState::Failed, 6),
2755 (TaskState::Rejected, 7),
2756 (TaskState::AuthRequired, 8),
2757 (TaskState::Unknown, 9),
2758 (TaskState::Paused, 10),
2759 ];
2760 for (state, code) in expected {
2761 assert_eq!(state.code(), code);
2762 assert_eq!(TaskState::from_code(code), state);
2763 }
2764 let future = TaskState::from_code(42);
2766 assert_eq!(future, TaskState::Unrecognized(42));
2767 assert_eq!(future.code(), 42);
2768 assert!(!future.is_terminal());
2769 assert!(TaskState::Completed.is_terminal());
2770 assert!(!TaskState::Working.is_terminal());
2771 assert!(!TaskState::Paused.is_terminal());
2772 assert_eq!(TaskState::Paused.to_string(), "paused");
2773 assert_eq!("paused".parse::<TaskState>(), Ok(TaskState::Paused));
2774 }
2775
2776 #[test]
2777 fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
2778 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2779 assert_eq!(
2780 "auth-required".parse::<TaskState>().expect("parses"),
2781 TaskState::AuthRequired
2782 );
2783 assert!("nope".parse::<TaskState>().is_err());
2784 }
2785
2786 #[test]
2787 fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
2788 assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
2789 assert_eq!(AgentErrorCode::Internal.code(), 7);
2790 assert_eq!(
2791 AgentErrorCode::from_code(99),
2792 AgentErrorCode::Unrecognized(99)
2793 );
2794 assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
2795 assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
2796 assert_eq!(
2797 DeadLetterReason::from_code(77),
2798 DeadLetterReason::Unrecognized(77)
2799 );
2800 }
2801
2802 #[test]
2803 fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
2804 for code in 0u8..=u8::MAX {
2808 assert_eq!(TaskState::from_code(code).code(), code);
2809 assert_eq!(AgentErrorCode::from_code(code).code(), code);
2810 assert_eq!(DeadLetterReason::from_code(code).code(), code);
2811 }
2812 }
2813
2814 #[test]
2815 fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
2816 assert!("job-123-attempt-2".parse::<IdempotencyKey>().is_ok());
2817 assert!("".parse::<IdempotencyKey>().is_err());
2818 assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
2819 }
2820
2821 #[test]
2822 fn given_an_idempotency_key_with_a_control_character_when_parsed_then_should_refuse_it() {
2823 assert!("job-123\nattempt-2".parse::<IdempotencyKey>().is_err());
2824 assert!("job-123\u{7f}".parse::<IdempotencyKey>().is_err());
2825 assert!(
2826 serde_json::from_str::<IdempotencyKey>("\"job\\u0000\"").is_err(),
2827 "a decoded key is checked too"
2828 );
2829 }
2830
2831 #[test]
2832 fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
2833 let (record, conversation, source, correlation) = ids();
2834 let command =
2835 AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
2836 validate(&command).expect("a well-formed command validates");
2837
2838 let mut missing = command.clone();
2840 missing.correlation = None;
2841 assert_eq!(
2842 validate(&missing),
2843 Err(ValidateError::Missing {
2844 kind: AgentKind::Command,
2845 field: "correlation"
2846 })
2847 );
2848
2849 let with_usage = command.clone().with_usage(TokenUsage::default());
2851 assert!(matches!(
2852 validate(&with_usage),
2853 Err(ValidateError::Forbidden { field: "usage", .. })
2854 ));
2855
2856 let mut with_channel = command;
2858 with_channel.channel = Some(ChannelId::from_u128(1));
2859 assert!(matches!(
2860 validate(&with_channel),
2861 Err(ValidateError::Forbidden {
2862 field: "channel",
2863 ..
2864 })
2865 ));
2866 }
2867
2868 #[test]
2869 fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
2870 let (_, conversation, source, correlation) = ids();
2871 let channel = ChannelId::from_u128(23);
2872 let chunk = AgentEnvelope::chunk(
2873 conversation,
2874 source.clone(),
2875 correlation,
2876 channel,
2877 0,
2878 b"tok".to_vec(),
2879 )
2880 .with_operation(OPERATION_CHAT);
2881 validate(&chunk).expect("a stream chunk validates");
2882
2883 let mut undeclared = chunk.clone();
2885 undeclared.operation = None;
2886 assert!(matches!(
2887 validate(&undeclared),
2888 Err(ValidateError::Missing {
2889 field: "operation",
2890 ..
2891 })
2892 ));
2893 let mut off_vocabulary = chunk.clone();
2894 off_vocabulary.operation = Some("telemetry".to_owned());
2895 assert!(matches!(
2896 validate(&off_vocabulary),
2897 Err(ValidateError::Invalid {
2898 field: "operation",
2899 ..
2900 })
2901 ));
2902
2903 let redeclared = AgentEnvelope::chunk(
2905 conversation,
2906 source.clone(),
2907 correlation,
2908 channel,
2909 3,
2910 b"tok".to_vec(),
2911 )
2912 .with_operation(OPERATION_REASONING);
2913 assert!(matches!(
2914 validate(&redeclared),
2915 Err(ValidateError::Invalid {
2916 field: "operation",
2917 ..
2918 })
2919 ));
2920
2921 let terminal = AgentEnvelope::chunk(
2923 conversation,
2924 source.clone(),
2925 correlation,
2926 channel,
2927 41,
2928 Vec::new(),
2929 )
2930 .terminal("stop")
2931 .with_usage(TokenUsage {
2932 input_tokens: 100,
2933 output_tokens: 42,
2934 ..Default::default()
2935 });
2936 validate(&terminal).expect("a terminal chunk validates");
2937
2938 let mut early_finish = chunk.clone();
2940 early_finish.finish_reason = Some("stop".to_owned());
2941 assert!(matches!(
2942 validate(&early_finish),
2943 Err(ValidateError::Invalid {
2944 field: "finish_reason",
2945 ..
2946 })
2947 ));
2948
2949 let empty = AgentEnvelope::chunk(
2951 conversation,
2952 source.clone(),
2953 correlation,
2954 channel,
2955 1,
2956 Vec::new(),
2957 );
2958 assert!(matches!(
2959 validate(&empty),
2960 Err(ValidateError::Missing { field: "body", .. })
2961 ));
2962
2963 let mut keyed = chunk.clone();
2965 keyed.idempotency_key = Some("k".parse().expect("valid key"));
2966 assert!(matches!(
2967 validate(&keyed),
2968 Err(ValidateError::Forbidden {
2969 field: "idempotency_key",
2970 ..
2971 })
2972 ));
2973
2974 let opening = chunk.with_deadline_micros(1);
2976 validate(&opening).expect("an opening chunk may declare the bound");
2977 let late = AgentEnvelope::chunk(
2978 conversation,
2979 source.clone(),
2980 correlation,
2981 channel,
2982 5,
2983 b"tok".to_vec(),
2984 )
2985 .with_deadline_micros(1);
2986 assert!(matches!(
2987 validate(&late),
2988 Err(ValidateError::Invalid {
2989 field: "deadline_micros",
2990 ..
2991 })
2992 ));
2993 }
2994
2995 #[test]
2996 fn given_status_signals_when_validated_then_task_updates_should_require_state() {
2997 let (record, conversation, source, correlation) = ids();
2998 let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
3000 validate(&card).expect("a card validates");
3001
3002 let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
3004 assert!(matches!(
3005 validate(&bare_task),
3006 Err(ValidateError::Missing {
3007 field: "correlation",
3008 ..
3009 })
3010 ));
3011 let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
3012 .with_correlation(correlation)
3013 .with_task_state(TaskState::Working);
3014 validate(&task).expect("a task update validates");
3015
3016 let off_vocabulary =
3018 AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
3019 assert!(matches!(
3020 validate(&off_vocabulary),
3021 Err(ValidateError::Invalid {
3022 field: "operation",
3023 ..
3024 })
3025 ));
3026 }
3027
3028 #[test]
3029 fn given_registry_facts_when_validated_then_should_require_the_signed_deadline() {
3030 let (record, conversation, source, _) = ids();
3031 for operation in [OPERATION_QUARANTINE, OPERATION_UNQUARANTINE] {
3032 let mut fact = AgentEnvelope::status(record, conversation, source.clone(), operation);
3033 fact.body = b"worker".to_vec();
3034 assert!(matches!(
3035 validate(&fact),
3036 Err(ValidateError::Missing {
3037 field: "deadline_micros",
3038 ..
3039 })
3040 ));
3041 validate(&fact.with_deadline_micros(1)).expect("a fact with its deadline validates");
3042 }
3043 let card = AgentEnvelope::status(record, conversation, source, OPERATION_CARD)
3044 .with_deadline_micros(1);
3045 assert!(matches!(
3046 validate(&card),
3047 Err(ValidateError::Forbidden {
3048 field: "deadline_micros",
3049 ..
3050 })
3051 ));
3052 }
3053
3054 #[test]
3055 fn given_an_error_when_validated_then_last_should_be_forbidden() {
3056 let (record, conversation, source, correlation) = ids();
3057 let error =
3058 AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
3059 validate(&error).expect("an error validates");
3060
3061 let mut flagged = error.clone();
3063 flagged.last = true;
3064 assert!(matches!(
3065 validate(&flagged),
3066 Err(ValidateError::Forbidden { field: "last", .. })
3067 ));
3068
3069 let mut dangling = error;
3071 dangling.sequence = Some(3);
3072 assert!(matches!(
3073 validate(&dangling),
3074 Err(ValidateError::Invalid {
3075 field: "sequence",
3076 ..
3077 })
3078 ));
3079 }
3080
3081 fn descriptor(skill_id: &str) -> CapabilityDescriptor {
3082 CapabilityDescriptor {
3083 skill_id: skill_id.to_owned(),
3084 input: None,
3085 output: None,
3086 cost_class: None,
3087 latency_class: None,
3088 max_concurrency: None,
3089 health: None,
3090 load: None,
3091 }
3092 }
3093
3094 #[test]
3095 fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
3096 let card = AgentCard {
3097 name: Some("rollout-planner".to_owned()),
3098 version: Some("1.4.2".to_owned()),
3099 capabilities: vec![
3100 CapabilityDescriptor {
3101 skill_id: "chat".to_owned(),
3102 input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
3103 output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
3104 cost_class: Some(2),
3105 latency_class: Some(1),
3106 max_concurrency: Some(8),
3107 health: Some(Health::Healthy),
3108 load: Some(250),
3109 },
3110 descriptor("plan_rollout"),
3111 ],
3112 ttl_micros: Some(30_000_000),
3113 inbox: Some("rollout-planner.work".to_owned()),
3114 };
3115 card.validate().expect("a well-formed card validates");
3116
3117 let mut crowded = card.clone();
3118 crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
3119 assert!(matches!(
3120 crowded.validate(),
3121 Err(ValidateError::TooLarge {
3122 field: "capabilities",
3123 ..
3124 })
3125 ));
3126
3127 let mut oversized = card.clone();
3128 oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
3129 assert!(matches!(
3130 oversized.validate(),
3131 Err(ValidateError::TooLarge { field: "name", .. })
3132 ));
3133
3134 let mut long_inbox = card;
3135 long_inbox.inbox = Some("i".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
3136 assert!(matches!(
3137 long_inbox.validate(),
3138 Err(ValidateError::TooLarge { field: "inbox", .. })
3139 ));
3140 }
3141
3142 #[cfg(feature = "cbor")]
3143 #[test]
3144 fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
3145 {
3146 use crate::framing::{decode_named, encode_named};
3147 let reference = ContentRef::SchemaId("json".to_owned());
3150 let bytes = encode_named(&reference).expect("encodes");
3151 let back: ContentRef = decode_named(&bytes).expect("decodes");
3152 assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
3153 let ct = ContentRef::ContentType(crate::content::ContentType::Json);
3155 let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
3156 assert_eq!(back, ct);
3157 }
3158
3159 #[test]
3160 fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
3161 let valid = Signature {
3162 scheme: SIGNATURE_SCHEME_ED25519,
3163 key_id: vec![1u8; 8],
3164 bytes: vec![2u8; 64],
3165 context: None,
3166 };
3167 valid
3168 .validate()
3169 .expect("a well-formed Ed25519 signature validates");
3170
3171 let mut short_key = valid.clone();
3172 short_key.key_id = vec![1u8; 4];
3173 assert!(matches!(
3174 short_key.validate(),
3175 Err(ValidateError::Invalid {
3176 field: "key_id",
3177 ..
3178 })
3179 ));
3180
3181 let mut short_signature = valid.clone();
3182 short_signature.bytes = vec![2u8; 32];
3183 assert!(matches!(
3184 short_signature.validate(),
3185 Err(ValidateError::Invalid { field: "bytes", .. })
3186 ));
3187
3188 let future = Signature {
3191 scheme: 42,
3192 key_id: vec![1u8; 3],
3193 bytes: vec![2u8; 99],
3194 context: None,
3195 };
3196 future.validate().expect("an unknown scheme flows through");
3197 }
3198
3199 #[test]
3200 fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
3201 let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
3202 valid.validate().expect("a well-formed reference validates");
3203
3204 let mut empty = valid.clone();
3205 empty.reference = String::new();
3206 assert!(matches!(
3207 empty.validate(),
3208 Err(ValidateError::Invalid {
3209 field: "reference",
3210 ..
3211 })
3212 ));
3213
3214 let mut oversized = valid.clone();
3215 oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
3216 assert!(matches!(
3217 oversized.validate(),
3218 Err(ValidateError::TooLarge {
3219 field: "reference",
3220 ..
3221 })
3222 ));
3223
3224 let mut truncated = valid;
3225 truncated.sha256 = vec![7u8; 16];
3226 assert!(matches!(
3227 truncated.validate(),
3228 Err(ValidateError::Invalid {
3229 field: "sha256",
3230 ..
3231 })
3232 ));
3233 }
3234
3235 #[test]
3236 fn given_oversized_metadata_when_validated_then_should_reject() {
3237 let (record, conversation, source, correlation) = ids();
3238 let mut command =
3239 AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
3240 for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
3241 command = command.with_metadata(format!("k{i}"), i as i64);
3242 }
3243 assert!(matches!(
3244 validate(&command),
3245 Err(ValidateError::TooLarge {
3246 field: "metadata",
3247 ..
3248 })
3249 ));
3250 }
3251
3252 fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
3253 (
3254 RecordId::from_u128(7),
3255 ConversationId::from_u128(11),
3256 "test-agent".parse().expect("valid agent id"),
3257 CorrelationId::from_u128(17),
3258 )
3259 }
3260}
3261
3262#[cfg(all(test, feature = "cbor"))]
3263mod wire_tests {
3264 use super::*;
3265 use crate::framing::{decode_named, encode_named};
3266
3267 #[test]
3268 fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
3269 let mut valid = vec![0x40 | 20];
3272 valid.extend_from_slice(&[0u8; 20]);
3273 decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
3274
3275 for bad_len in [0u8, 19, 21, 23] {
3276 let mut bytes = vec![0x40 | bad_len];
3277 bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
3278 assert!(
3279 decode_named::<LogPosition>(&bytes).is_err(),
3280 "a {bad_len}-byte locator must error, not panic"
3281 );
3282 }
3283 }
3284
3285 #[test]
3286 fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
3287 let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
3288 let bytes = encode_named(&pos).expect("encodes");
3289 let back: LogPosition = decode_named(&bytes).expect("decodes");
3290 assert_eq!(back, pos);
3291 }
3292
3293 #[test]
3294 fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
3295 let envelope = AgentEnvelope::command(
3296 RecordId::from_u128(1),
3297 ConversationId::from_u128(2),
3298 "source-agent".parse().expect("valid agent id"),
3299 CorrelationId::from_u128(4),
3300 b"payload".to_vec(),
3301 )
3302 .with_target("target-agent".parse().expect("valid agent id"))
3303 .with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
3304 .with_idempotency_key("job-1".parse().expect("valid key"))
3305 .with_deadline_micros(1_700_000_000_000_000)
3306 .with_operation("chat")
3307 .with_tool("search")
3308 .with_metadata("pool", "eu-west");
3309 let bytes = encode_named(&envelope).expect("encodes");
3310 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3311 assert_eq!(back, envelope);
3312 }
3313
3314 #[test]
3315 fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
3316 let envelope = AgentEnvelope::event(
3320 RecordId::from_u128(1),
3321 ConversationId::from_u128(2),
3322 "source-agent".parse().expect("valid agent id"),
3323 b"e".to_vec(),
3324 )
3325 .requiring(0b101);
3326 let bytes = encode_named(&envelope).expect("encodes");
3327 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3328 assert_eq!(back.must_understand, 0b101);
3329 assert_eq!(back.unmet_requirements(0b001), 0b100);
3331 assert_eq!(back.unmet_requirements(0b111), 0);
3333 let plain = AgentEnvelope::event(
3335 RecordId::from_u128(1),
3336 ConversationId::from_u128(2),
3337 "source-agent".parse().expect("valid agent id"),
3338 b"e".to_vec(),
3339 );
3340 assert_eq!(plain.unmet_requirements(features::NONE), 0);
3341 let json = serde_json::to_string(&plain).expect("json");
3342 assert!(
3343 !json.contains("must_understand"),
3344 "zero marker must be omitted: {json}"
3345 );
3346 }
3347
3348 #[test]
3349 fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
3350 let envelope = AgentEnvelope::event(
3353 RecordId::from_u128(1),
3354 ConversationId::from_u128(2),
3355 "source-agent".parse().expect("valid agent id"),
3356 b"e".to_vec(),
3357 );
3358 let bytes = encode_named(&envelope).expect("encodes");
3359 assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
3362 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3363 assert!(!back.last);
3364 assert!(back.metadata.is_none());
3365 }
3366
3367 #[test]
3368 fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
3369 let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
3370 assert_eq!(bytes.len(), 17);
3373 assert_eq!(bytes[0], 0x50);
3374 assert_eq!(bytes[16], 0x02);
3375 assert_eq!(bytes[15], 0x01);
3376 }
3377
3378 #[test]
3379 fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
3380 let bytes = encode_named(&TaskState::Completed).expect("encodes");
3381 assert_eq!(bytes, vec![4]);
3382 let unknown = encode_named(&42u8).expect("encodes a raw code");
3383 let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
3384 assert_eq!(back, TaskState::Unrecognized(42));
3385 }
3386
3387 #[test]
3388 fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
3389 let body = AgentErrorBody {
3390 code: AgentErrorCode::ToolFailure,
3391 message: Some("search timed out".to_owned()),
3392 retryable: true,
3393 detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
3394 };
3395 let bytes = encode_named(&body).expect("encodes");
3396 let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
3397 assert_eq!(back, body);
3398 }
3399
3400 #[test]
3401 fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
3402 let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
3403 let bytes = encode_named(&capsule).expect("encodes");
3404 let back: BodyRef = decode_named(&bytes).expect("decodes");
3405 assert_eq!(back, capsule);
3406 assert!(back.encryption.is_none(), "absent encryption costs nothing");
3407 back.validate().expect("decoded capsule validates");
3408 }
3409
3410 #[test]
3411 fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
3412 let inner = AgentEnvelope::command(
3413 RecordId::from_u128(9),
3414 ConversationId::from_u128(8),
3415 "source-agent".parse().expect("valid agent id"),
3416 CorrelationId::from_u128(6),
3417 b"poison".to_vec(),
3418 );
3419 let payload = encode_named(&inner).expect("inner encodes");
3420 let capsule = AgentDeadLetter {
3421 source: LogPosition::new(1, 2, 3, 99),
3422 reason: DeadLetterReason::RetryExhausted,
3423 attempts: 5,
3424 detail: Some("handler kept failing".to_owned()),
3425 payload: payload.clone(),
3426 };
3427 let bytes = encode_named(&capsule).expect("encodes");
3428 let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
3429 assert_eq!(back.payload, payload, "redrive needs the original bytes");
3430 let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
3431 assert_eq!(redrive, inner);
3432 }
3433 #[test]
3434 fn given_a_task_status_when_validated_then_should_require_the_correlation() {
3435 let base = AgentEnvelope::status(
3437 RecordId::from_u128(1),
3438 ConversationId::from_u128(2),
3439 "runner".parse().expect("valid agent id"),
3440 OPERATION_TASK,
3441 )
3442 .with_task_state(TaskState::Working);
3443 assert!(
3444 matches!(
3445 validate(&base),
3446 Err(ValidateError::Missing {
3447 kind: AgentKind::Status,
3448 field: "correlation"
3449 })
3450 ),
3451 "a task status without a correlation is rejected"
3452 );
3453 let correlated = base.with_correlation(CorrelationId::from_u128(2));
3454 validate(&correlated)
3455 .expect("the run-lifecycle status validates once it correlates on the run");
3456 }
3457
3458 #[test]
3459 fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
3460 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
3461 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
3462 assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
3463 assert_eq!(
3464 "input-required".parse::<TaskState>().expect("parses"),
3465 TaskState::InputRequired
3466 );
3467 assert!("bogus".parse::<TaskState>().is_err());
3468 }
3469}