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 if value.chars().any(char::is_control) {
367 return Err(InvalidError::new(
368 "idempotency key contains a control character",
369 ));
370 }
371 Ok(Self(value))
372 }
373}
374
375impl From<IdempotencyKey> for String {
376 fn from(value: IdempotencyKey) -> Self {
377 value.0
378 }
379}
380
381impl fmt::Display for IdempotencyKey {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 f.write_str(&self.0)
384 }
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
397#[serde(try_from = "String", into = "String")]
398pub struct AgentId(String);
399
400impl AgentId {
401 pub fn as_str(&self) -> &str {
403 &self.0
404 }
405}
406
407impl FromStr for AgentId {
408 type Err = InvalidError;
409
410 fn from_str(s: &str) -> Result<Self, Self::Err> {
411 s.to_owned().try_into()
412 }
413}
414
415impl TryFrom<String> for AgentId {
416 type Error = InvalidError;
417
418 fn try_from(value: String) -> Result<Self, Self::Error> {
419 if value.is_empty() {
420 return Err(InvalidError::new("agent id must not be empty"));
421 }
422 if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
423 return Err(InvalidError::new(format!(
424 "agent id is {}B, exceeds cap {}B",
425 value.len(),
426 crate::limits::MAX_AGENT_STRING_BYTES
427 )));
428 }
429 if let Some(c) = value.chars().find(|c| c.is_control()) {
430 return Err(InvalidError::new(format!(
431 "agent id must not contain control characters (found {c:?})"
432 )));
433 }
434 Ok(Self(value))
435 }
436}
437
438impl TryFrom<&str> for AgentId {
439 type Error = InvalidError;
440
441 fn try_from(value: &str) -> Result<Self, Self::Error> {
442 value.to_owned().try_into()
443 }
444}
445
446impl From<AgentId> for String {
447 fn from(value: AgentId) -> Self {
448 value.0
449 }
450}
451
452impl fmt::Display for AgentId {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 f.write_str(&self.0)
455 }
456}
457
458#[derive(
462 Clone,
463 Copy,
464 Debug,
465 PartialEq,
466 Eq,
467 Hash,
468 Serialize,
469 Deserialize,
470 strum::Display,
471 strum::EnumString,
472)]
473#[serde(rename_all = "snake_case")]
474#[strum(serialize_all = "snake_case")]
475pub enum AgentKind {
476 Command,
479 Response,
481 Event,
483 Chunk,
485 Status,
488 Error,
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
504#[serde(from = "u8", into = "u8")]
505pub enum TaskState {
506 Submitted,
507 Working,
508 InputRequired,
509 Completed,
510 Canceled,
511 Failed,
512 Rejected,
513 AuthRequired,
514 Unknown,
515 Paused,
517 Unrecognized(u8),
519}
520
521impl TaskState {
522 pub const fn code(self) -> u8 {
524 match self {
525 TaskState::Submitted => 1,
526 TaskState::Working => 2,
527 TaskState::InputRequired => 3,
528 TaskState::Completed => 4,
529 TaskState::Canceled => 5,
530 TaskState::Failed => 6,
531 TaskState::Rejected => 7,
532 TaskState::AuthRequired => 8,
533 TaskState::Unknown => 9,
534 TaskState::Paused => 10,
535 TaskState::Unrecognized(code) => code,
536 }
537 }
538
539 pub const fn from_code(code: u8) -> Self {
542 match code {
543 1 => TaskState::Submitted,
544 2 => TaskState::Working,
545 3 => TaskState::InputRequired,
546 4 => TaskState::Completed,
547 5 => TaskState::Canceled,
548 6 => TaskState::Failed,
549 7 => TaskState::Rejected,
550 8 => TaskState::AuthRequired,
551 9 => TaskState::Unknown,
552 10 => TaskState::Paused,
553 other => TaskState::Unrecognized(other),
554 }
555 }
556
557 pub const fn is_terminal(self) -> bool {
560 matches!(
561 self,
562 TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
563 )
564 }
565}
566
567impl From<u8> for TaskState {
568 fn from(code: u8) -> Self {
569 Self::from_code(code)
570 }
571}
572
573impl From<TaskState> for u8 {
574 fn from(state: TaskState) -> u8 {
575 state.code()
576 }
577}
578
579impl fmt::Display for TaskState {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582 match self {
583 TaskState::Submitted => f.write_str("submitted"),
584 TaskState::Working => f.write_str("working"),
585 TaskState::InputRequired => f.write_str("input-required"),
586 TaskState::Completed => f.write_str("completed"),
587 TaskState::Canceled => f.write_str("canceled"),
588 TaskState::Failed => f.write_str("failed"),
589 TaskState::Rejected => f.write_str("rejected"),
590 TaskState::AuthRequired => f.write_str("auth-required"),
591 TaskState::Unknown => f.write_str("unknown"),
592 TaskState::Paused => f.write_str("paused"),
593 TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
594 }
595 }
596}
597
598impl FromStr for TaskState {
599 type Err = InvalidError;
600
601 fn from_str(s: &str) -> Result<Self, Self::Err> {
602 Ok(match s {
603 "submitted" => TaskState::Submitted,
604 "working" => TaskState::Working,
605 "input-required" => TaskState::InputRequired,
606 "completed" => TaskState::Completed,
607 "canceled" => TaskState::Canceled,
608 "failed" => TaskState::Failed,
609 "rejected" => TaskState::Rejected,
610 "auth-required" => TaskState::AuthRequired,
611 "unknown" => TaskState::Unknown,
612 "paused" => TaskState::Paused,
613 other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
614 })
615 }
616}
617
618#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
620#[serde(rename_all = "snake_case")]
621pub enum SessionStatus {
622 Submitted,
623 Active,
624 Paused,
625 Completed,
626 Failed,
627 Canceled,
628 #[serde(other)]
629 Unrecognized,
630}
631
632#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
634pub struct SessionRef {
635 pub stream: String,
636 pub session: ConversationId,
637}
638
639#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
641pub struct SdkInfo {
642 pub language: String,
643 pub version: String,
644}
645
646#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
648pub struct Budget {
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub tokens: Option<u64>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub cost_micros: Option<u64>,
653}
654
655#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
657pub struct SessionStart {
658 #[serde(default, skip_serializing_if = "Option::is_none")]
659 pub label: Option<String>,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub namespace: Option<String>,
662 pub agent: AgentId,
663 pub sdk: SdkInfo,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub parent: Option<ConversationId>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub root: Option<ConversationId>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub idle_timeout_micros: Option<u64>,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub budget: Option<Budget>,
672 #[serde(default, skip_serializing_if = "Vec::is_empty")]
673 pub tags: Vec<String>,
674}
675
676#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
678pub struct SessionTransition {
679 #[serde(default, skip_serializing_if = "Option::is_none")]
680 pub actor: Option<AgentId>,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub acknowledges: Option<LogPosition>,
683}
684
685#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
690pub struct SessionPauseRequest {
691 #[serde(default, skip_serializing_if = "Vec::is_empty")]
692 pub participants: Vec<AgentId>,
693}
694
695#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
701pub struct SessionParking {
702 pub source: crate::graph::SourceRef,
703 pub role: AgentId,
704 pub request: LogPosition,
705}
706
707impl crate::validate::Validate for SessionParking {
708 fn validate(&self) -> Result<(), InvalidError> {
709 match &self.source {
710 crate::graph::SourceRef::Message {
711 generation: Some(_),
712 ..
713 } => Ok(()),
714 crate::graph::SourceRef::Message { .. } => Err(InvalidError::new(
715 "a parked record must name its source topic generation",
716 )),
717 _ => Err(InvalidError::new(
718 "a parked record must point at a log record",
719 )),
720 }
721 }
722}
723
724#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
726pub struct SessionEnd {
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub reason: Option<String>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub error: Option<AgentErrorBody>,
731}
732
733pub const fn estimate_tokens(bytes: usize) -> u64 {
736 bytes.div_ceil(4) as u64
737}
738
739pub const METADATA_REQUEST_MODEL: &str = "gen_ai.request.model";
741pub const METADATA_RESPONSE_MODEL: &str = "gen_ai.response.model";
743pub const METADATA_PROVIDER_NAME: &str = "gen_ai.provider.name";
745pub const METADATA_SUBMITTED: &str = "submitted";
749pub const METADATA_DURATION_MICROS: &str = "duration_micros";
752
753#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
755pub struct ContextManifest {
756 pub policy: String,
757 pub policy_version: String,
758 pub fragments: Vec<Fragment>,
759 pub tokens: u64,
760 pub bytes: u64,
761 pub frontier: Vec<(u32, u32, u64)>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub correlation: Option<CorrelationId>,
764}
765
766#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
768pub enum Fragment {
769 Message {
770 at: crate::graph::SourceRef,
771 tokens: u32,
772 bytes: u32,
773 },
774 Memory {
775 id: String,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
777 version: Option<u64>,
778 #[serde(default, skip_serializing_if = "Option::is_none")]
779 digest: Option<crate::schema::Digest32>,
780 tokens: u32,
781 bytes: u32,
782 },
783 Kv {
784 namespace: String,
785 key: String,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
787 version: Option<u64>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
789 digest: Option<crate::schema::Digest32>,
790 tokens: u32,
791 bytes: u32,
792 },
793 State {
794 key: String,
795 revision: u64,
796 tokens: u32,
797 bytes: u32,
798 },
799 Summary {
800 at: crate::graph::SourceRef,
801 tokens: u32,
802 bytes: u32,
803 },
804}
805
806#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
808pub struct ContextCompaction {
809 pub summary_at: crate::graph::SourceRef,
810 pub covered: Vec<(u32, u32, u64, u64)>,
811 pub summarizer: crate::graph::ProducerInfo,
812}
813
814#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
816pub struct ContextRetrieval {
817 #[serde(default, skip_serializing_if = "Option::is_none")]
818 pub query: Option<String>,
819 pub items: Vec<(String, f32)>,
820}
821
822pub use json_patch::PatchOperation;
824
825#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
827pub struct StateDelta {
828 pub base_revision: u64,
829 pub patch: Vec<json_patch::PatchOperation>,
830 pub op_id: String,
831}
832
833#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
835pub struct StateSnapshot {
836 pub base_revision: u64,
837 pub document: serde_json::Value,
838}
839
840impl crate::validate::Validate for ContextManifest {
841 fn validate(&self) -> Result<(), InvalidError> {
842 if self.fragments.len() > crate::limits::MAX_MANIFEST_FRAGMENTS {
843 return Err(InvalidError::new("context manifest has too many fragments"));
844 }
845 for fragment in &self.fragments {
846 let digest = match fragment {
847 Fragment::Memory { digest, .. } | Fragment::Kv { digest, .. } => digest.as_ref(),
848 _ => None,
849 };
850 if let Some(digest) = digest {
851 crate::validate::Validate::validate(digest)?;
852 }
853 }
854 Ok(())
855 }
856}
857
858impl crate::validate::Validate for StateDelta {
859 fn validate(&self) -> Result<(), InvalidError> {
860 if self.patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
861 return Err(InvalidError::new("state patch has too many operations"));
862 }
863 if self.op_id.is_empty() {
864 return Err(InvalidError::new(
865 "state patch operation id must not be empty",
866 ));
867 }
868 let bytes = serde_json::to_vec(&self.patch)
869 .map_err(|error| InvalidError::new(error.to_string()))?;
870 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
871 return Err(InvalidError::new(
872 "state patch exceeds the document byte cap",
873 ));
874 }
875 let patch = serde_json::to_value(&self.patch)
876 .map_err(|error| InvalidError::new(error.to_string()))?;
877 validate_portable_json(&patch)?;
878 Ok(())
879 }
880}
881
882impl crate::validate::Validate for StateSnapshot {
883 fn validate(&self) -> Result<(), InvalidError> {
884 let bytes = serde_json::to_vec(&self.document)
885 .map_err(|error| InvalidError::new(error.to_string()))?;
886 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
887 return Err(InvalidError::new("state document exceeds its byte cap"));
888 }
889 validate_portable_json(&self.document)?;
890 Ok(())
891 }
892}
893
894const MAX_EXACT_JSON_INTEGER: i128 = 9_007_199_254_740_991;
895
896fn validate_portable_json(value: &serde_json::Value) -> Result<(), InvalidError> {
897 match value {
898 serde_json::Value::Number(number) => {
899 let exact = if let Some(integer) = number.as_i64() {
900 i128::from(integer).abs() <= MAX_EXACT_JSON_INTEGER
901 } else if let Some(integer) = number.as_u64() {
902 i128::from(integer) <= MAX_EXACT_JSON_INTEGER
903 } else {
904 number.as_f64().is_some_and(|float| {
905 float.is_finite()
906 && (float.fract() != 0.0 || float.abs() <= MAX_EXACT_JSON_INTEGER as f64)
907 })
908 };
909 if !exact {
910 return Err(InvalidError::new(
911 "state JSON integer exceeds the exact number range",
912 ));
913 }
914 }
915 serde_json::Value::Array(items) => {
916 for item in items {
917 validate_portable_json(item)?;
918 }
919 }
920 serde_json::Value::Object(fields) => {
921 for item in fields.values() {
922 validate_portable_json(item)?;
923 }
924 }
925 _ => {}
926 }
927 Ok(())
928}
929
930pub fn apply_json_patch(
932 document: &serde_json::Value,
933 patch: &[json_patch::PatchOperation],
934) -> Result<serde_json::Value, InvalidError> {
935 if patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
936 return Err(InvalidError::new("state patch has too many operations"));
937 }
938 let patch_bytes =
939 serde_json::to_vec(patch).map_err(|error| InvalidError::new(error.to_string()))?;
940 if patch_bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
941 return Err(InvalidError::new(
942 "state patch exceeds the document byte cap",
943 ));
944 }
945 validate_portable_json(document)?;
946 let patch_value =
947 serde_json::to_value(patch).map_err(|error| InvalidError::new(error.to_string()))?;
948 validate_portable_json(&patch_value)?;
949 let mut result = document.clone();
950 json_patch::patch(&mut result, patch).map_err(|error| InvalidError::new(error.to_string()))?;
951 crate::validate::Validate::validate(&StateSnapshot {
952 base_revision: 0,
953 document: result.clone(),
954 })?;
955 Ok(result)
956}
957
958impl crate::validate::Validate for SessionStart {
959 fn validate(&self) -> Result<(), InvalidError> {
960 if let Some(label) = &self.label
961 && (label.len() > crate::limits::MAX_SESSION_LABEL_BYTES
962 || label.chars().any(char::is_control))
963 {
964 return Err(InvalidError::new(
965 "session label exceeds its cap or contains a control character",
966 ));
967 }
968 if let Some(namespace) = &self.namespace {
969 crate::kv::validate_namespace(namespace)?;
970 }
971 if self.root.is_some() && self.parent.is_none() {
972 return Err(InvalidError::new("session root requires a parent"));
973 }
974 if self.tags.len() > crate::limits::MAX_MEMORY_TAGS {
975 return Err(InvalidError::new("session has too many tags"));
976 }
977 if self
978 .tags
979 .iter()
980 .any(|tag| tag.len() > crate::limits::MAX_MEMORY_TAG_BYTES)
981 {
982 return Err(InvalidError::new("session tag exceeds its byte cap"));
983 }
984 Ok(())
985 }
986}
987
988#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
995pub struct TokenUsage {
996 pub input_tokens: u64,
997 pub output_tokens: u64,
998 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub reasoning_output_tokens: Option<u64>,
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub cache_read_input_tokens: Option<u64>,
1002 #[serde(default, skip_serializing_if = "Option::is_none")]
1003 pub cache_creation_input_tokens: Option<u64>,
1004 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub cost_micros: Option<u64>,
1007}
1008
1009#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1012#[serde(from = "u8", into = "u8")]
1013pub enum AgentErrorCode {
1014 InvalidRequest,
1015 Unauthorized,
1016 Unsupported,
1017 DeadlineExceeded,
1018 Cancelled,
1019 ToolFailure,
1020 Internal,
1021 Unrecognized(u8),
1023}
1024
1025impl AgentErrorCode {
1026 pub const fn code(self) -> u8 {
1028 match self {
1029 AgentErrorCode::InvalidRequest => 1,
1030 AgentErrorCode::Unauthorized => 2,
1031 AgentErrorCode::Unsupported => 3,
1032 AgentErrorCode::DeadlineExceeded => 4,
1033 AgentErrorCode::Cancelled => 5,
1034 AgentErrorCode::ToolFailure => 6,
1035 AgentErrorCode::Internal => 7,
1036 AgentErrorCode::Unrecognized(code) => code,
1037 }
1038 }
1039
1040 pub const fn from_code(code: u8) -> Self {
1042 match code {
1043 1 => AgentErrorCode::InvalidRequest,
1044 2 => AgentErrorCode::Unauthorized,
1045 3 => AgentErrorCode::Unsupported,
1046 4 => AgentErrorCode::DeadlineExceeded,
1047 5 => AgentErrorCode::Cancelled,
1048 6 => AgentErrorCode::ToolFailure,
1049 7 => AgentErrorCode::Internal,
1050 other => AgentErrorCode::Unrecognized(other),
1051 }
1052 }
1053}
1054
1055impl From<u8> for AgentErrorCode {
1056 fn from(code: u8) -> Self {
1057 Self::from_code(code)
1058 }
1059}
1060
1061impl From<AgentErrorCode> for u8 {
1062 fn from(code: AgentErrorCode) -> u8 {
1063 code.code()
1064 }
1065}
1066
1067#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1071pub struct AgentErrorBody {
1072 pub code: AgentErrorCode,
1073 #[serde(default, skip_serializing_if = "Option::is_none")]
1074 pub message: Option<String>,
1075 #[serde(default)]
1076 pub retryable: bool,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub detail: Option<BTreeMap<String, Value>>,
1079}
1080
1081#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1083#[serde(from = "u8", into = "u8")]
1084pub enum DeadLetterReason {
1085 RetryExhausted,
1086 Rejected,
1087 DecodeFailed,
1088 DeadlineExceeded,
1089 Unrecognized(u8),
1091}
1092
1093impl DeadLetterReason {
1094 pub const fn code(self) -> u8 {
1096 match self {
1097 DeadLetterReason::RetryExhausted => 1,
1098 DeadLetterReason::Rejected => 2,
1099 DeadLetterReason::DecodeFailed => 3,
1100 DeadLetterReason::DeadlineExceeded => 4,
1101 DeadLetterReason::Unrecognized(code) => code,
1102 }
1103 }
1104
1105 pub const fn from_code(code: u8) -> Self {
1107 match code {
1108 1 => DeadLetterReason::RetryExhausted,
1109 2 => DeadLetterReason::Rejected,
1110 3 => DeadLetterReason::DecodeFailed,
1111 4 => DeadLetterReason::DeadlineExceeded,
1112 other => DeadLetterReason::Unrecognized(other),
1113 }
1114 }
1115}
1116
1117impl From<u8> for DeadLetterReason {
1118 fn from(code: u8) -> Self {
1119 Self::from_code(code)
1120 }
1121}
1122
1123impl From<DeadLetterReason> for u8 {
1124 fn from(reason: DeadLetterReason) -> u8 {
1125 reason.code()
1126 }
1127}
1128
1129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1139pub struct AgentDeadLetter {
1140 pub source: LogPosition,
1141 pub reason: DeadLetterReason,
1142 pub attempts: u32,
1143 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub detail: Option<String>,
1145 #[serde(with = "crate::encoding::bin_bytes")]
1146 pub payload: Vec<u8>,
1147}
1148
1149#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1152#[serde(from = "u8", into = "u8")]
1153pub enum Health {
1154 Healthy,
1155 Degraded,
1156 Unavailable,
1157 Unrecognized(u8),
1159}
1160
1161impl Health {
1162 pub const fn code(self) -> u8 {
1164 match self {
1165 Health::Healthy => 1,
1166 Health::Degraded => 2,
1167 Health::Unavailable => 3,
1168 Health::Unrecognized(code) => code,
1169 }
1170 }
1171
1172 pub const fn from_code(code: u8) -> Self {
1175 match code {
1176 1 => Health::Healthy,
1177 2 => Health::Degraded,
1178 3 => Health::Unavailable,
1179 other => Health::Unrecognized(other),
1180 }
1181 }
1182}
1183
1184impl From<u8> for Health {
1185 fn from(code: u8) -> Self {
1186 Self::from_code(code)
1187 }
1188}
1189
1190impl From<Health> for u8 {
1191 fn from(health: Health) -> u8 {
1192 health.code()
1193 }
1194}
1195
1196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1204#[serde(rename_all = "snake_case")]
1205pub enum ContentRef {
1206 ContentType(crate::content::ContentType),
1207 SchemaId(String),
1208}
1209
1210#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1214pub struct CapabilityDescriptor {
1215 pub skill_id: String,
1217 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub input: Option<ContentRef>,
1219 #[serde(default, skip_serializing_if = "Option::is_none")]
1220 pub output: Option<ContentRef>,
1221 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub cost_class: Option<u8>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1226 pub latency_class: Option<u8>,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub max_concurrency: Option<u32>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub health: Option<Health>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub load: Option<u16>,
1234}
1235
1236#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1244pub struct AgentCard {
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub name: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub version: Option<String>,
1252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1255 pub capabilities: Vec<CapabilityDescriptor>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub ttl_micros: Option<u64>,
1260}
1261
1262impl AgentCard {
1263 pub fn validate(&self) -> Result<(), ValidateError> {
1265 cap_str(self.name.as_deref(), "name")?;
1266 cap_str(self.version.as_deref(), "version")?;
1267 if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
1268 return Err(ValidateError::TooLarge {
1269 field: "capabilities",
1270 size: self.capabilities.len(),
1271 cap: crate::limits::MAX_CARD_CAPABILITIES,
1272 });
1273 }
1274 for capability in &self.capabilities {
1275 cap_str(Some(&capability.skill_id), "capability skill_id")?;
1276 }
1277 Ok(())
1278 }
1279}
1280
1281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1299pub struct AgentPresence {
1300 pub v: u32,
1303 pub agent: AgentId,
1308 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 pub inbox: Option<String>,
1313}
1314
1315impl AgentPresence {
1316 pub fn new(agent: AgentId) -> Self {
1319 Self {
1320 v: crate::codes::PRESENCE_OP_VERSION,
1321 agent,
1322 inbox: None,
1323 }
1324 }
1325
1326 pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
1328 self.inbox = Some(inbox.into());
1329 self
1330 }
1331
1332 pub fn validate(&self) -> Result<(), ValidateError> {
1335 cap_str(self.inbox.as_deref(), "inbox")?;
1336 Ok(())
1337 }
1338}
1339
1340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1349pub struct BodyRef {
1350 pub reference: String,
1353 pub size_bytes: u64,
1355 #[serde(with = "crate::encoding::bin_bytes")]
1357 pub sha256: Vec<u8>,
1358 #[serde(default, skip_serializing_if = "Option::is_none")]
1362 pub encryption: Option<u8>,
1363}
1364
1365const SHA256_BYTES: usize = 32;
1366
1367impl BodyRef {
1368 pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
1370 Self {
1371 reference: reference.into(),
1372 size_bytes,
1373 sha256: sha256.to_vec(),
1374 encryption: None,
1375 }
1376 }
1377
1378 pub fn validate(&self) -> Result<(), ValidateError> {
1382 if self.reference.is_empty() {
1383 return Err(ValidateError::Invalid {
1384 field: "reference",
1385 reason: "reference must not be empty".to_owned(),
1386 });
1387 }
1388 if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
1389 return Err(ValidateError::TooLarge {
1390 field: "reference",
1391 size: self.reference.len(),
1392 cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
1393 });
1394 }
1395 if self.sha256.len() != SHA256_BYTES {
1396 return Err(ValidateError::Invalid {
1397 field: "sha256",
1398 reason: format!(
1399 "digest must be {SHA256_BYTES} bytes, got {}",
1400 self.sha256.len()
1401 ),
1402 });
1403 }
1404 Ok(())
1405 }
1406}
1407
1408#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1420pub struct Signature {
1421 pub scheme: u8,
1424 #[serde(with = "crate::encoding::bin_bytes")]
1426 pub key_id: Vec<u8>,
1427 #[serde(with = "crate::encoding::bin_bytes")]
1430 pub bytes: Vec<u8>,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 pub context: Option<SignatureContext>,
1438}
1439
1440#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1446pub struct SignatureContext {
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1449 pub content_type: Option<u8>,
1450 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 pub agent_version: Option<u32>,
1453}
1454
1455pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
1457
1458pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
1463
1464const ED25519_KEY_ID_BYTES: usize = 8;
1465const ED25519_SIGNATURE_BYTES: usize = 64;
1466
1467impl Signature {
1468 pub fn validate(&self) -> Result<(), ValidateError> {
1473 if self.scheme != SIGNATURE_SCHEME_ED25519 {
1474 return Ok(());
1475 }
1476 if self.key_id.len() != ED25519_KEY_ID_BYTES {
1477 return Err(ValidateError::Invalid {
1478 field: "key_id",
1479 reason: format!(
1480 "Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
1481 self.key_id.len()
1482 ),
1483 });
1484 }
1485 if self.bytes.len() != ED25519_SIGNATURE_BYTES {
1486 return Err(ValidateError::Invalid {
1487 field: "bytes",
1488 reason: format!(
1489 "Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
1490 self.bytes.len()
1491 ),
1492 });
1493 }
1494 Ok(())
1495 }
1496}
1497
1498#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1511pub struct AgentEnvelope {
1512 pub kind: AgentKind,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 pub record: Option<RecordId>,
1518 pub conversation: ConversationId,
1520 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub parent: Option<ConversationId>,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1525 pub root: Option<ConversationId>,
1526 pub source: AgentId,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1532 pub target: Option<AgentId>,
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub cause: Option<RecordId>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub cause_at: Option<LogPosition>,
1541 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub correlation: Option<CorrelationId>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub channel: Option<ChannelId>,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub idempotency_key: Option<IdempotencyKey>,
1549 #[serde(default, skip_serializing_if = "Option::is_none")]
1553 pub deadline_micros: Option<u64>,
1554 #[serde(default, skip_serializing_if = "Option::is_none")]
1556 pub sequence: Option<u64>,
1557 #[serde(default, skip_serializing_if = "is_false")]
1561 pub last: bool,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1566 pub finish_reason: Option<String>,
1567 #[serde(default, skip_serializing_if = "Option::is_none")]
1568 pub task_state: Option<TaskState>,
1569 #[serde(default, skip_serializing_if = "Option::is_none")]
1572 pub operation: Option<String>,
1573 #[serde(default, skip_serializing_if = "Option::is_none")]
1575 pub tool: Option<String>,
1576 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub usage: Option<TokenUsage>,
1578 #[serde(default, skip_serializing_if = "Option::is_none")]
1580 pub metadata: Option<BTreeMap<String, Value>>,
1581 #[serde(default, skip_serializing_if = "is_zero_u64")]
1593 pub must_understand: u64,
1594 #[serde(
1597 default,
1598 skip_serializing_if = "Vec::is_empty",
1599 with = "crate::encoding::bin_bytes"
1600 )]
1601 pub body: Vec<u8>,
1602 #[serde(default, skip_serializing_if = "Option::is_none")]
1609 pub signature: Option<Signature>,
1610}
1611
1612fn is_false(value: &bool) -> bool {
1613 !*value
1614}
1615
1616fn is_zero_u64(value: &u64) -> bool {
1617 *value == 0
1618}
1619
1620pub mod features {
1627 pub const NONE: u64 = 0;
1631}
1632
1633impl AgentEnvelope {
1634 fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
1635 Self {
1636 kind,
1637 record: None,
1638 conversation,
1639 parent: None,
1640 root: None,
1641 source,
1642 target: None,
1643 cause: None,
1644 cause_at: None,
1645 correlation: None,
1646 channel: None,
1647 idempotency_key: None,
1648 deadline_micros: None,
1649 sequence: None,
1650 last: false,
1651 finish_reason: None,
1652 task_state: None,
1653 operation: None,
1654 tool: None,
1655 usage: None,
1656 metadata: None,
1657 must_understand: 0,
1658 body: Vec::new(),
1659 signature: None,
1660 }
1661 }
1662
1663 #[must_use]
1668 pub fn requiring(mut self, bits: u64) -> Self {
1669 self.must_understand = bits;
1670 self
1671 }
1672
1673 pub fn unmet_requirements(&self, understood: u64) -> u64 {
1678 self.must_understand & !understood
1679 }
1680
1681 pub fn command(
1683 record: RecordId,
1684 conversation: ConversationId,
1685 source: AgentId,
1686 correlation: CorrelationId,
1687 body: Vec<u8>,
1688 ) -> Self {
1689 let mut envelope = Self::base(AgentKind::Command, conversation, source);
1690 envelope.record = Some(record);
1691 envelope.correlation = Some(correlation);
1692 envelope.body = body;
1693 envelope
1694 }
1695
1696 pub fn response(
1698 record: RecordId,
1699 conversation: ConversationId,
1700 source: AgentId,
1701 correlation: CorrelationId,
1702 body: Vec<u8>,
1703 ) -> Self {
1704 let mut envelope = Self::base(AgentKind::Response, conversation, source);
1705 envelope.record = Some(record);
1706 envelope.correlation = Some(correlation);
1707 envelope.body = body;
1708 envelope
1709 }
1710
1711 pub fn event(
1713 record: RecordId,
1714 conversation: ConversationId,
1715 source: AgentId,
1716 body: Vec<u8>,
1717 ) -> Self {
1718 let mut envelope = Self::base(AgentKind::Event, conversation, source);
1719 envelope.record = Some(record);
1720 envelope.body = body;
1721 envelope
1722 }
1723
1724 pub fn chunk(
1727 conversation: ConversationId,
1728 source: AgentId,
1729 correlation: CorrelationId,
1730 channel: ChannelId,
1731 sequence: u64,
1732 body: Vec<u8>,
1733 ) -> Self {
1734 let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
1735 envelope.correlation = Some(correlation);
1736 envelope.channel = Some(channel);
1737 envelope.sequence = Some(sequence);
1738 envelope.body = body;
1739 envelope
1740 }
1741
1742 pub fn status(
1746 record: RecordId,
1747 conversation: ConversationId,
1748 source: AgentId,
1749 operation: impl Into<String>,
1750 ) -> Self {
1751 let mut envelope = Self::base(AgentKind::Status, conversation, source);
1752 envelope.record = Some(record);
1753 envelope.operation = Some(operation.into());
1754 envelope
1755 }
1756
1757 pub fn error(
1760 record: RecordId,
1761 conversation: ConversationId,
1762 source: AgentId,
1763 correlation: CorrelationId,
1764 body: Vec<u8>,
1765 ) -> Self {
1766 let mut envelope = Self::base(AgentKind::Error, conversation, source);
1767 envelope.record = Some(record);
1768 envelope.correlation = Some(correlation);
1769 envelope.body = body;
1770 envelope
1771 }
1772
1773 pub fn with_target(mut self, target: AgentId) -> Self {
1775 self.target = Some(target);
1776 self
1777 }
1778
1779 pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
1783 self.cause = Some(cause);
1784 self.cause_at = cause_at;
1785 self
1786 }
1787
1788 pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
1792 self.correlation = Some(correlation);
1793 self
1794 }
1795
1796 pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
1798 self.idempotency_key = Some(key);
1799 self
1800 }
1801
1802 pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
1805 self.deadline_micros = Some(deadline_micros);
1806 self
1807 }
1808
1809 pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
1812 self.last = true;
1813 self.finish_reason = Some(finish_reason.into());
1814 self
1815 }
1816
1817 pub fn with_task_state(mut self, state: TaskState) -> Self {
1820 self.task_state = Some(state);
1821 self
1822 }
1823
1824 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
1826 self.operation = Some(operation.into());
1827 self
1828 }
1829
1830 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
1832 self.tool = Some(tool.into());
1833 self
1834 }
1835
1836 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
1838 self.usage = Some(usage);
1839 self
1840 }
1841
1842 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1844 self.metadata
1845 .get_or_insert_with(BTreeMap::new)
1846 .insert(key.into(), value.into());
1847 self
1848 }
1849
1850 pub fn with_signature(mut self, signature: Signature) -> Self {
1855 self.signature = Some(signature);
1856 self
1857 }
1858}
1859
1860#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1863#[non_exhaustive]
1864pub enum ValidateError {
1865 #[error("{kind} requires `{field}`")]
1866 Missing {
1867 kind: AgentKind,
1868 field: &'static str,
1869 },
1870 #[error("`{field}` is invalid on {kind}")]
1871 Forbidden {
1872 kind: AgentKind,
1873 field: &'static str,
1874 },
1875 #[error("`{field}` is {size}B, exceeds cap {cap}B")]
1876 TooLarge {
1877 field: &'static str,
1878 size: usize,
1879 cap: usize,
1880 },
1881 #[error("`{field}`: {reason}")]
1882 Invalid { field: &'static str, reason: String },
1883}
1884
1885pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
1910 use AgentKind::*;
1911 let kind = envelope.kind;
1912
1913 let require = |present: bool, field: &'static str| {
1914 if present {
1915 Ok(())
1916 } else {
1917 Err(ValidateError::Missing { kind, field })
1918 }
1919 };
1920 let forbid = |absent: bool, field: &'static str| {
1921 if absent {
1922 Ok(())
1923 } else {
1924 Err(ValidateError::Forbidden { kind, field })
1925 }
1926 };
1927
1928 if kind != Chunk {
1930 require(envelope.record.is_some(), "record")?;
1931 }
1932
1933 if envelope.root.is_some() && envelope.parent.is_none() {
1934 return Err(ValidateError::Invalid {
1935 field: "root",
1936 reason: "root requires parent".to_owned(),
1937 });
1938 }
1939 if envelope.parent == Some(envelope.conversation)
1940 || envelope.root == Some(envelope.conversation)
1941 {
1942 return Err(ValidateError::Invalid {
1943 field: "parent",
1944 reason: "parent and root must differ from conversation".to_owned(),
1945 });
1946 }
1947
1948 match kind {
1950 Command | Response | Chunk | Error => {
1951 require(envelope.correlation.is_some(), "correlation")?
1952 }
1953 Status => {
1954 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1955 require(envelope.correlation.is_some(), "correlation")?;
1956 }
1957 }
1958 Event => {}
1959 }
1960
1961 match kind {
1964 Chunk => {
1965 require(envelope.channel.is_some(), "channel")?;
1966 require(envelope.sequence.is_some(), "sequence")?;
1967 }
1968 Error => {
1969 if envelope.sequence.is_some() && envelope.channel.is_none() {
1970 return Err(ValidateError::Invalid {
1971 field: "sequence",
1972 reason: "sequence requires channel".to_owned(),
1973 });
1974 }
1975 }
1976 _ => {
1977 forbid(envelope.channel.is_none(), "channel")?;
1978 forbid(envelope.sequence.is_none(), "sequence")?;
1979 }
1980 }
1981
1982 if envelope.last && !matches!(kind, Chunk | Status) {
1985 return Err(ValidateError::Forbidden {
1986 kind,
1987 field: "last",
1988 });
1989 }
1990
1991 match kind {
1993 Response => {}
1994 Chunk => {
1995 if envelope.finish_reason.is_some() && !envelope.last {
1996 return Err(ValidateError::Invalid {
1997 field: "finish_reason",
1998 reason: "finish_reason rides only the terminal chunk".to_owned(),
1999 });
2000 }
2001 }
2002 _ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
2003 }
2004
2005 if matches!(kind, Chunk | Status | Error) {
2008 forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
2009 }
2010
2011 if matches!(kind, Response | Event | Status | Error) {
2014 forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
2015 }
2016 if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
2017 return Err(ValidateError::Invalid {
2018 field: "deadline_micros",
2019 reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
2020 });
2021 }
2022
2023 match kind {
2025 Status => {
2026 if matches!(
2027 envelope.operation.as_deref(),
2028 Some(OPERATION_TASK | OPERATION_SESSION)
2029 ) {
2030 require(envelope.task_state.is_some(), "task_state")?;
2031 }
2032 }
2033 Response | Error => {}
2034 _ => forbid(envelope.task_state.is_none(), "task_state")?,
2035 }
2036
2037 match kind {
2043 Status => {
2044 require(envelope.operation.is_some(), "operation")?;
2045 if let Some(operation) = envelope.operation.as_deref()
2046 && !matches!(
2047 operation,
2048 OPERATION_TASK
2049 | OPERATION_SESSION
2050 | OPERATION_CARD
2051 | OPERATION_PROGRESS
2052 | OPERATION_QUARANTINE
2053 | OPERATION_UNQUARANTINE
2054 )
2055 {
2056 return Err(ValidateError::Invalid {
2057 field: "operation",
2058 reason: format!(
2059 "status operation must be `{OPERATION_TASK}`, `{OPERATION_SESSION}`, `{OPERATION_CARD}`, \
2060 `{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
2061 `{OPERATION_UNQUARANTINE}`, got `{operation}`"
2062 ),
2063 });
2064 }
2065 }
2066 Chunk => {
2067 if envelope.sequence == Some(0) {
2068 require(envelope.operation.is_some(), "operation")?;
2069 }
2070 if let Some(operation) = envelope.operation.as_deref() {
2071 if envelope.sequence != Some(0) {
2072 return Err(ValidateError::Invalid {
2073 field: "operation",
2074 reason: "the stream purpose rides the opening chunk (sequence 0)"
2075 .to_owned(),
2076 });
2077 }
2078 if !matches!(
2079 operation,
2080 OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
2081 ) {
2082 return Err(ValidateError::Invalid {
2083 field: "operation",
2084 reason: format!(
2085 "chunk-stream purpose must be `{OPERATION_CHAT}`, \
2086 `{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
2087 got `{operation}`"
2088 ),
2089 });
2090 }
2091 }
2092 }
2093 Command | Response | Event | Error => {}
2094 }
2095
2096 if kind == Status {
2098 forbid(envelope.tool.is_none(), "tool")?;
2099 }
2100
2101 match kind {
2103 Command => forbid(envelope.usage.is_none(), "usage")?,
2104 Chunk if envelope.usage.is_some() && !envelope.last => {
2105 return Err(ValidateError::Invalid {
2106 field: "usage",
2107 reason: "whole-stream accounting rides the terminal chunk".to_owned(),
2108 });
2109 }
2110 _ => {}
2111 }
2112
2113 match kind {
2115 Status if envelope.operation.as_deref() == Some(OPERATION_SESSION) => {
2116 require(!envelope.body.is_empty(), "body")?;
2117 let state = envelope
2118 .task_state
2119 .expect("session state was required above");
2120 if envelope.last != state.is_terminal() {
2121 return Err(ValidateError::Invalid {
2122 field: "last",
2123 reason: "session last must match terminal task state".to_owned(),
2124 });
2125 }
2126 #[cfg(feature = "cbor")]
2128 {
2129 let invalid_body = |reason: String| ValidateError::Invalid {
2130 field: "body",
2131 reason,
2132 };
2133 let validate_start = |start: &SessionStart| {
2134 crate::validate::Validate::validate(start)
2135 .map_err(|error| invalid_body(error.to_string()))?;
2136 if start.parent != envelope.parent || start.root != envelope.root {
2137 return Err(invalid_body(
2138 "session ancestry must match envelope parent and root".to_owned(),
2139 ));
2140 }
2141 Ok(())
2142 };
2143 match state {
2144 TaskState::Submitted => {
2145 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2146 .map_err(|error| invalid_body(error.to_string()))?;
2147 validate_start(&start)?;
2148 }
2149 TaskState::Working => {
2150 let body: ciborium::value::Value =
2151 crate::framing::decode_named(&envelope.body)
2152 .map_err(|error| invalid_body(error.to_string()))?;
2153 let is_start = matches!(
2154 body,
2155 ciborium::value::Value::Map(ref entries)
2156 if entries.iter().any(|(key, _)| {
2157 matches!(key, ciborium::value::Value::Text(name) if name == "agent" || name == "sdk")
2158 })
2159 );
2160 if is_start {
2161 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2162 .map_err(|error| invalid_body(error.to_string()))?;
2163 validate_start(&start)?;
2164 } else {
2165 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2166 .map_err(|error| invalid_body(error.to_string()))?;
2167 }
2168 }
2169 state if state.is_terminal() => {
2170 crate::framing::decode_named::<SessionEnd>(&envelope.body)
2171 .map_err(|error| invalid_body(error.to_string()))?;
2172 }
2173 _ => {
2174 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2175 .map_err(|error| invalid_body(error.to_string()))?;
2176 }
2177 }
2178 }
2179 }
2180 Status => {}
2181 Chunk => {
2182 if envelope.body.is_empty() && !envelope.last {
2183 return Err(ValidateError::Missing {
2184 kind,
2185 field: "body",
2186 });
2187 }
2188 }
2189 _ => require(!envelope.body.is_empty(), "body")?,
2190 }
2191
2192 cap_str(envelope.operation.as_deref(), "operation")?;
2196 cap_str(envelope.tool.as_deref(), "tool")?;
2197 cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
2198 if let Some(metadata) = &envelope.metadata {
2199 if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
2200 return Err(ValidateError::TooLarge {
2201 field: "metadata",
2202 size: metadata.len(),
2203 cap: crate::limits::MAX_METADATA_ENTRIES,
2204 });
2205 }
2206 let mut total = 0usize;
2207 for (key, value) in metadata {
2208 if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
2209 return Err(ValidateError::TooLarge {
2210 field: "metadata key",
2211 size: key.len(),
2212 cap: crate::limits::MAX_METADATA_KEY_BYTES,
2213 });
2214 }
2215 let value_size = value_size(value);
2216 if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
2217 return Err(ValidateError::TooLarge {
2218 field: "metadata value",
2219 size: value_size,
2220 cap: crate::limits::MAX_METADATA_VALUE_BYTES,
2221 });
2222 }
2223 total += key.len() + value_size;
2224 }
2225 if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
2226 return Err(ValidateError::TooLarge {
2227 field: "metadata",
2228 size: total,
2229 cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
2230 });
2231 }
2232 }
2233 if let Some(signature) = &envelope.signature {
2236 signature.validate()?;
2237 }
2238 Ok(())
2239}
2240
2241pub const OPERATION_TASK: &str = "task";
2243pub const OPERATION_SESSION: &str = "session";
2245pub const OPERATION_CARD: &str = "card";
2247pub const OPERATION_PROGRESS: &str = "progress";
2249pub const OPERATION_QUARANTINE: &str = "quarantine";
2254pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
2259
2260pub const OPERATION_CHAT: &str = "chat";
2267pub const OPERATION_REASONING: &str = "reasoning";
2269pub const OPERATION_TOOL_ARGS: &str = "tool_args";
2271
2272pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
2278pub const OPERATION_STATE_DELTA: &str = "state_delta";
2281pub const OPERATION_SESSION_PARKED: &str = "session_parked";
2284pub const OPERATION_SESSION_UNPARKED: &str = "session_unparked";
2287
2288pub const METADATA_ROLE: &str = "role";
2295pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
2301pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
2305pub const METADATA_PURPOSE: &str = "purpose";
2308pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
2311pub const METADATA_TASK_CONTEXT: &str = "task_context";
2314pub const METADATA_SESSION_INTENT: &str = "session_intent";
2317
2318const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2322
2323pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
2324 let mut out = [0u8; 26];
2325 let mut v = value;
2326 for slot in out.iter_mut().rev() {
2327 *slot = CROCKFORD[(v & 0x1f) as usize];
2328 v >>= 5;
2329 }
2330 out
2331}
2332
2333pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
2334 let bytes = s.as_bytes();
2335 if bytes.len() != 26 {
2336 return Err(IdParseError::Length { got: bytes.len() });
2337 }
2338 let mut value: u128 = 0;
2339 for (i, byte) in bytes.iter().enumerate() {
2340 let digit = CROCKFORD
2341 .iter()
2342 .position(|c| *c == byte.to_ascii_uppercase())
2343 .ok_or(IdParseError::Char(*byte as char))?;
2344 if i == 0 && digit > 7 {
2347 return Err(IdParseError::Overflow);
2348 }
2349 value = (value << 5) | digit as u128;
2350 }
2351 Ok(value)
2352}
2353
2354fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
2355 if let Some(value) = value
2356 && value.len() > crate::limits::MAX_AGENT_STRING_BYTES
2357 {
2358 return Err(ValidateError::TooLarge {
2359 field,
2360 size: value.len(),
2361 cap: crate::limits::MAX_AGENT_STRING_BYTES,
2362 });
2363 }
2364 Ok(())
2365}
2366
2367fn value_size(value: &Value) -> usize {
2370 match value {
2371 Value::Str(s) => s.len(),
2372 Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
2373 _ => 9,
2374 }
2375}
2376
2377#[cfg(test)]
2378mod tests {
2379 use super::*;
2380 use crate::validate::Validate;
2381
2382 fn session_start() -> SessionStart {
2383 SessionStart {
2384 label: Some("Research".to_owned()),
2385 namespace: Some("agents".to_owned()),
2386 agent: "planner".parse().expect("valid agent id"),
2387 sdk: SdkInfo {
2388 language: "rust".to_owned(),
2389 version: "0.8.0".to_owned(),
2390 },
2391 parent: None,
2392 root: None,
2393 idle_timeout_micros: Some(300_000_000),
2394 budget: Some(Budget {
2395 tokens: Some(1000),
2396 cost_micros: None,
2397 }),
2398 tags: vec!["demo".to_owned()],
2399 }
2400 }
2401
2402 #[test]
2403 fn given_session_status_when_decoded_then_should_keep_known_and_unknown_values() {
2404 assert_eq!(
2405 serde_json::from_str::<SessionStatus>("\"paused\"").expect("paused"),
2406 SessionStatus::Paused
2407 );
2408 assert_eq!(
2409 serde_json::from_str::<SessionStatus>("\"future\"").expect("unknown"),
2410 SessionStatus::Unrecognized
2411 );
2412 }
2413
2414 #[test]
2415 fn given_session_start_when_validated_then_should_enforce_label_and_tag_caps() {
2416 let mut start = session_start();
2417 start.validate().expect("valid start");
2418 start.label = Some("bad\nlabel".to_owned());
2419 assert!(start.validate().is_err());
2420 start.label = Some("x".repeat(crate::limits::MAX_SESSION_LABEL_BYTES + 1));
2421 assert!(start.validate().is_err());
2422 start.label = None;
2423 start.tags = vec!["tag".to_owned(); crate::limits::MAX_MEMORY_TAGS + 1];
2424 assert!(start.validate().is_err());
2425 }
2426
2427 #[cfg(feature = "cbor")]
2428 #[test]
2429 fn given_session_status_bodies_when_validated_then_should_match_the_lifecycle() {
2430 let mut status = AgentEnvelope::status(
2431 RecordId::from_u128(1),
2432 ConversationId::from_u128(2),
2433 "planner".parse().expect("valid agent id"),
2434 OPERATION_SESSION,
2435 );
2436 status.task_state = Some(TaskState::Submitted);
2437 status.body = crate::framing::encode_named(&session_start()).expect("start encodes");
2438 validate(&status).expect("submitted start");
2439 status.parent = Some(ConversationId::from_u128(8));
2440 assert!(
2441 validate(&status).is_err(),
2442 "start and envelope ancestry must match"
2443 );
2444 status.parent = None;
2445
2446 status.task_state = Some(TaskState::Paused);
2447 status.body = crate::framing::encode_named(&SessionTransition::default())
2448 .expect("transition encodes");
2449 validate(&status).expect("paused transition");
2450
2451 status.task_state = Some(TaskState::Completed);
2452 status.body = crate::framing::encode_named(&SessionEnd::default()).expect("end encodes");
2453 assert!(validate(&status).is_err(), "terminal status needs last");
2454 status.last = true;
2455 validate(&status).expect("completed end");
2456 }
2457
2458 #[cfg(feature = "cbor")]
2459 #[test]
2460 fn given_a_parked_record_when_encoded_then_should_round_trip_and_require_a_generation() {
2461 use crate::validate::Validate;
2462 let source = |generation| crate::graph::SourceRef::Message {
2463 stream: 1,
2464 topic: 2,
2465 partition: 3,
2466 offset: 4,
2467 generation,
2468 conversation: Some(ConversationId::from_u128(5).to_string()),
2469 };
2470 let parking = SessionParking {
2471 source: source(Some(6)),
2472 role: "worker".parse().expect("valid agent id"),
2473 request: LogPosition::new(1, 7, 0, 8),
2474 };
2475 parking.validate().expect("a complete parking is valid");
2476 let bytes = crate::framing::encode_named(&parking).expect("parking encodes");
2477 let decoded: SessionParking = crate::framing::decode_named(&bytes).expect("decodes");
2478 assert_eq!(decoded, parking);
2479 let unproven = SessionParking {
2480 source: source(None),
2481 ..parking.clone()
2482 };
2483 assert!(unproven.validate().is_err());
2484 let not_a_record = SessionParking {
2485 source: crate::graph::SourceRef::Memory { id: "m".to_owned() },
2486 ..parking
2487 };
2488 assert!(not_a_record.validate().is_err());
2489 }
2490
2491 #[test]
2492 fn given_a_pause_request_body_when_decoded_then_should_read_an_empty_body_as_no_participants() {
2493 let empty: SessionPauseRequest = serde_json::from_slice(b"{}").expect("empty decodes");
2494 assert!(empty.participants.is_empty());
2495 let named = SessionPauseRequest {
2496 participants: vec!["worker".parse().expect("valid agent id")],
2497 };
2498 let json = serde_json::to_string(&named).expect("request encodes");
2499 assert_eq!(json, r#"{"participants":["worker"]}"#);
2500 assert_eq!(
2501 serde_json::from_str::<SessionPauseRequest>(&json).expect("decodes"),
2502 named
2503 );
2504 }
2505
2506 #[test]
2507 fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
2508 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2509 let text = id.to_string();
2510 assert_eq!(text.len(), 26);
2511 assert_eq!(text.parse::<RecordId>().expect("parses"), id);
2512 assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
2514 assert_eq!(
2515 RecordId::from_u128(0).to_string(),
2516 "00000000000000000000000000"
2517 );
2518 assert_eq!(
2519 RecordId::from_u128(u128::MAX).to_string(),
2520 "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
2521 );
2522 }
2523
2524 #[test]
2525 fn given_an_id_in_json_when_encoded_then_should_use_readable_text() {
2526 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2527 let json = serde_json::to_string(&id).expect("serializes");
2528 assert_eq!(json, format!("\"{id}\""));
2529 assert_eq!(
2530 serde_json::from_str::<RecordId>(&json).expect("deserializes"),
2531 id
2532 );
2533 let byte_array = serde_json::to_string(&[0_u8; 16]).expect("serializes byte array");
2534 assert!(serde_json::from_str::<RecordId>(&byte_array).is_err());
2535 }
2536
2537 #[cfg(feature = "cbor")]
2538 #[test]
2539 fn given_an_id_in_named_cbor_when_encoded_then_should_use_fixed_width_bytes() {
2540 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2541 let encoded = crate::framing::encode_named(&id).expect("serializes");
2542 let decoded = crate::framing::decode_named::<RecordId>(&encoded).expect("deserializes");
2543 assert_eq!(decoded, id);
2544 }
2545
2546 #[test]
2547 fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
2548 assert_eq!(
2549 "short".parse::<RecordId>(),
2550 Err(IdParseError::Length { got: 5 })
2551 );
2552 assert_eq!(
2553 "8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
2554 Err(IdParseError::Overflow)
2555 );
2556 assert!(matches!(
2557 "UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
2558 Err(IdParseError::Char('U'))
2559 ));
2560 }
2561
2562 #[test]
2563 fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
2564 for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
2565 assert_eq!(
2566 s.parse::<AgentId>()
2567 .expect("a printable agent id is valid")
2568 .as_str(),
2569 s
2570 );
2571 }
2572 assert!("".parse::<AgentId>().is_err());
2573 assert!("bad\nid".parse::<AgentId>().is_err());
2574 }
2575
2576 #[test]
2577 fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
2578 let expected = [
2579 (TaskState::Submitted, 1u8),
2580 (TaskState::Working, 2),
2581 (TaskState::InputRequired, 3),
2582 (TaskState::Completed, 4),
2583 (TaskState::Canceled, 5),
2584 (TaskState::Failed, 6),
2585 (TaskState::Rejected, 7),
2586 (TaskState::AuthRequired, 8),
2587 (TaskState::Unknown, 9),
2588 (TaskState::Paused, 10),
2589 ];
2590 for (state, code) in expected {
2591 assert_eq!(state.code(), code);
2592 assert_eq!(TaskState::from_code(code), state);
2593 }
2594 let future = TaskState::from_code(42);
2596 assert_eq!(future, TaskState::Unrecognized(42));
2597 assert_eq!(future.code(), 42);
2598 assert!(!future.is_terminal());
2599 assert!(TaskState::Completed.is_terminal());
2600 assert!(!TaskState::Working.is_terminal());
2601 assert!(!TaskState::Paused.is_terminal());
2602 assert_eq!(TaskState::Paused.to_string(), "paused");
2603 assert_eq!("paused".parse::<TaskState>(), Ok(TaskState::Paused));
2604 }
2605
2606 #[test]
2607 fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
2608 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2609 assert_eq!(
2610 "auth-required".parse::<TaskState>().expect("parses"),
2611 TaskState::AuthRequired
2612 );
2613 assert!("nope".parse::<TaskState>().is_err());
2614 }
2615
2616 #[test]
2617 fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
2618 assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
2619 assert_eq!(AgentErrorCode::Internal.code(), 7);
2620 assert_eq!(
2621 AgentErrorCode::from_code(99),
2622 AgentErrorCode::Unrecognized(99)
2623 );
2624 assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
2625 assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
2626 assert_eq!(
2627 DeadLetterReason::from_code(77),
2628 DeadLetterReason::Unrecognized(77)
2629 );
2630 }
2631
2632 #[test]
2633 fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
2634 for code in 0u8..=u8::MAX {
2638 assert_eq!(TaskState::from_code(code).code(), code);
2639 assert_eq!(AgentErrorCode::from_code(code).code(), code);
2640 assert_eq!(DeadLetterReason::from_code(code).code(), code);
2641 }
2642 }
2643
2644 #[test]
2645 fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
2646 assert!("job-123-attempt-2".parse::<IdempotencyKey>().is_ok());
2647 assert!("".parse::<IdempotencyKey>().is_err());
2648 assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
2649 }
2650
2651 #[test]
2652 fn given_an_idempotency_key_with_a_control_character_when_parsed_then_should_refuse_it() {
2653 assert!("job-123\nattempt-2".parse::<IdempotencyKey>().is_err());
2654 assert!("job-123\u{7f}".parse::<IdempotencyKey>().is_err());
2655 assert!(
2656 serde_json::from_str::<IdempotencyKey>("\"job\\u0000\"").is_err(),
2657 "a decoded key is checked too"
2658 );
2659 }
2660
2661 #[test]
2662 fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
2663 let (record, conversation, source, correlation) = ids();
2664 let command =
2665 AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
2666 validate(&command).expect("a well-formed command validates");
2667
2668 let mut missing = command.clone();
2670 missing.correlation = None;
2671 assert_eq!(
2672 validate(&missing),
2673 Err(ValidateError::Missing {
2674 kind: AgentKind::Command,
2675 field: "correlation"
2676 })
2677 );
2678
2679 let with_usage = command.clone().with_usage(TokenUsage::default());
2681 assert!(matches!(
2682 validate(&with_usage),
2683 Err(ValidateError::Forbidden { field: "usage", .. })
2684 ));
2685
2686 let mut with_channel = command;
2688 with_channel.channel = Some(ChannelId::from_u128(1));
2689 assert!(matches!(
2690 validate(&with_channel),
2691 Err(ValidateError::Forbidden {
2692 field: "channel",
2693 ..
2694 })
2695 ));
2696 }
2697
2698 #[test]
2699 fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
2700 let (_, conversation, source, correlation) = ids();
2701 let channel = ChannelId::from_u128(23);
2702 let chunk = AgentEnvelope::chunk(
2703 conversation,
2704 source.clone(),
2705 correlation,
2706 channel,
2707 0,
2708 b"tok".to_vec(),
2709 )
2710 .with_operation(OPERATION_CHAT);
2711 validate(&chunk).expect("a stream chunk validates");
2712
2713 let mut undeclared = chunk.clone();
2715 undeclared.operation = None;
2716 assert!(matches!(
2717 validate(&undeclared),
2718 Err(ValidateError::Missing {
2719 field: "operation",
2720 ..
2721 })
2722 ));
2723 let mut off_vocabulary = chunk.clone();
2724 off_vocabulary.operation = Some("telemetry".to_owned());
2725 assert!(matches!(
2726 validate(&off_vocabulary),
2727 Err(ValidateError::Invalid {
2728 field: "operation",
2729 ..
2730 })
2731 ));
2732
2733 let redeclared = AgentEnvelope::chunk(
2735 conversation,
2736 source.clone(),
2737 correlation,
2738 channel,
2739 3,
2740 b"tok".to_vec(),
2741 )
2742 .with_operation(OPERATION_REASONING);
2743 assert!(matches!(
2744 validate(&redeclared),
2745 Err(ValidateError::Invalid {
2746 field: "operation",
2747 ..
2748 })
2749 ));
2750
2751 let terminal = AgentEnvelope::chunk(
2753 conversation,
2754 source.clone(),
2755 correlation,
2756 channel,
2757 41,
2758 Vec::new(),
2759 )
2760 .terminal("stop")
2761 .with_usage(TokenUsage {
2762 input_tokens: 100,
2763 output_tokens: 42,
2764 ..Default::default()
2765 });
2766 validate(&terminal).expect("a terminal chunk validates");
2767
2768 let mut early_finish = chunk.clone();
2770 early_finish.finish_reason = Some("stop".to_owned());
2771 assert!(matches!(
2772 validate(&early_finish),
2773 Err(ValidateError::Invalid {
2774 field: "finish_reason",
2775 ..
2776 })
2777 ));
2778
2779 let empty = AgentEnvelope::chunk(
2781 conversation,
2782 source.clone(),
2783 correlation,
2784 channel,
2785 1,
2786 Vec::new(),
2787 );
2788 assert!(matches!(
2789 validate(&empty),
2790 Err(ValidateError::Missing { field: "body", .. })
2791 ));
2792
2793 let mut keyed = chunk.clone();
2795 keyed.idempotency_key = Some("k".parse().expect("valid key"));
2796 assert!(matches!(
2797 validate(&keyed),
2798 Err(ValidateError::Forbidden {
2799 field: "idempotency_key",
2800 ..
2801 })
2802 ));
2803
2804 let opening = chunk.with_deadline_micros(1);
2806 validate(&opening).expect("an opening chunk may declare the bound");
2807 let late = AgentEnvelope::chunk(
2808 conversation,
2809 source.clone(),
2810 correlation,
2811 channel,
2812 5,
2813 b"tok".to_vec(),
2814 )
2815 .with_deadline_micros(1);
2816 assert!(matches!(
2817 validate(&late),
2818 Err(ValidateError::Invalid {
2819 field: "deadline_micros",
2820 ..
2821 })
2822 ));
2823 }
2824
2825 #[test]
2826 fn given_status_signals_when_validated_then_task_updates_should_require_state() {
2827 let (record, conversation, source, correlation) = ids();
2828 let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
2830 validate(&card).expect("a card validates");
2831
2832 let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
2834 assert!(matches!(
2835 validate(&bare_task),
2836 Err(ValidateError::Missing {
2837 field: "correlation",
2838 ..
2839 })
2840 ));
2841 let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
2842 .with_correlation(correlation)
2843 .with_task_state(TaskState::Working);
2844 validate(&task).expect("a task update validates");
2845
2846 let off_vocabulary =
2848 AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
2849 assert!(matches!(
2850 validate(&off_vocabulary),
2851 Err(ValidateError::Invalid {
2852 field: "operation",
2853 ..
2854 })
2855 ));
2856 }
2857
2858 #[test]
2859 fn given_an_error_when_validated_then_last_should_be_forbidden() {
2860 let (record, conversation, source, correlation) = ids();
2861 let error =
2862 AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
2863 validate(&error).expect("an error validates");
2864
2865 let mut flagged = error.clone();
2867 flagged.last = true;
2868 assert!(matches!(
2869 validate(&flagged),
2870 Err(ValidateError::Forbidden { field: "last", .. })
2871 ));
2872
2873 let mut dangling = error;
2875 dangling.sequence = Some(3);
2876 assert!(matches!(
2877 validate(&dangling),
2878 Err(ValidateError::Invalid {
2879 field: "sequence",
2880 ..
2881 })
2882 ));
2883 }
2884
2885 fn descriptor(skill_id: &str) -> CapabilityDescriptor {
2886 CapabilityDescriptor {
2887 skill_id: skill_id.to_owned(),
2888 input: None,
2889 output: None,
2890 cost_class: None,
2891 latency_class: None,
2892 max_concurrency: None,
2893 health: None,
2894 load: None,
2895 }
2896 }
2897
2898 #[test]
2899 fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
2900 let card = AgentCard {
2901 name: Some("rollout-planner".to_owned()),
2902 version: Some("1.4.2".to_owned()),
2903 capabilities: vec![
2904 CapabilityDescriptor {
2905 skill_id: "chat".to_owned(),
2906 input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2907 output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2908 cost_class: Some(2),
2909 latency_class: Some(1),
2910 max_concurrency: Some(8),
2911 health: Some(Health::Healthy),
2912 load: Some(250),
2913 },
2914 descriptor("plan_rollout"),
2915 ],
2916 ttl_micros: Some(30_000_000),
2917 };
2918 card.validate().expect("a well-formed card validates");
2919
2920 let mut crowded = card.clone();
2921 crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
2922 assert!(matches!(
2923 crowded.validate(),
2924 Err(ValidateError::TooLarge {
2925 field: "capabilities",
2926 ..
2927 })
2928 ));
2929
2930 let mut oversized = card;
2931 oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
2932 assert!(matches!(
2933 oversized.validate(),
2934 Err(ValidateError::TooLarge { field: "name", .. })
2935 ));
2936 }
2937
2938 #[cfg(feature = "cbor")]
2939 #[test]
2940 fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
2941 {
2942 use crate::framing::{decode_named, encode_named};
2943 let reference = ContentRef::SchemaId("json".to_owned());
2946 let bytes = encode_named(&reference).expect("encodes");
2947 let back: ContentRef = decode_named(&bytes).expect("decodes");
2948 assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
2949 let ct = ContentRef::ContentType(crate::content::ContentType::Json);
2951 let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
2952 assert_eq!(back, ct);
2953 }
2954
2955 #[test]
2956 fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
2957 let valid = Signature {
2958 scheme: SIGNATURE_SCHEME_ED25519,
2959 key_id: vec![1u8; 8],
2960 bytes: vec![2u8; 64],
2961 context: None,
2962 };
2963 valid
2964 .validate()
2965 .expect("a well-formed Ed25519 signature validates");
2966
2967 let mut short_key = valid.clone();
2968 short_key.key_id = vec![1u8; 4];
2969 assert!(matches!(
2970 short_key.validate(),
2971 Err(ValidateError::Invalid {
2972 field: "key_id",
2973 ..
2974 })
2975 ));
2976
2977 let mut short_signature = valid.clone();
2978 short_signature.bytes = vec![2u8; 32];
2979 assert!(matches!(
2980 short_signature.validate(),
2981 Err(ValidateError::Invalid { field: "bytes", .. })
2982 ));
2983
2984 let future = Signature {
2987 scheme: 42,
2988 key_id: vec![1u8; 3],
2989 bytes: vec![2u8; 99],
2990 context: None,
2991 };
2992 future.validate().expect("an unknown scheme flows through");
2993 }
2994
2995 #[test]
2996 fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
2997 let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
2998 valid.validate().expect("a well-formed reference validates");
2999
3000 let mut empty = valid.clone();
3001 empty.reference = String::new();
3002 assert!(matches!(
3003 empty.validate(),
3004 Err(ValidateError::Invalid {
3005 field: "reference",
3006 ..
3007 })
3008 ));
3009
3010 let mut oversized = valid.clone();
3011 oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
3012 assert!(matches!(
3013 oversized.validate(),
3014 Err(ValidateError::TooLarge {
3015 field: "reference",
3016 ..
3017 })
3018 ));
3019
3020 let mut truncated = valid;
3021 truncated.sha256 = vec![7u8; 16];
3022 assert!(matches!(
3023 truncated.validate(),
3024 Err(ValidateError::Invalid {
3025 field: "sha256",
3026 ..
3027 })
3028 ));
3029 }
3030
3031 #[test]
3032 fn given_oversized_metadata_when_validated_then_should_reject() {
3033 let (record, conversation, source, correlation) = ids();
3034 let mut command =
3035 AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
3036 for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
3037 command = command.with_metadata(format!("k{i}"), i as i64);
3038 }
3039 assert!(matches!(
3040 validate(&command),
3041 Err(ValidateError::TooLarge {
3042 field: "metadata",
3043 ..
3044 })
3045 ));
3046 }
3047
3048 fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
3049 (
3050 RecordId::from_u128(7),
3051 ConversationId::from_u128(11),
3052 "test-agent".parse().expect("valid agent id"),
3053 CorrelationId::from_u128(17),
3054 )
3055 }
3056}
3057
3058#[cfg(all(test, feature = "cbor"))]
3059mod wire_tests {
3060 use super::*;
3061 use crate::framing::{decode_named, encode_named};
3062
3063 #[test]
3064 fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
3065 let mut valid = vec![0x40 | 20];
3068 valid.extend_from_slice(&[0u8; 20]);
3069 decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
3070
3071 for bad_len in [0u8, 19, 21, 23] {
3072 let mut bytes = vec![0x40 | bad_len];
3073 bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
3074 assert!(
3075 decode_named::<LogPosition>(&bytes).is_err(),
3076 "a {bad_len}-byte locator must error, not panic"
3077 );
3078 }
3079 }
3080
3081 #[test]
3082 fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
3083 let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
3084 let bytes = encode_named(&pos).expect("encodes");
3085 let back: LogPosition = decode_named(&bytes).expect("decodes");
3086 assert_eq!(back, pos);
3087 }
3088
3089 #[test]
3090 fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
3091 let envelope = AgentEnvelope::command(
3092 RecordId::from_u128(1),
3093 ConversationId::from_u128(2),
3094 "source-agent".parse().expect("valid agent id"),
3095 CorrelationId::from_u128(4),
3096 b"payload".to_vec(),
3097 )
3098 .with_target("target-agent".parse().expect("valid agent id"))
3099 .with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
3100 .with_idempotency_key("job-1".parse().expect("valid key"))
3101 .with_deadline_micros(1_700_000_000_000_000)
3102 .with_operation("chat")
3103 .with_tool("search")
3104 .with_metadata("pool", "eu-west");
3105 let bytes = encode_named(&envelope).expect("encodes");
3106 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3107 assert_eq!(back, envelope);
3108 }
3109
3110 #[test]
3111 fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
3112 let envelope = AgentEnvelope::event(
3116 RecordId::from_u128(1),
3117 ConversationId::from_u128(2),
3118 "source-agent".parse().expect("valid agent id"),
3119 b"e".to_vec(),
3120 )
3121 .requiring(0b101);
3122 let bytes = encode_named(&envelope).expect("encodes");
3123 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3124 assert_eq!(back.must_understand, 0b101);
3125 assert_eq!(back.unmet_requirements(0b001), 0b100);
3127 assert_eq!(back.unmet_requirements(0b111), 0);
3129 let plain = AgentEnvelope::event(
3131 RecordId::from_u128(1),
3132 ConversationId::from_u128(2),
3133 "source-agent".parse().expect("valid agent id"),
3134 b"e".to_vec(),
3135 );
3136 assert_eq!(plain.unmet_requirements(features::NONE), 0);
3137 let json = serde_json::to_string(&plain).expect("json");
3138 assert!(
3139 !json.contains("must_understand"),
3140 "zero marker must be omitted: {json}"
3141 );
3142 }
3143
3144 #[test]
3145 fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
3146 let envelope = AgentEnvelope::event(
3149 RecordId::from_u128(1),
3150 ConversationId::from_u128(2),
3151 "source-agent".parse().expect("valid agent id"),
3152 b"e".to_vec(),
3153 );
3154 let bytes = encode_named(&envelope).expect("encodes");
3155 assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
3158 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3159 assert!(!back.last);
3160 assert!(back.metadata.is_none());
3161 }
3162
3163 #[test]
3164 fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
3165 let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
3166 assert_eq!(bytes.len(), 17);
3169 assert_eq!(bytes[0], 0x50);
3170 assert_eq!(bytes[16], 0x02);
3171 assert_eq!(bytes[15], 0x01);
3172 }
3173
3174 #[test]
3175 fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
3176 let bytes = encode_named(&TaskState::Completed).expect("encodes");
3177 assert_eq!(bytes, vec![4]);
3178 let unknown = encode_named(&42u8).expect("encodes a raw code");
3179 let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
3180 assert_eq!(back, TaskState::Unrecognized(42));
3181 }
3182
3183 #[test]
3184 fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
3185 let body = AgentErrorBody {
3186 code: AgentErrorCode::ToolFailure,
3187 message: Some("search timed out".to_owned()),
3188 retryable: true,
3189 detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
3190 };
3191 let bytes = encode_named(&body).expect("encodes");
3192 let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
3193 assert_eq!(back, body);
3194 }
3195
3196 #[test]
3197 fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
3198 let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
3199 let bytes = encode_named(&capsule).expect("encodes");
3200 let back: BodyRef = decode_named(&bytes).expect("decodes");
3201 assert_eq!(back, capsule);
3202 assert!(back.encryption.is_none(), "absent encryption costs nothing");
3203 back.validate().expect("decoded capsule validates");
3204 }
3205
3206 #[test]
3207 fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
3208 let inner = AgentEnvelope::command(
3209 RecordId::from_u128(9),
3210 ConversationId::from_u128(8),
3211 "source-agent".parse().expect("valid agent id"),
3212 CorrelationId::from_u128(6),
3213 b"poison".to_vec(),
3214 );
3215 let payload = encode_named(&inner).expect("inner encodes");
3216 let capsule = AgentDeadLetter {
3217 source: LogPosition::new(1, 2, 3, 99),
3218 reason: DeadLetterReason::RetryExhausted,
3219 attempts: 5,
3220 detail: Some("handler kept failing".to_owned()),
3221 payload: payload.clone(),
3222 };
3223 let bytes = encode_named(&capsule).expect("encodes");
3224 let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
3225 assert_eq!(back.payload, payload, "redrive needs the original bytes");
3226 let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
3227 assert_eq!(redrive, inner);
3228 }
3229 #[test]
3230 fn given_a_task_status_when_validated_then_should_require_the_correlation() {
3231 let base = AgentEnvelope::status(
3233 RecordId::from_u128(1),
3234 ConversationId::from_u128(2),
3235 "runner".parse().expect("valid agent id"),
3236 OPERATION_TASK,
3237 )
3238 .with_task_state(TaskState::Working);
3239 assert!(
3240 matches!(
3241 validate(&base),
3242 Err(ValidateError::Missing {
3243 kind: AgentKind::Status,
3244 field: "correlation"
3245 })
3246 ),
3247 "a task status without a correlation is rejected"
3248 );
3249 let correlated = base.with_correlation(CorrelationId::from_u128(2));
3250 validate(&correlated)
3251 .expect("the run-lifecycle status validates once it correlates on the run");
3252 }
3253
3254 #[test]
3255 fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
3256 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
3257 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
3258 assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
3259 assert_eq!(
3260 "input-required".parse::<TaskState>().expect("parses"),
3261 TaskState::InputRequired
3262 );
3263 assert!("bogus".parse::<TaskState>().is_err());
3264 }
3265}