1use crate::error::InvalidError;
19use crate::query::Value;
20use serde::de::{self, Visitor};
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use std::collections::BTreeMap;
23use std::fmt;
24use std::str::FromStr;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28#[non_exhaustive]
29pub enum IdParseError {
30 #[error("id must be 26 characters, got {got}")]
31 Length { got: usize },
32 #[error("id contains invalid character `{0}`")]
33 Char(char),
34 #[error("id overflows 128 bits")]
35 Overflow,
36}
37
38macro_rules! wire_id {
43 ($(#[$doc:meta])* $name:ident) => {
44 $(#[$doc])*
45 #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
46 pub struct $name(u128);
47
48 impl $name {
49 pub const fn from_u128(value: u128) -> Self {
51 Self(value)
52 }
53
54 pub const fn as_u128(self) -> u128 {
56 self.0
57 }
58
59 pub const fn to_bytes(self) -> [u8; 16] {
61 self.0.to_be_bytes()
62 }
63
64 pub const fn from_bytes(payload: [u8; 16]) -> Self {
66 Self(u128::from_be_bytes(payload))
67 }
68 }
69
70 impl std::fmt::Display for $name {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 let encoded = $crate::agent::crockford_encode(self.0);
73 f.write_str(std::str::from_utf8(&encoded).expect("crockford output is ASCII"))
75 }
76 }
77
78 impl std::fmt::Debug for $name {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "{}({})", stringify!($name), self)
81 }
82 }
83
84 impl std::str::FromStr for $name {
85 type Err = $crate::agent::IdParseError;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 $crate::agent::crockford_decode(s).map(Self)
89 }
90 }
91
92 impl From<u128> for $name {
93 fn from(value: u128) -> Self {
94 Self(value)
95 }
96 }
97
98 impl From<$name> for u128 {
99 fn from(value: $name) -> u128 {
100 value.0
101 }
102 }
103
104 impl serde::Serialize for $name {
105 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
106 if serializer.is_human_readable() {
107 serializer.serialize_str(&self.to_string())
108 } else {
109 serializer.serialize_bytes(&self.to_bytes())
110 }
111 }
112 }
113
114 impl<'de> serde::Deserialize<'de> for $name {
115 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 if deserializer.is_human_readable() {
117 struct TextVisitor;
118
119 impl<'de> serde::de::Visitor<'de> for TextVisitor {
120 type Value = $name;
121
122 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.write_str("a 26-character Crockford base32 id or 16 id bytes")
124 }
125
126 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
127 value.parse().map_err(E::custom)
128 }
129
130 fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
131 let payload: [u8; 16] = value
132 .try_into()
133 .map_err(|_| E::invalid_length(value.len(), &self))?;
134 Ok($name::from_bytes(payload))
135 }
136
137 fn visit_byte_buf<E: serde::de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
138 self.visit_bytes(&value)
139 }
140 }
141
142 return deserializer.deserialize_any(TextVisitor);
146 }
147
148 struct BytesVisitor;
149
150 impl<'de> serde::de::Visitor<'de> for BytesVisitor {
151 type Value = $name;
152
153 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str("16 big-endian id bytes")
155 }
156
157 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
158 let payload: [u8; 16] = v
159 .try_into()
160 .map_err(|_| E::invalid_length(v.len(), &self))?;
161 Ok($name::from_bytes(payload))
162 }
163
164 fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
165 self.visit_bytes(&v)
166 }
167
168 fn visit_seq<A: serde::de::SeqAccess<'de>>(
169 self,
170 mut sequence: A,
171 ) -> Result<Self::Value, A::Error> {
172 let mut payload = [0_u8; 16];
173 for (index, byte) in payload.iter_mut().enumerate() {
174 *byte = sequence
175 .next_element()?
176 .ok_or_else(|| serde::de::Error::invalid_length(index, &self))?;
177 }
178 if sequence.next_element::<u8>()?.is_some() {
179 return Err(serde::de::Error::invalid_length(17, &self));
180 }
181 Ok($name::from_bytes(payload))
182 }
183 }
184
185 deserializer.deserialize_bytes(BytesVisitor)
186 }
187 }
188 };
189}
190
191pub(crate) use wire_id;
194
195wire_id!(
196 RecordId
203);
204wire_id!(
205 ConversationId
208);
209wire_id!(
210 CorrelationId
213);
214wire_id!(
215 ChannelId
218);
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
232pub struct LogPosition {
233 pub stream_id: u32,
234 pub topic_id: u32,
235 pub partition_id: u32,
236 pub offset: u64,
237}
238
239const LOG_POSITION_BYTES: usize = 20;
240
241impl LogPosition {
242 pub const fn new(stream_id: u32, topic_id: u32, partition_id: u32, offset: u64) -> Self {
244 Self {
245 stream_id,
246 topic_id,
247 partition_id,
248 offset,
249 }
250 }
251
252 pub fn to_bytes(self) -> [u8; LOG_POSITION_BYTES] {
254 let mut out = [0u8; LOG_POSITION_BYTES];
255 out[0..4].copy_from_slice(&self.stream_id.to_be_bytes());
256 out[4..8].copy_from_slice(&self.topic_id.to_be_bytes());
257 out[8..12].copy_from_slice(&self.partition_id.to_be_bytes());
258 out[12..20].copy_from_slice(&self.offset.to_be_bytes());
259 out
260 }
261
262 pub fn from_bytes(payload: [u8; LOG_POSITION_BYTES]) -> Self {
264 let u32_at = |start: usize| {
265 u32::from_be_bytes(payload[start..start + 4].try_into().expect("4-byte slice"))
266 };
267 Self {
268 stream_id: u32_at(0),
269 topic_id: u32_at(4),
270 partition_id: u32_at(8),
271 offset: u64::from_be_bytes(payload[12..20].try_into().expect("8-byte slice")),
272 }
273 }
274}
275
276impl Serialize for LogPosition {
280 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
281 serializer.serialize_bytes(&self.to_bytes())
282 }
283}
284
285impl<'de> Deserialize<'de> for LogPosition {
286 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
287 struct LocatorVisitor;
288
289 impl<'de> Visitor<'de> for LocatorVisitor {
290 type Value = LogPosition;
291
292 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.write_str("20 packed locator bytes")
294 }
295
296 fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
297 let payload: [u8; LOG_POSITION_BYTES] = v
298 .try_into()
299 .map_err(|_| E::invalid_length(v.len(), &self))?;
300 Ok(LogPosition::from_bytes(payload))
301 }
302
303 fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
304 self.visit_bytes(&v)
305 }
306
307 fn visit_seq<A: de::SeqAccess<'de>>(
308 self,
309 mut sequence: A,
310 ) -> Result<Self::Value, A::Error> {
311 let mut payload = [0_u8; LOG_POSITION_BYTES];
312 for (index, byte) in payload.iter_mut().enumerate() {
313 *byte = sequence
314 .next_element()?
315 .ok_or_else(|| de::Error::invalid_length(index, &self))?;
316 }
317 if sequence.next_element::<u8>()?.is_some() {
318 return Err(de::Error::invalid_length(LOG_POSITION_BYTES + 1, &self));
319 }
320 Ok(LogPosition::from_bytes(payload))
321 }
322 }
323
324 deserializer.deserialize_bytes(LocatorVisitor)
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[serde(try_from = "String", into = "String")]
335pub struct IdempotencyKey(String);
336
337impl IdempotencyKey {
338 pub fn as_str(&self) -> &str {
340 &self.0
341 }
342}
343
344impl FromStr for IdempotencyKey {
345 type Err = InvalidError;
346
347 fn from_str(s: &str) -> Result<Self, Self::Err> {
348 s.to_owned().try_into()
349 }
350}
351
352impl TryFrom<String> for IdempotencyKey {
353 type Error = InvalidError;
354
355 fn try_from(value: String) -> Result<Self, Self::Error> {
356 if value.is_empty() {
357 return Err(InvalidError::new("idempotency key must not be empty"));
358 }
359 if value.len() > crate::limits::MAX_IDEMPOTENCY_KEY_BYTES {
360 return Err(InvalidError::new(format!(
361 "idempotency key is {}B, exceeds cap {}B",
362 value.len(),
363 crate::limits::MAX_IDEMPOTENCY_KEY_BYTES
364 )));
365 }
366 Ok(Self(value))
367 }
368}
369
370impl From<IdempotencyKey> for String {
371 fn from(value: IdempotencyKey) -> Self {
372 value.0
373 }
374}
375
376impl fmt::Display for IdempotencyKey {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.write_str(&self.0)
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
392#[serde(try_from = "String", into = "String")]
393pub struct AgentId(String);
394
395impl AgentId {
396 pub fn as_str(&self) -> &str {
398 &self.0
399 }
400}
401
402impl FromStr for AgentId {
403 type Err = InvalidError;
404
405 fn from_str(s: &str) -> Result<Self, Self::Err> {
406 s.to_owned().try_into()
407 }
408}
409
410impl TryFrom<String> for AgentId {
411 type Error = InvalidError;
412
413 fn try_from(value: String) -> Result<Self, Self::Error> {
414 if value.is_empty() {
415 return Err(InvalidError::new("agent id must not be empty"));
416 }
417 if value.len() > crate::limits::MAX_AGENT_STRING_BYTES {
418 return Err(InvalidError::new(format!(
419 "agent id is {}B, exceeds cap {}B",
420 value.len(),
421 crate::limits::MAX_AGENT_STRING_BYTES
422 )));
423 }
424 if let Some(c) = value.chars().find(|c| c.is_control()) {
425 return Err(InvalidError::new(format!(
426 "agent id must not contain control characters (found {c:?})"
427 )));
428 }
429 Ok(Self(value))
430 }
431}
432
433impl TryFrom<&str> for AgentId {
434 type Error = InvalidError;
435
436 fn try_from(value: &str) -> Result<Self, Self::Error> {
437 value.to_owned().try_into()
438 }
439}
440
441impl From<AgentId> for String {
442 fn from(value: AgentId) -> Self {
443 value.0
444 }
445}
446
447impl fmt::Display for AgentId {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 f.write_str(&self.0)
450 }
451}
452
453#[derive(
457 Clone,
458 Copy,
459 Debug,
460 PartialEq,
461 Eq,
462 Hash,
463 Serialize,
464 Deserialize,
465 strum::Display,
466 strum::EnumString,
467)]
468#[serde(rename_all = "snake_case")]
469#[strum(serialize_all = "snake_case")]
470pub enum AgentKind {
471 Command,
474 Response,
476 Event,
478 Chunk,
480 Status,
483 Error,
485}
486
487#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
499#[serde(from = "u8", into = "u8")]
500pub enum TaskState {
501 Submitted,
502 Working,
503 InputRequired,
504 Completed,
505 Canceled,
506 Failed,
507 Rejected,
508 AuthRequired,
509 Unknown,
510 Paused,
512 Unrecognized(u8),
514}
515
516impl TaskState {
517 pub const fn code(self) -> u8 {
519 match self {
520 TaskState::Submitted => 1,
521 TaskState::Working => 2,
522 TaskState::InputRequired => 3,
523 TaskState::Completed => 4,
524 TaskState::Canceled => 5,
525 TaskState::Failed => 6,
526 TaskState::Rejected => 7,
527 TaskState::AuthRequired => 8,
528 TaskState::Unknown => 9,
529 TaskState::Paused => 10,
530 TaskState::Unrecognized(code) => code,
531 }
532 }
533
534 pub const fn from_code(code: u8) -> Self {
537 match code {
538 1 => TaskState::Submitted,
539 2 => TaskState::Working,
540 3 => TaskState::InputRequired,
541 4 => TaskState::Completed,
542 5 => TaskState::Canceled,
543 6 => TaskState::Failed,
544 7 => TaskState::Rejected,
545 8 => TaskState::AuthRequired,
546 9 => TaskState::Unknown,
547 10 => TaskState::Paused,
548 other => TaskState::Unrecognized(other),
549 }
550 }
551
552 pub const fn is_terminal(self) -> bool {
555 matches!(
556 self,
557 TaskState::Completed | TaskState::Canceled | TaskState::Failed | TaskState::Rejected
558 )
559 }
560}
561
562impl From<u8> for TaskState {
563 fn from(code: u8) -> Self {
564 Self::from_code(code)
565 }
566}
567
568impl From<TaskState> for u8 {
569 fn from(state: TaskState) -> u8 {
570 state.code()
571 }
572}
573
574impl fmt::Display for TaskState {
575 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577 match self {
578 TaskState::Submitted => f.write_str("submitted"),
579 TaskState::Working => f.write_str("working"),
580 TaskState::InputRequired => f.write_str("input-required"),
581 TaskState::Completed => f.write_str("completed"),
582 TaskState::Canceled => f.write_str("canceled"),
583 TaskState::Failed => f.write_str("failed"),
584 TaskState::Rejected => f.write_str("rejected"),
585 TaskState::AuthRequired => f.write_str("auth-required"),
586 TaskState::Unknown => f.write_str("unknown"),
587 TaskState::Paused => f.write_str("paused"),
588 TaskState::Unrecognized(code) => write!(f, "unrecognized-{code}"),
589 }
590 }
591}
592
593impl FromStr for TaskState {
594 type Err = InvalidError;
595
596 fn from_str(s: &str) -> Result<Self, Self::Err> {
597 Ok(match s {
598 "submitted" => TaskState::Submitted,
599 "working" => TaskState::Working,
600 "input-required" => TaskState::InputRequired,
601 "completed" => TaskState::Completed,
602 "canceled" => TaskState::Canceled,
603 "failed" => TaskState::Failed,
604 "rejected" => TaskState::Rejected,
605 "auth-required" => TaskState::AuthRequired,
606 "unknown" => TaskState::Unknown,
607 "paused" => TaskState::Paused,
608 other => return Err(InvalidError::new(format!("unknown task state `{other}`"))),
609 })
610 }
611}
612
613#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "snake_case")]
616pub enum SessionStatus {
617 Submitted,
618 Active,
619 Paused,
620 Completed,
621 Failed,
622 Canceled,
623 #[serde(other)]
624 Unrecognized,
625}
626
627#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
629pub struct SessionRef {
630 pub stream: String,
631 pub session: ConversationId,
632}
633
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
636pub struct SdkInfo {
637 pub language: String,
638 pub version: String,
639}
640
641#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
643pub struct Budget {
644 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub tokens: Option<u64>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub cost_micros: Option<u64>,
648}
649
650#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
652pub struct SessionStart {
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub label: Option<String>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub namespace: Option<String>,
657 pub agent: AgentId,
658 pub sdk: SdkInfo,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub parent: Option<ConversationId>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub root: Option<ConversationId>,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub idle_timeout_micros: Option<u64>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub budget: Option<Budget>,
667 #[serde(default, skip_serializing_if = "Vec::is_empty")]
668 pub tags: Vec<String>,
669}
670
671#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
673pub struct SessionTransition {
674 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub actor: Option<AgentId>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub acknowledges: Option<LogPosition>,
678}
679
680#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
685pub struct SessionPauseRequest {
686 #[serde(default, skip_serializing_if = "Vec::is_empty")]
687 pub participants: Vec<AgentId>,
688}
689
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
696pub struct SessionParking {
697 pub source: crate::graph::SourceRef,
698 pub role: AgentId,
699 pub request: LogPosition,
700}
701
702impl crate::validate::Validate for SessionParking {
703 fn validate(&self) -> Result<(), InvalidError> {
704 match &self.source {
705 crate::graph::SourceRef::Message {
706 generation: Some(_),
707 ..
708 } => Ok(()),
709 crate::graph::SourceRef::Message { .. } => Err(InvalidError::new(
710 "a parked record must name its source topic generation",
711 )),
712 _ => Err(InvalidError::new(
713 "a parked record must point at a log record",
714 )),
715 }
716 }
717}
718
719#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
721pub struct SessionEnd {
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub reason: Option<String>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub error: Option<AgentErrorBody>,
726}
727
728pub const fn estimate_tokens(bytes: usize) -> u64 {
731 bytes.div_ceil(4) as u64
732}
733
734pub const METADATA_REQUEST_MODEL: &str = "gen_ai.request.model";
736pub const METADATA_RESPONSE_MODEL: &str = "gen_ai.response.model";
738pub const METADATA_PROVIDER_NAME: &str = "gen_ai.provider.name";
740pub const METADATA_SUBMITTED: &str = "submitted";
744pub const METADATA_DURATION_MICROS: &str = "duration_micros";
747
748#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
750pub struct ContextManifest {
751 pub policy: String,
752 pub policy_version: String,
753 pub fragments: Vec<Fragment>,
754 pub tokens: u64,
755 pub bytes: u64,
756 pub frontier: Vec<(u32, u32, u64)>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub correlation: Option<CorrelationId>,
759}
760
761#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
763pub enum Fragment {
764 Message {
765 at: crate::graph::SourceRef,
766 tokens: u32,
767 bytes: u32,
768 },
769 Memory {
770 id: String,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
772 version: Option<u64>,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
774 digest: Option<crate::schema::Digest32>,
775 tokens: u32,
776 bytes: u32,
777 },
778 Kv {
779 namespace: String,
780 key: String,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
782 version: Option<u64>,
783 #[serde(default, skip_serializing_if = "Option::is_none")]
784 digest: Option<crate::schema::Digest32>,
785 tokens: u32,
786 bytes: u32,
787 },
788 State {
789 key: String,
790 revision: u64,
791 tokens: u32,
792 bytes: u32,
793 },
794 Summary {
795 at: crate::graph::SourceRef,
796 tokens: u32,
797 bytes: u32,
798 },
799}
800
801#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
803pub struct ContextCompaction {
804 pub summary_at: crate::graph::SourceRef,
805 pub covered: Vec<(u32, u32, u64, u64)>,
806 pub summarizer: crate::graph::ProducerInfo,
807}
808
809#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
811pub struct ContextRetrieval {
812 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub query: Option<String>,
814 pub items: Vec<(String, f32)>,
815}
816
817pub use json_patch::PatchOperation;
819
820#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
822pub struct StateDelta {
823 pub base_revision: u64,
824 pub patch: Vec<json_patch::PatchOperation>,
825 pub op_id: String,
826}
827
828#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
830pub struct StateSnapshot {
831 pub base_revision: u64,
832 pub document: serde_json::Value,
833}
834
835impl crate::validate::Validate for ContextManifest {
836 fn validate(&self) -> Result<(), InvalidError> {
837 if self.fragments.len() > crate::limits::MAX_MANIFEST_FRAGMENTS {
838 return Err(InvalidError::new("context manifest has too many fragments"));
839 }
840 for fragment in &self.fragments {
841 let digest = match fragment {
842 Fragment::Memory { digest, .. } | Fragment::Kv { digest, .. } => digest.as_ref(),
843 _ => None,
844 };
845 if let Some(digest) = digest {
846 crate::validate::Validate::validate(digest)?;
847 }
848 }
849 Ok(())
850 }
851}
852
853impl crate::validate::Validate for StateDelta {
854 fn validate(&self) -> Result<(), InvalidError> {
855 if self.patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
856 return Err(InvalidError::new("state patch has too many operations"));
857 }
858 if self.op_id.is_empty() {
859 return Err(InvalidError::new(
860 "state patch operation id must not be empty",
861 ));
862 }
863 let bytes = serde_json::to_vec(&self.patch)
864 .map_err(|error| InvalidError::new(error.to_string()))?;
865 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
866 return Err(InvalidError::new(
867 "state patch exceeds the document byte cap",
868 ));
869 }
870 let patch = serde_json::to_value(&self.patch)
871 .map_err(|error| InvalidError::new(error.to_string()))?;
872 validate_portable_json(&patch)?;
873 Ok(())
874 }
875}
876
877impl crate::validate::Validate for StateSnapshot {
878 fn validate(&self) -> Result<(), InvalidError> {
879 let bytes = serde_json::to_vec(&self.document)
880 .map_err(|error| InvalidError::new(error.to_string()))?;
881 if bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
882 return Err(InvalidError::new("state document exceeds its byte cap"));
883 }
884 validate_portable_json(&self.document)?;
885 Ok(())
886 }
887}
888
889const MAX_EXACT_JSON_INTEGER: i128 = 9_007_199_254_740_991;
890
891fn validate_portable_json(value: &serde_json::Value) -> Result<(), InvalidError> {
892 match value {
893 serde_json::Value::Number(number) => {
894 let exact = if let Some(integer) = number.as_i64() {
895 i128::from(integer).abs() <= MAX_EXACT_JSON_INTEGER
896 } else if let Some(integer) = number.as_u64() {
897 i128::from(integer) <= MAX_EXACT_JSON_INTEGER
898 } else {
899 number.as_f64().is_some_and(|float| {
900 float.is_finite()
901 && (float.fract() != 0.0 || float.abs() <= MAX_EXACT_JSON_INTEGER as f64)
902 })
903 };
904 if !exact {
905 return Err(InvalidError::new(
906 "state JSON integer exceeds the exact number range",
907 ));
908 }
909 }
910 serde_json::Value::Array(items) => {
911 for item in items {
912 validate_portable_json(item)?;
913 }
914 }
915 serde_json::Value::Object(fields) => {
916 for item in fields.values() {
917 validate_portable_json(item)?;
918 }
919 }
920 _ => {}
921 }
922 Ok(())
923}
924
925pub fn apply_json_patch(
927 document: &serde_json::Value,
928 patch: &[json_patch::PatchOperation],
929) -> Result<serde_json::Value, InvalidError> {
930 if patch.len() > crate::limits::MAX_STATE_PATCH_OPS {
931 return Err(InvalidError::new("state patch has too many operations"));
932 }
933 let patch_bytes =
934 serde_json::to_vec(patch).map_err(|error| InvalidError::new(error.to_string()))?;
935 if patch_bytes.len() > crate::limits::MAX_STATE_DOCUMENT_BYTES {
936 return Err(InvalidError::new(
937 "state patch exceeds the document byte cap",
938 ));
939 }
940 validate_portable_json(document)?;
941 let patch_value =
942 serde_json::to_value(patch).map_err(|error| InvalidError::new(error.to_string()))?;
943 validate_portable_json(&patch_value)?;
944 let mut result = document.clone();
945 json_patch::patch(&mut result, patch).map_err(|error| InvalidError::new(error.to_string()))?;
946 crate::validate::Validate::validate(&StateSnapshot {
947 base_revision: 0,
948 document: result.clone(),
949 })?;
950 Ok(result)
951}
952
953impl crate::validate::Validate for SessionStart {
954 fn validate(&self) -> Result<(), InvalidError> {
955 if let Some(label) = &self.label
956 && (label.len() > crate::limits::MAX_SESSION_LABEL_BYTES
957 || label.chars().any(char::is_control))
958 {
959 return Err(InvalidError::new(
960 "session label exceeds its cap or contains a control character",
961 ));
962 }
963 if let Some(namespace) = &self.namespace {
964 crate::kv::validate_namespace(namespace)?;
965 }
966 if self.root.is_some() && self.parent.is_none() {
967 return Err(InvalidError::new("session root requires a parent"));
968 }
969 if self.tags.len() > crate::limits::MAX_MEMORY_TAGS {
970 return Err(InvalidError::new("session has too many tags"));
971 }
972 if self
973 .tags
974 .iter()
975 .any(|tag| tag.len() > crate::limits::MAX_MEMORY_TAG_BYTES)
976 {
977 return Err(InvalidError::new("session tag exceeds its byte cap"));
978 }
979 Ok(())
980 }
981}
982
983#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
990pub struct TokenUsage {
991 pub input_tokens: u64,
992 pub output_tokens: u64,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub reasoning_output_tokens: Option<u64>,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
996 pub cache_read_input_tokens: Option<u64>,
997 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub cache_creation_input_tokens: Option<u64>,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub cost_micros: Option<u64>,
1002}
1003
1004#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1007#[serde(from = "u8", into = "u8")]
1008pub enum AgentErrorCode {
1009 InvalidRequest,
1010 Unauthorized,
1011 Unsupported,
1012 DeadlineExceeded,
1013 Cancelled,
1014 ToolFailure,
1015 Internal,
1016 Unrecognized(u8),
1018}
1019
1020impl AgentErrorCode {
1021 pub const fn code(self) -> u8 {
1023 match self {
1024 AgentErrorCode::InvalidRequest => 1,
1025 AgentErrorCode::Unauthorized => 2,
1026 AgentErrorCode::Unsupported => 3,
1027 AgentErrorCode::DeadlineExceeded => 4,
1028 AgentErrorCode::Cancelled => 5,
1029 AgentErrorCode::ToolFailure => 6,
1030 AgentErrorCode::Internal => 7,
1031 AgentErrorCode::Unrecognized(code) => code,
1032 }
1033 }
1034
1035 pub const fn from_code(code: u8) -> Self {
1037 match code {
1038 1 => AgentErrorCode::InvalidRequest,
1039 2 => AgentErrorCode::Unauthorized,
1040 3 => AgentErrorCode::Unsupported,
1041 4 => AgentErrorCode::DeadlineExceeded,
1042 5 => AgentErrorCode::Cancelled,
1043 6 => AgentErrorCode::ToolFailure,
1044 7 => AgentErrorCode::Internal,
1045 other => AgentErrorCode::Unrecognized(other),
1046 }
1047 }
1048}
1049
1050impl From<u8> for AgentErrorCode {
1051 fn from(code: u8) -> Self {
1052 Self::from_code(code)
1053 }
1054}
1055
1056impl From<AgentErrorCode> for u8 {
1057 fn from(code: AgentErrorCode) -> u8 {
1058 code.code()
1059 }
1060}
1061
1062#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1066pub struct AgentErrorBody {
1067 pub code: AgentErrorCode,
1068 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub message: Option<String>,
1070 #[serde(default)]
1071 pub retryable: bool,
1072 #[serde(default, skip_serializing_if = "Option::is_none")]
1073 pub detail: Option<BTreeMap<String, Value>>,
1074}
1075
1076#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1078#[serde(from = "u8", into = "u8")]
1079pub enum DeadLetterReason {
1080 RetryExhausted,
1081 Rejected,
1082 DecodeFailed,
1083 DeadlineExceeded,
1084 Unrecognized(u8),
1086}
1087
1088impl DeadLetterReason {
1089 pub const fn code(self) -> u8 {
1091 match self {
1092 DeadLetterReason::RetryExhausted => 1,
1093 DeadLetterReason::Rejected => 2,
1094 DeadLetterReason::DecodeFailed => 3,
1095 DeadLetterReason::DeadlineExceeded => 4,
1096 DeadLetterReason::Unrecognized(code) => code,
1097 }
1098 }
1099
1100 pub const fn from_code(code: u8) -> Self {
1102 match code {
1103 1 => DeadLetterReason::RetryExhausted,
1104 2 => DeadLetterReason::Rejected,
1105 3 => DeadLetterReason::DecodeFailed,
1106 4 => DeadLetterReason::DeadlineExceeded,
1107 other => DeadLetterReason::Unrecognized(other),
1108 }
1109 }
1110}
1111
1112impl From<u8> for DeadLetterReason {
1113 fn from(code: u8) -> Self {
1114 Self::from_code(code)
1115 }
1116}
1117
1118impl From<DeadLetterReason> for u8 {
1119 fn from(reason: DeadLetterReason) -> u8 {
1120 reason.code()
1121 }
1122}
1123
1124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1134pub struct AgentDeadLetter {
1135 pub source: LogPosition,
1136 pub reason: DeadLetterReason,
1137 pub attempts: u32,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub detail: Option<String>,
1140 #[serde(with = "crate::encoding::bin_bytes")]
1141 pub payload: Vec<u8>,
1142}
1143
1144#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1147#[serde(from = "u8", into = "u8")]
1148pub enum Health {
1149 Healthy,
1150 Degraded,
1151 Unavailable,
1152 Unrecognized(u8),
1154}
1155
1156impl Health {
1157 pub const fn code(self) -> u8 {
1159 match self {
1160 Health::Healthy => 1,
1161 Health::Degraded => 2,
1162 Health::Unavailable => 3,
1163 Health::Unrecognized(code) => code,
1164 }
1165 }
1166
1167 pub const fn from_code(code: u8) -> Self {
1170 match code {
1171 1 => Health::Healthy,
1172 2 => Health::Degraded,
1173 3 => Health::Unavailable,
1174 other => Health::Unrecognized(other),
1175 }
1176 }
1177}
1178
1179impl From<u8> for Health {
1180 fn from(code: u8) -> Self {
1181 Self::from_code(code)
1182 }
1183}
1184
1185impl From<Health> for u8 {
1186 fn from(health: Health) -> u8 {
1187 health.code()
1188 }
1189}
1190
1191#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1199#[serde(rename_all = "snake_case")]
1200pub enum ContentRef {
1201 ContentType(crate::content::ContentType),
1202 SchemaId(String),
1203}
1204
1205#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1209pub struct CapabilityDescriptor {
1210 pub skill_id: String,
1212 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub input: Option<ContentRef>,
1214 #[serde(default, skip_serializing_if = "Option::is_none")]
1215 pub output: Option<ContentRef>,
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1218 pub cost_class: Option<u8>,
1219 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub latency_class: Option<u8>,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub max_concurrency: Option<u32>,
1224 #[serde(default, skip_serializing_if = "Option::is_none")]
1225 pub health: Option<Health>,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub load: Option<u16>,
1229}
1230
1231#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1239pub struct AgentCard {
1240 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub name: Option<String>,
1244 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub version: Option<String>,
1247 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1250 pub capabilities: Vec<CapabilityDescriptor>,
1251 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub ttl_micros: Option<u64>,
1255}
1256
1257impl AgentCard {
1258 pub fn validate(&self) -> Result<(), ValidateError> {
1260 cap_str(self.name.as_deref(), "name")?;
1261 cap_str(self.version.as_deref(), "version")?;
1262 if self.capabilities.len() > crate::limits::MAX_CARD_CAPABILITIES {
1263 return Err(ValidateError::TooLarge {
1264 field: "capabilities",
1265 size: self.capabilities.len(),
1266 cap: crate::limits::MAX_CARD_CAPABILITIES,
1267 });
1268 }
1269 for capability in &self.capabilities {
1270 cap_str(Some(&capability.skill_id), "capability skill_id")?;
1271 }
1272 Ok(())
1273 }
1274}
1275
1276#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1294pub struct AgentPresence {
1295 pub v: u32,
1298 pub agent: AgentId,
1303 #[serde(default, skip_serializing_if = "Option::is_none")]
1307 pub inbox: Option<String>,
1308}
1309
1310impl AgentPresence {
1311 pub fn new(agent: AgentId) -> Self {
1314 Self {
1315 v: crate::codes::PRESENCE_OP_VERSION,
1316 agent,
1317 inbox: None,
1318 }
1319 }
1320
1321 pub fn with_inbox(mut self, inbox: impl Into<String>) -> Self {
1323 self.inbox = Some(inbox.into());
1324 self
1325 }
1326
1327 pub fn validate(&self) -> Result<(), ValidateError> {
1330 cap_str(self.inbox.as_deref(), "inbox")?;
1331 Ok(())
1332 }
1333}
1334
1335#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1344pub struct BodyRef {
1345 pub reference: String,
1348 pub size_bytes: u64,
1350 #[serde(with = "crate::encoding::bin_bytes")]
1352 pub sha256: Vec<u8>,
1353 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub encryption: Option<u8>,
1358}
1359
1360const SHA256_BYTES: usize = 32;
1361
1362impl BodyRef {
1363 pub fn new(reference: impl Into<String>, size_bytes: u64, sha256: [u8; 32]) -> Self {
1365 Self {
1366 reference: reference.into(),
1367 size_bytes,
1368 sha256: sha256.to_vec(),
1369 encryption: None,
1370 }
1371 }
1372
1373 pub fn validate(&self) -> Result<(), ValidateError> {
1377 if self.reference.is_empty() {
1378 return Err(ValidateError::Invalid {
1379 field: "reference",
1380 reason: "reference must not be empty".to_owned(),
1381 });
1382 }
1383 if self.reference.len() > crate::limits::MAX_BODY_REFERENCE_BYTES {
1384 return Err(ValidateError::TooLarge {
1385 field: "reference",
1386 size: self.reference.len(),
1387 cap: crate::limits::MAX_BODY_REFERENCE_BYTES,
1388 });
1389 }
1390 if self.sha256.len() != SHA256_BYTES {
1391 return Err(ValidateError::Invalid {
1392 field: "sha256",
1393 reason: format!(
1394 "digest must be {SHA256_BYTES} bytes, got {}",
1395 self.sha256.len()
1396 ),
1397 });
1398 }
1399 Ok(())
1400 }
1401}
1402
1403#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1415pub struct Signature {
1416 pub scheme: u8,
1419 #[serde(with = "crate::encoding::bin_bytes")]
1421 pub key_id: Vec<u8>,
1422 #[serde(with = "crate::encoding::bin_bytes")]
1425 pub bytes: Vec<u8>,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub context: Option<SignatureContext>,
1433}
1434
1435#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1441pub struct SignatureContext {
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub content_type: Option<u8>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub agent_version: Option<u32>,
1448}
1449
1450pub const SIGNATURE_SCHEME_ED25519: u8 = 1;
1452
1453pub const SIGNATURE_DOMAIN: &[u8] = b"agdx.signature.v1";
1458
1459const ED25519_KEY_ID_BYTES: usize = 8;
1460const ED25519_SIGNATURE_BYTES: usize = 64;
1461
1462impl Signature {
1463 pub fn validate(&self) -> Result<(), ValidateError> {
1468 if self.scheme != SIGNATURE_SCHEME_ED25519 {
1469 return Ok(());
1470 }
1471 if self.key_id.len() != ED25519_KEY_ID_BYTES {
1472 return Err(ValidateError::Invalid {
1473 field: "key_id",
1474 reason: format!(
1475 "Ed25519 key id must be {ED25519_KEY_ID_BYTES} bytes, got {}",
1476 self.key_id.len()
1477 ),
1478 });
1479 }
1480 if self.bytes.len() != ED25519_SIGNATURE_BYTES {
1481 return Err(ValidateError::Invalid {
1482 field: "bytes",
1483 reason: format!(
1484 "Ed25519 signature must be {ED25519_SIGNATURE_BYTES} bytes, got {}",
1485 self.bytes.len()
1486 ),
1487 });
1488 }
1489 Ok(())
1490 }
1491}
1492
1493#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1506pub struct AgentEnvelope {
1507 pub kind: AgentKind,
1508 #[serde(default, skip_serializing_if = "Option::is_none")]
1512 pub record: Option<RecordId>,
1513 pub conversation: ConversationId,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 pub parent: Option<ConversationId>,
1518 #[serde(default, skip_serializing_if = "Option::is_none")]
1520 pub root: Option<ConversationId>,
1521 pub source: AgentId,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1527 pub target: Option<AgentId>,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub cause: Option<RecordId>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1535 pub cause_at: Option<LogPosition>,
1536 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub correlation: Option<CorrelationId>,
1539 #[serde(default, skip_serializing_if = "Option::is_none")]
1541 pub channel: Option<ChannelId>,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub idempotency_key: Option<IdempotencyKey>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub deadline_micros: Option<u64>,
1549 #[serde(default, skip_serializing_if = "Option::is_none")]
1551 pub sequence: Option<u64>,
1552 #[serde(default, skip_serializing_if = "is_false")]
1556 pub last: bool,
1557 #[serde(default, skip_serializing_if = "Option::is_none")]
1561 pub finish_reason: Option<String>,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1563 pub task_state: Option<TaskState>,
1564 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 pub operation: Option<String>,
1568 #[serde(default, skip_serializing_if = "Option::is_none")]
1570 pub tool: Option<String>,
1571 #[serde(default, skip_serializing_if = "Option::is_none")]
1572 pub usage: Option<TokenUsage>,
1573 #[serde(default, skip_serializing_if = "Option::is_none")]
1575 pub metadata: Option<BTreeMap<String, Value>>,
1576 #[serde(default, skip_serializing_if = "is_zero_u64")]
1588 pub must_understand: u64,
1589 #[serde(
1592 default,
1593 skip_serializing_if = "Vec::is_empty",
1594 with = "crate::encoding::bin_bytes"
1595 )]
1596 pub body: Vec<u8>,
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1604 pub signature: Option<Signature>,
1605}
1606
1607fn is_false(value: &bool) -> bool {
1608 !*value
1609}
1610
1611fn is_zero_u64(value: &u64) -> bool {
1612 *value == 0
1613}
1614
1615pub mod features {
1622 pub const NONE: u64 = 0;
1626}
1627
1628impl AgentEnvelope {
1629 fn base(kind: AgentKind, conversation: ConversationId, source: AgentId) -> Self {
1630 Self {
1631 kind,
1632 record: None,
1633 conversation,
1634 parent: None,
1635 root: None,
1636 source,
1637 target: None,
1638 cause: None,
1639 cause_at: None,
1640 correlation: None,
1641 channel: None,
1642 idempotency_key: None,
1643 deadline_micros: None,
1644 sequence: None,
1645 last: false,
1646 finish_reason: None,
1647 task_state: None,
1648 operation: None,
1649 tool: None,
1650 usage: None,
1651 metadata: None,
1652 must_understand: 0,
1653 body: Vec::new(),
1654 signature: None,
1655 }
1656 }
1657
1658 #[must_use]
1663 pub fn requiring(mut self, bits: u64) -> Self {
1664 self.must_understand = bits;
1665 self
1666 }
1667
1668 pub fn unmet_requirements(&self, understood: u64) -> u64 {
1673 self.must_understand & !understood
1674 }
1675
1676 pub fn command(
1678 record: RecordId,
1679 conversation: ConversationId,
1680 source: AgentId,
1681 correlation: CorrelationId,
1682 body: Vec<u8>,
1683 ) -> Self {
1684 let mut envelope = Self::base(AgentKind::Command, conversation, source);
1685 envelope.record = Some(record);
1686 envelope.correlation = Some(correlation);
1687 envelope.body = body;
1688 envelope
1689 }
1690
1691 pub fn response(
1693 record: RecordId,
1694 conversation: ConversationId,
1695 source: AgentId,
1696 correlation: CorrelationId,
1697 body: Vec<u8>,
1698 ) -> Self {
1699 let mut envelope = Self::base(AgentKind::Response, conversation, source);
1700 envelope.record = Some(record);
1701 envelope.correlation = Some(correlation);
1702 envelope.body = body;
1703 envelope
1704 }
1705
1706 pub fn event(
1708 record: RecordId,
1709 conversation: ConversationId,
1710 source: AgentId,
1711 body: Vec<u8>,
1712 ) -> Self {
1713 let mut envelope = Self::base(AgentKind::Event, conversation, source);
1714 envelope.record = Some(record);
1715 envelope.body = body;
1716 envelope
1717 }
1718
1719 pub fn chunk(
1722 conversation: ConversationId,
1723 source: AgentId,
1724 correlation: CorrelationId,
1725 channel: ChannelId,
1726 sequence: u64,
1727 body: Vec<u8>,
1728 ) -> Self {
1729 let mut envelope = Self::base(AgentKind::Chunk, conversation, source);
1730 envelope.correlation = Some(correlation);
1731 envelope.channel = Some(channel);
1732 envelope.sequence = Some(sequence);
1733 envelope.body = body;
1734 envelope
1735 }
1736
1737 pub fn status(
1741 record: RecordId,
1742 conversation: ConversationId,
1743 source: AgentId,
1744 operation: impl Into<String>,
1745 ) -> Self {
1746 let mut envelope = Self::base(AgentKind::Status, conversation, source);
1747 envelope.record = Some(record);
1748 envelope.operation = Some(operation.into());
1749 envelope
1750 }
1751
1752 pub fn error(
1755 record: RecordId,
1756 conversation: ConversationId,
1757 source: AgentId,
1758 correlation: CorrelationId,
1759 body: Vec<u8>,
1760 ) -> Self {
1761 let mut envelope = Self::base(AgentKind::Error, conversation, source);
1762 envelope.record = Some(record);
1763 envelope.correlation = Some(correlation);
1764 envelope.body = body;
1765 envelope
1766 }
1767
1768 pub fn with_target(mut self, target: AgentId) -> Self {
1770 self.target = Some(target);
1771 self
1772 }
1773
1774 pub fn with_cause(mut self, cause: RecordId, cause_at: Option<LogPosition>) -> Self {
1778 self.cause = Some(cause);
1779 self.cause_at = cause_at;
1780 self
1781 }
1782
1783 pub fn with_correlation(mut self, correlation: CorrelationId) -> Self {
1787 self.correlation = Some(correlation);
1788 self
1789 }
1790
1791 pub fn with_idempotency_key(mut self, key: IdempotencyKey) -> Self {
1793 self.idempotency_key = Some(key);
1794 self
1795 }
1796
1797 pub fn with_deadline_micros(mut self, deadline_micros: u64) -> Self {
1800 self.deadline_micros = Some(deadline_micros);
1801 self
1802 }
1803
1804 pub fn terminal(mut self, finish_reason: impl Into<String>) -> Self {
1807 self.last = true;
1808 self.finish_reason = Some(finish_reason.into());
1809 self
1810 }
1811
1812 pub fn with_task_state(mut self, state: TaskState) -> Self {
1815 self.task_state = Some(state);
1816 self
1817 }
1818
1819 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
1821 self.operation = Some(operation.into());
1822 self
1823 }
1824
1825 pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
1827 self.tool = Some(tool.into());
1828 self
1829 }
1830
1831 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
1833 self.usage = Some(usage);
1834 self
1835 }
1836
1837 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
1839 self.metadata
1840 .get_or_insert_with(BTreeMap::new)
1841 .insert(key.into(), value.into());
1842 self
1843 }
1844
1845 pub fn with_signature(mut self, signature: Signature) -> Self {
1850 self.signature = Some(signature);
1851 self
1852 }
1853}
1854
1855#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1858#[non_exhaustive]
1859pub enum ValidateError {
1860 #[error("{kind} requires `{field}`")]
1861 Missing {
1862 kind: AgentKind,
1863 field: &'static str,
1864 },
1865 #[error("`{field}` is invalid on {kind}")]
1866 Forbidden {
1867 kind: AgentKind,
1868 field: &'static str,
1869 },
1870 #[error("`{field}` is {size}B, exceeds cap {cap}B")]
1871 TooLarge {
1872 field: &'static str,
1873 size: usize,
1874 cap: usize,
1875 },
1876 #[error("`{field}`: {reason}")]
1877 Invalid { field: &'static str, reason: String },
1878}
1879
1880pub fn validate(envelope: &AgentEnvelope) -> Result<(), ValidateError> {
1905 use AgentKind::*;
1906 let kind = envelope.kind;
1907
1908 let require = |present: bool, field: &'static str| {
1909 if present {
1910 Ok(())
1911 } else {
1912 Err(ValidateError::Missing { kind, field })
1913 }
1914 };
1915 let forbid = |absent: bool, field: &'static str| {
1916 if absent {
1917 Ok(())
1918 } else {
1919 Err(ValidateError::Forbidden { kind, field })
1920 }
1921 };
1922
1923 if kind != Chunk {
1925 require(envelope.record.is_some(), "record")?;
1926 }
1927
1928 if envelope.root.is_some() && envelope.parent.is_none() {
1929 return Err(ValidateError::Invalid {
1930 field: "root",
1931 reason: "root requires parent".to_owned(),
1932 });
1933 }
1934 if envelope.parent == Some(envelope.conversation)
1935 || envelope.root == Some(envelope.conversation)
1936 {
1937 return Err(ValidateError::Invalid {
1938 field: "parent",
1939 reason: "parent and root must differ from conversation".to_owned(),
1940 });
1941 }
1942
1943 match kind {
1945 Command | Response | Chunk | Error => {
1946 require(envelope.correlation.is_some(), "correlation")?
1947 }
1948 Status => {
1949 if envelope.operation.as_deref() == Some(OPERATION_TASK) {
1950 require(envelope.correlation.is_some(), "correlation")?;
1951 }
1952 }
1953 Event => {}
1954 }
1955
1956 match kind {
1959 Chunk => {
1960 require(envelope.channel.is_some(), "channel")?;
1961 require(envelope.sequence.is_some(), "sequence")?;
1962 }
1963 Error => {
1964 if envelope.sequence.is_some() && envelope.channel.is_none() {
1965 return Err(ValidateError::Invalid {
1966 field: "sequence",
1967 reason: "sequence requires channel".to_owned(),
1968 });
1969 }
1970 }
1971 _ => {
1972 forbid(envelope.channel.is_none(), "channel")?;
1973 forbid(envelope.sequence.is_none(), "sequence")?;
1974 }
1975 }
1976
1977 if envelope.last && !matches!(kind, Chunk | Status) {
1980 return Err(ValidateError::Forbidden {
1981 kind,
1982 field: "last",
1983 });
1984 }
1985
1986 match kind {
1988 Response => {}
1989 Chunk => {
1990 if envelope.finish_reason.is_some() && !envelope.last {
1991 return Err(ValidateError::Invalid {
1992 field: "finish_reason",
1993 reason: "finish_reason rides only the terminal chunk".to_owned(),
1994 });
1995 }
1996 }
1997 _ => forbid(envelope.finish_reason.is_none(), "finish_reason")?,
1998 }
1999
2000 if matches!(kind, Chunk | Status | Error) {
2003 forbid(envelope.idempotency_key.is_none(), "idempotency_key")?;
2004 }
2005
2006 if matches!(kind, Response | Event | Status | Error) {
2009 forbid(envelope.deadline_micros.is_none(), "deadline_micros")?;
2010 }
2011 if kind == Chunk && envelope.deadline_micros.is_some() && envelope.sequence != Some(0) {
2012 return Err(ValidateError::Invalid {
2013 field: "deadline_micros",
2014 reason: "the stream bound rides the opening chunk (sequence 0)".to_owned(),
2015 });
2016 }
2017
2018 match kind {
2020 Status => {
2021 if matches!(
2022 envelope.operation.as_deref(),
2023 Some(OPERATION_TASK | OPERATION_SESSION)
2024 ) {
2025 require(envelope.task_state.is_some(), "task_state")?;
2026 }
2027 }
2028 Response | Error => {}
2029 _ => forbid(envelope.task_state.is_none(), "task_state")?,
2030 }
2031
2032 match kind {
2038 Status => {
2039 require(envelope.operation.is_some(), "operation")?;
2040 if let Some(operation) = envelope.operation.as_deref()
2041 && !matches!(
2042 operation,
2043 OPERATION_TASK
2044 | OPERATION_SESSION
2045 | OPERATION_CARD
2046 | OPERATION_PROGRESS
2047 | OPERATION_QUARANTINE
2048 | OPERATION_UNQUARANTINE
2049 )
2050 {
2051 return Err(ValidateError::Invalid {
2052 field: "operation",
2053 reason: format!(
2054 "status operation must be `{OPERATION_TASK}`, `{OPERATION_SESSION}`, `{OPERATION_CARD}`, \
2055 `{OPERATION_PROGRESS}`, `{OPERATION_QUARANTINE}`, or \
2056 `{OPERATION_UNQUARANTINE}`, got `{operation}`"
2057 ),
2058 });
2059 }
2060 }
2061 Chunk => {
2062 if envelope.sequence == Some(0) {
2063 require(envelope.operation.is_some(), "operation")?;
2064 }
2065 if let Some(operation) = envelope.operation.as_deref() {
2066 if envelope.sequence != Some(0) {
2067 return Err(ValidateError::Invalid {
2068 field: "operation",
2069 reason: "the stream purpose rides the opening chunk (sequence 0)"
2070 .to_owned(),
2071 });
2072 }
2073 if !matches!(
2074 operation,
2075 OPERATION_CHAT | OPERATION_REASONING | OPERATION_TOOL_ARGS
2076 ) {
2077 return Err(ValidateError::Invalid {
2078 field: "operation",
2079 reason: format!(
2080 "chunk-stream purpose must be `{OPERATION_CHAT}`, \
2081 `{OPERATION_REASONING}`, or `{OPERATION_TOOL_ARGS}`, \
2082 got `{operation}`"
2083 ),
2084 });
2085 }
2086 }
2087 }
2088 Command | Response | Event | Error => {}
2089 }
2090
2091 if kind == Status {
2093 forbid(envelope.tool.is_none(), "tool")?;
2094 }
2095
2096 match kind {
2098 Command => forbid(envelope.usage.is_none(), "usage")?,
2099 Chunk if envelope.usage.is_some() && !envelope.last => {
2100 return Err(ValidateError::Invalid {
2101 field: "usage",
2102 reason: "whole-stream accounting rides the terminal chunk".to_owned(),
2103 });
2104 }
2105 _ => {}
2106 }
2107
2108 match kind {
2110 Status if envelope.operation.as_deref() == Some(OPERATION_SESSION) => {
2111 require(!envelope.body.is_empty(), "body")?;
2112 let state = envelope
2113 .task_state
2114 .expect("session state was required above");
2115 if envelope.last != state.is_terminal() {
2116 return Err(ValidateError::Invalid {
2117 field: "last",
2118 reason: "session last must match terminal task state".to_owned(),
2119 });
2120 }
2121 #[cfg(feature = "cbor")]
2123 {
2124 let invalid_body = |reason: String| ValidateError::Invalid {
2125 field: "body",
2126 reason,
2127 };
2128 let validate_start = |start: &SessionStart| {
2129 crate::validate::Validate::validate(start)
2130 .map_err(|error| invalid_body(error.to_string()))?;
2131 if start.parent != envelope.parent || start.root != envelope.root {
2132 return Err(invalid_body(
2133 "session ancestry must match envelope parent and root".to_owned(),
2134 ));
2135 }
2136 Ok(())
2137 };
2138 match state {
2139 TaskState::Submitted => {
2140 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2141 .map_err(|error| invalid_body(error.to_string()))?;
2142 validate_start(&start)?;
2143 }
2144 TaskState::Working => {
2145 let body: ciborium::value::Value =
2146 crate::framing::decode_named(&envelope.body)
2147 .map_err(|error| invalid_body(error.to_string()))?;
2148 let is_start = matches!(
2149 body,
2150 ciborium::value::Value::Map(ref entries)
2151 if entries.iter().any(|(key, _)| {
2152 matches!(key, ciborium::value::Value::Text(name) if name == "agent" || name == "sdk")
2153 })
2154 );
2155 if is_start {
2156 let start: SessionStart = crate::framing::decode_named(&envelope.body)
2157 .map_err(|error| invalid_body(error.to_string()))?;
2158 validate_start(&start)?;
2159 } else {
2160 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2161 .map_err(|error| invalid_body(error.to_string()))?;
2162 }
2163 }
2164 state if state.is_terminal() => {
2165 crate::framing::decode_named::<SessionEnd>(&envelope.body)
2166 .map_err(|error| invalid_body(error.to_string()))?;
2167 }
2168 _ => {
2169 crate::framing::decode_named::<SessionTransition>(&envelope.body)
2170 .map_err(|error| invalid_body(error.to_string()))?;
2171 }
2172 }
2173 }
2174 }
2175 Status => {}
2176 Chunk => {
2177 if envelope.body.is_empty() && !envelope.last {
2178 return Err(ValidateError::Missing {
2179 kind,
2180 field: "body",
2181 });
2182 }
2183 }
2184 _ => require(!envelope.body.is_empty(), "body")?,
2185 }
2186
2187 cap_str(envelope.operation.as_deref(), "operation")?;
2191 cap_str(envelope.tool.as_deref(), "tool")?;
2192 cap_str(envelope.finish_reason.as_deref(), "finish_reason")?;
2193 if let Some(metadata) = &envelope.metadata {
2194 if metadata.len() > crate::limits::MAX_METADATA_ENTRIES {
2195 return Err(ValidateError::TooLarge {
2196 field: "metadata",
2197 size: metadata.len(),
2198 cap: crate::limits::MAX_METADATA_ENTRIES,
2199 });
2200 }
2201 let mut total = 0usize;
2202 for (key, value) in metadata {
2203 if key.len() > crate::limits::MAX_METADATA_KEY_BYTES {
2204 return Err(ValidateError::TooLarge {
2205 field: "metadata key",
2206 size: key.len(),
2207 cap: crate::limits::MAX_METADATA_KEY_BYTES,
2208 });
2209 }
2210 let value_size = value_size(value);
2211 if value_size > crate::limits::MAX_METADATA_VALUE_BYTES {
2212 return Err(ValidateError::TooLarge {
2213 field: "metadata value",
2214 size: value_size,
2215 cap: crate::limits::MAX_METADATA_VALUE_BYTES,
2216 });
2217 }
2218 total += key.len() + value_size;
2219 }
2220 if total > crate::limits::MAX_METADATA_TOTAL_BYTES {
2221 return Err(ValidateError::TooLarge {
2222 field: "metadata",
2223 size: total,
2224 cap: crate::limits::MAX_METADATA_TOTAL_BYTES,
2225 });
2226 }
2227 }
2228 if let Some(signature) = &envelope.signature {
2231 signature.validate()?;
2232 }
2233 Ok(())
2234}
2235
2236pub const OPERATION_TASK: &str = "task";
2238pub const OPERATION_SESSION: &str = "session";
2240pub const OPERATION_CARD: &str = "card";
2242pub const OPERATION_PROGRESS: &str = "progress";
2244pub const OPERATION_QUARANTINE: &str = "quarantine";
2249pub const OPERATION_UNQUARANTINE: &str = "unquarantine";
2254
2255pub const OPERATION_CHAT: &str = "chat";
2262pub const OPERATION_REASONING: &str = "reasoning";
2264pub const OPERATION_TOOL_ARGS: &str = "tool_args";
2266
2267pub const OPERATION_STATE_SNAPSHOT: &str = "state_snapshot";
2273pub const OPERATION_STATE_DELTA: &str = "state_delta";
2276pub const OPERATION_SESSION_PARKED: &str = "session_parked";
2279pub const OPERATION_SESSION_UNPARKED: &str = "session_unparked";
2282
2283pub const METADATA_ROLE: &str = "role";
2290pub const METADATA_BRIDGE_HOPS: &str = "bridge_hops";
2296pub const METADATA_DELEGATED_BY: &str = "on_behalf_of";
2300pub const METADATA_PURPOSE: &str = "purpose";
2303pub const METADATA_DATA_CLASSIFICATION: &str = "data_classification";
2306pub const METADATA_TASK_CONTEXT: &str = "task_context";
2309pub const METADATA_SESSION_INTENT: &str = "session_intent";
2312
2313const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2317
2318pub(crate) fn crockford_encode(value: u128) -> [u8; 26] {
2319 let mut out = [0u8; 26];
2320 let mut v = value;
2321 for slot in out.iter_mut().rev() {
2322 *slot = CROCKFORD[(v & 0x1f) as usize];
2323 v >>= 5;
2324 }
2325 out
2326}
2327
2328pub(crate) fn crockford_decode(s: &str) -> Result<u128, IdParseError> {
2329 let bytes = s.as_bytes();
2330 if bytes.len() != 26 {
2331 return Err(IdParseError::Length { got: bytes.len() });
2332 }
2333 let mut value: u128 = 0;
2334 for (i, byte) in bytes.iter().enumerate() {
2335 let digit = CROCKFORD
2336 .iter()
2337 .position(|c| *c == byte.to_ascii_uppercase())
2338 .ok_or(IdParseError::Char(*byte as char))?;
2339 if i == 0 && digit > 7 {
2342 return Err(IdParseError::Overflow);
2343 }
2344 value = (value << 5) | digit as u128;
2345 }
2346 Ok(value)
2347}
2348
2349fn cap_str(value: Option<&str>, field: &'static str) -> Result<(), ValidateError> {
2350 if let Some(value) = value
2351 && value.len() > crate::limits::MAX_AGENT_STRING_BYTES
2352 {
2353 return Err(ValidateError::TooLarge {
2354 field,
2355 size: value.len(),
2356 cap: crate::limits::MAX_AGENT_STRING_BYTES,
2357 });
2358 }
2359 Ok(())
2360}
2361
2362fn value_size(value: &Value) -> usize {
2365 match value {
2366 Value::Str(s) => s.len(),
2367 Value::List(items) => items.iter().map(|item| 1 + value_size(item)).sum(),
2368 _ => 9,
2369 }
2370}
2371
2372#[cfg(test)]
2373mod tests {
2374 use super::*;
2375 use crate::validate::Validate;
2376
2377 fn session_start() -> SessionStart {
2378 SessionStart {
2379 label: Some("Research".to_owned()),
2380 namespace: Some("agents".to_owned()),
2381 agent: "planner".parse().expect("valid agent id"),
2382 sdk: SdkInfo {
2383 language: "rust".to_owned(),
2384 version: "0.8.0".to_owned(),
2385 },
2386 parent: None,
2387 root: None,
2388 idle_timeout_micros: Some(300_000_000),
2389 budget: Some(Budget {
2390 tokens: Some(1000),
2391 cost_micros: None,
2392 }),
2393 tags: vec!["demo".to_owned()],
2394 }
2395 }
2396
2397 #[test]
2398 fn given_session_status_when_decoded_then_should_keep_known_and_unknown_values() {
2399 assert_eq!(
2400 serde_json::from_str::<SessionStatus>("\"paused\"").expect("paused"),
2401 SessionStatus::Paused
2402 );
2403 assert_eq!(
2404 serde_json::from_str::<SessionStatus>("\"future\"").expect("unknown"),
2405 SessionStatus::Unrecognized
2406 );
2407 }
2408
2409 #[test]
2410 fn given_session_start_when_validated_then_should_enforce_label_and_tag_caps() {
2411 let mut start = session_start();
2412 start.validate().expect("valid start");
2413 start.label = Some("bad\nlabel".to_owned());
2414 assert!(start.validate().is_err());
2415 start.label = Some("x".repeat(crate::limits::MAX_SESSION_LABEL_BYTES + 1));
2416 assert!(start.validate().is_err());
2417 start.label = None;
2418 start.tags = vec!["tag".to_owned(); crate::limits::MAX_MEMORY_TAGS + 1];
2419 assert!(start.validate().is_err());
2420 }
2421
2422 #[cfg(feature = "cbor")]
2423 #[test]
2424 fn given_session_status_bodies_when_validated_then_should_match_the_lifecycle() {
2425 let mut status = AgentEnvelope::status(
2426 RecordId::from_u128(1),
2427 ConversationId::from_u128(2),
2428 "planner".parse().expect("valid agent id"),
2429 OPERATION_SESSION,
2430 );
2431 status.task_state = Some(TaskState::Submitted);
2432 status.body = crate::framing::encode_named(&session_start()).expect("start encodes");
2433 validate(&status).expect("submitted start");
2434 status.parent = Some(ConversationId::from_u128(8));
2435 assert!(
2436 validate(&status).is_err(),
2437 "start and envelope ancestry must match"
2438 );
2439 status.parent = None;
2440
2441 status.task_state = Some(TaskState::Paused);
2442 status.body = crate::framing::encode_named(&SessionTransition::default())
2443 .expect("transition encodes");
2444 validate(&status).expect("paused transition");
2445
2446 status.task_state = Some(TaskState::Completed);
2447 status.body = crate::framing::encode_named(&SessionEnd::default()).expect("end encodes");
2448 assert!(validate(&status).is_err(), "terminal status needs last");
2449 status.last = true;
2450 validate(&status).expect("completed end");
2451 }
2452
2453 #[cfg(feature = "cbor")]
2454 #[test]
2455 fn given_a_parked_record_when_encoded_then_should_round_trip_and_require_a_generation() {
2456 use crate::validate::Validate;
2457 let source = |generation| crate::graph::SourceRef::Message {
2458 stream: 1,
2459 topic: 2,
2460 partition: 3,
2461 offset: 4,
2462 generation,
2463 conversation: Some(ConversationId::from_u128(5).to_string()),
2464 };
2465 let parking = SessionParking {
2466 source: source(Some(6)),
2467 role: "worker".parse().expect("valid agent id"),
2468 request: LogPosition::new(1, 7, 0, 8),
2469 };
2470 parking.validate().expect("a complete parking is valid");
2471 let bytes = crate::framing::encode_named(&parking).expect("parking encodes");
2472 let decoded: SessionParking = crate::framing::decode_named(&bytes).expect("decodes");
2473 assert_eq!(decoded, parking);
2474 let unproven = SessionParking {
2475 source: source(None),
2476 ..parking.clone()
2477 };
2478 assert!(unproven.validate().is_err());
2479 let not_a_record = SessionParking {
2480 source: crate::graph::SourceRef::Memory { id: "m".to_owned() },
2481 ..parking
2482 };
2483 assert!(not_a_record.validate().is_err());
2484 }
2485
2486 #[test]
2487 fn given_a_pause_request_body_when_decoded_then_should_read_an_empty_body_as_no_participants() {
2488 let empty: SessionPauseRequest = serde_json::from_slice(b"{}").expect("empty decodes");
2489 assert!(empty.participants.is_empty());
2490 let named = SessionPauseRequest {
2491 participants: vec!["worker".parse().expect("valid agent id")],
2492 };
2493 let json = serde_json::to_string(&named).expect("request encodes");
2494 assert_eq!(json, r#"{"participants":["worker"]}"#);
2495 assert_eq!(
2496 serde_json::from_str::<SessionPauseRequest>(&json).expect("decodes"),
2497 named
2498 );
2499 }
2500
2501 #[test]
2502 fn given_an_id_when_displayed_then_should_round_trip_through_crockford_base32() {
2503 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2504 let text = id.to_string();
2505 assert_eq!(text.len(), 26);
2506 assert_eq!(text.parse::<RecordId>().expect("parses"), id);
2507 assert_eq!(text.to_lowercase().parse::<RecordId>().expect("parses"), id);
2509 assert_eq!(
2510 RecordId::from_u128(0).to_string(),
2511 "00000000000000000000000000"
2512 );
2513 assert_eq!(
2514 RecordId::from_u128(u128::MAX).to_string(),
2515 "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"
2516 );
2517 }
2518
2519 #[test]
2520 fn given_an_id_in_json_when_encoded_then_should_use_readable_text() {
2521 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2522 let json = serde_json::to_string(&id).expect("serializes");
2523 assert_eq!(json, format!("\"{id}\""));
2524 assert_eq!(
2525 serde_json::from_str::<RecordId>(&json).expect("deserializes"),
2526 id
2527 );
2528 let byte_array = serde_json::to_string(&[0_u8; 16]).expect("serializes byte array");
2529 assert!(serde_json::from_str::<RecordId>(&byte_array).is_err());
2530 }
2531
2532 #[cfg(feature = "cbor")]
2533 #[test]
2534 fn given_an_id_in_named_cbor_when_encoded_then_should_use_fixed_width_bytes() {
2535 let id = RecordId::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
2536 let encoded = crate::framing::encode_named(&id).expect("serializes");
2537 let decoded = crate::framing::decode_named::<RecordId>(&encoded).expect("deserializes");
2538 assert_eq!(decoded, id);
2539 }
2540
2541 #[test]
2542 fn given_invalid_id_strings_when_parsed_then_should_reject_with_the_right_error() {
2543 assert_eq!(
2544 "short".parse::<RecordId>(),
2545 Err(IdParseError::Length { got: 5 })
2546 );
2547 assert_eq!(
2548 "8ZZZZZZZZZZZZZZZZZZZZZZZZZ".parse::<RecordId>(),
2549 Err(IdParseError::Overflow)
2550 );
2551 assert!(matches!(
2552 "UUUUUUUUUUUUUUUUUUUUUUUUUU".parse::<RecordId>(),
2553 Err(IdParseError::Char('U'))
2554 ));
2555 }
2556
2557 #[test]
2558 fn given_agent_id_strings_when_parsed_then_should_accept_printable_and_reject_control() {
2559 for s in ["planner", "planner@acme.example", "team/planner", "a:b"] {
2560 assert_eq!(
2561 s.parse::<AgentId>()
2562 .expect("a printable agent id is valid")
2563 .as_str(),
2564 s
2565 );
2566 }
2567 assert!("".parse::<AgentId>().is_err());
2568 assert!("bad\nid".parse::<AgentId>().is_err());
2569 }
2570
2571 #[test]
2572 fn given_task_state_codes_when_mapped_then_should_match_the_pinned_dictionary() {
2573 let expected = [
2574 (TaskState::Submitted, 1u8),
2575 (TaskState::Working, 2),
2576 (TaskState::InputRequired, 3),
2577 (TaskState::Completed, 4),
2578 (TaskState::Canceled, 5),
2579 (TaskState::Failed, 6),
2580 (TaskState::Rejected, 7),
2581 (TaskState::AuthRequired, 8),
2582 (TaskState::Unknown, 9),
2583 (TaskState::Paused, 10),
2584 ];
2585 for (state, code) in expected {
2586 assert_eq!(state.code(), code);
2587 assert_eq!(TaskState::from_code(code), state);
2588 }
2589 let future = TaskState::from_code(42);
2591 assert_eq!(future, TaskState::Unrecognized(42));
2592 assert_eq!(future.code(), 42);
2593 assert!(!future.is_terminal());
2594 assert!(TaskState::Completed.is_terminal());
2595 assert!(!TaskState::Working.is_terminal());
2596 assert!(!TaskState::Paused.is_terminal());
2597 assert_eq!(TaskState::Paused.to_string(), "paused");
2598 assert_eq!("paused".parse::<TaskState>(), Ok(TaskState::Paused));
2599 }
2600
2601 #[test]
2602 fn given_task_state_names_when_round_tripped_then_should_match_the_a2a_vocabulary() {
2603 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
2604 assert_eq!(
2605 "auth-required".parse::<TaskState>().expect("parses"),
2606 TaskState::AuthRequired
2607 );
2608 assert!("nope".parse::<TaskState>().is_err());
2609 }
2610
2611 #[test]
2612 fn given_error_and_dead_letter_codes_when_mapped_then_should_match_the_dictionaries() {
2613 assert_eq!(AgentErrorCode::InvalidRequest.code(), 1);
2614 assert_eq!(AgentErrorCode::Internal.code(), 7);
2615 assert_eq!(
2616 AgentErrorCode::from_code(99),
2617 AgentErrorCode::Unrecognized(99)
2618 );
2619 assert_eq!(DeadLetterReason::RetryExhausted.code(), 1);
2620 assert_eq!(DeadLetterReason::DeadlineExceeded.code(), 4);
2621 assert_eq!(
2622 DeadLetterReason::from_code(77),
2623 DeadLetterReason::Unrecognized(77)
2624 );
2625 }
2626
2627 #[test]
2628 fn given_every_u8_when_mapped_through_the_dictionaries_then_the_code_should_round_trip() {
2629 for code in 0u8..=u8::MAX {
2633 assert_eq!(TaskState::from_code(code).code(), code);
2634 assert_eq!(AgentErrorCode::from_code(code).code(), code);
2635 assert_eq!(DeadLetterReason::from_code(code).code(), code);
2636 }
2637 }
2638
2639 #[test]
2640 fn given_an_idempotency_key_when_validated_then_should_enforce_the_cap() {
2641 assert!("job-123-attempt-2".parse::<IdempotencyKey>().is_ok());
2642 assert!("".parse::<IdempotencyKey>().is_err());
2643 assert!("x".repeat(65).parse::<IdempotencyKey>().is_err());
2644 }
2645
2646 #[test]
2647 fn given_a_command_when_validated_then_should_pass_and_enforce_the_matrix() {
2648 let (record, conversation, source, correlation) = ids();
2649 let command =
2650 AgentEnvelope::command(record, conversation, source, correlation, b"do".to_vec());
2651 validate(&command).expect("a well-formed command validates");
2652
2653 let mut missing = command.clone();
2655 missing.correlation = None;
2656 assert_eq!(
2657 validate(&missing),
2658 Err(ValidateError::Missing {
2659 kind: AgentKind::Command,
2660 field: "correlation"
2661 })
2662 );
2663
2664 let with_usage = command.clone().with_usage(TokenUsage::default());
2666 assert!(matches!(
2667 validate(&with_usage),
2668 Err(ValidateError::Forbidden { field: "usage", .. })
2669 ));
2670
2671 let mut with_channel = command;
2673 with_channel.channel = Some(ChannelId::from_u128(1));
2674 assert!(matches!(
2675 validate(&with_channel),
2676 Err(ValidateError::Forbidden {
2677 field: "channel",
2678 ..
2679 })
2680 ));
2681 }
2682
2683 #[test]
2684 fn given_chunks_when_validated_then_should_enforce_stream_semantics() {
2685 let (_, conversation, source, correlation) = ids();
2686 let channel = ChannelId::from_u128(23);
2687 let chunk = AgentEnvelope::chunk(
2688 conversation,
2689 source.clone(),
2690 correlation,
2691 channel,
2692 0,
2693 b"tok".to_vec(),
2694 )
2695 .with_operation(OPERATION_CHAT);
2696 validate(&chunk).expect("a stream chunk validates");
2697
2698 let mut undeclared = chunk.clone();
2700 undeclared.operation = None;
2701 assert!(matches!(
2702 validate(&undeclared),
2703 Err(ValidateError::Missing {
2704 field: "operation",
2705 ..
2706 })
2707 ));
2708 let mut off_vocabulary = chunk.clone();
2709 off_vocabulary.operation = Some("telemetry".to_owned());
2710 assert!(matches!(
2711 validate(&off_vocabulary),
2712 Err(ValidateError::Invalid {
2713 field: "operation",
2714 ..
2715 })
2716 ));
2717
2718 let redeclared = AgentEnvelope::chunk(
2720 conversation,
2721 source.clone(),
2722 correlation,
2723 channel,
2724 3,
2725 b"tok".to_vec(),
2726 )
2727 .with_operation(OPERATION_REASONING);
2728 assert!(matches!(
2729 validate(&redeclared),
2730 Err(ValidateError::Invalid {
2731 field: "operation",
2732 ..
2733 })
2734 ));
2735
2736 let terminal = AgentEnvelope::chunk(
2738 conversation,
2739 source.clone(),
2740 correlation,
2741 channel,
2742 41,
2743 Vec::new(),
2744 )
2745 .terminal("stop")
2746 .with_usage(TokenUsage {
2747 input_tokens: 100,
2748 output_tokens: 42,
2749 ..Default::default()
2750 });
2751 validate(&terminal).expect("a terminal chunk validates");
2752
2753 let mut early_finish = chunk.clone();
2755 early_finish.finish_reason = Some("stop".to_owned());
2756 assert!(matches!(
2757 validate(&early_finish),
2758 Err(ValidateError::Invalid {
2759 field: "finish_reason",
2760 ..
2761 })
2762 ));
2763
2764 let empty = AgentEnvelope::chunk(
2766 conversation,
2767 source.clone(),
2768 correlation,
2769 channel,
2770 1,
2771 Vec::new(),
2772 );
2773 assert!(matches!(
2774 validate(&empty),
2775 Err(ValidateError::Missing { field: "body", .. })
2776 ));
2777
2778 let mut keyed = chunk.clone();
2780 keyed.idempotency_key = Some("k".parse().expect("valid key"));
2781 assert!(matches!(
2782 validate(&keyed),
2783 Err(ValidateError::Forbidden {
2784 field: "idempotency_key",
2785 ..
2786 })
2787 ));
2788
2789 let opening = chunk.with_deadline_micros(1);
2791 validate(&opening).expect("an opening chunk may declare the bound");
2792 let late = AgentEnvelope::chunk(
2793 conversation,
2794 source.clone(),
2795 correlation,
2796 channel,
2797 5,
2798 b"tok".to_vec(),
2799 )
2800 .with_deadline_micros(1);
2801 assert!(matches!(
2802 validate(&late),
2803 Err(ValidateError::Invalid {
2804 field: "deadline_micros",
2805 ..
2806 })
2807 ));
2808 }
2809
2810 #[test]
2811 fn given_status_signals_when_validated_then_task_updates_should_require_state() {
2812 let (record, conversation, source, correlation) = ids();
2813 let card = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_CARD);
2815 validate(&card).expect("a card validates");
2816
2817 let bare_task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK);
2819 assert!(matches!(
2820 validate(&bare_task),
2821 Err(ValidateError::Missing {
2822 field: "correlation",
2823 ..
2824 })
2825 ));
2826 let task = AgentEnvelope::status(record, conversation, source.clone(), OPERATION_TASK)
2827 .with_correlation(correlation)
2828 .with_task_state(TaskState::Working);
2829 validate(&task).expect("a task update validates");
2830
2831 let off_vocabulary =
2833 AgentEnvelope::status(record, conversation, source.clone(), "telemetry");
2834 assert!(matches!(
2835 validate(&off_vocabulary),
2836 Err(ValidateError::Invalid {
2837 field: "operation",
2838 ..
2839 })
2840 ));
2841 }
2842
2843 #[test]
2844 fn given_an_error_when_validated_then_last_should_be_forbidden() {
2845 let (record, conversation, source, correlation) = ids();
2846 let error =
2847 AgentEnvelope::error(record, conversation, source, correlation, b"boom".to_vec());
2848 validate(&error).expect("an error validates");
2849
2850 let mut flagged = error.clone();
2852 flagged.last = true;
2853 assert!(matches!(
2854 validate(&flagged),
2855 Err(ValidateError::Forbidden { field: "last", .. })
2856 ));
2857
2858 let mut dangling = error;
2860 dangling.sequence = Some(3);
2861 assert!(matches!(
2862 validate(&dangling),
2863 Err(ValidateError::Invalid {
2864 field: "sequence",
2865 ..
2866 })
2867 ));
2868 }
2869
2870 fn descriptor(skill_id: &str) -> CapabilityDescriptor {
2871 CapabilityDescriptor {
2872 skill_id: skill_id.to_owned(),
2873 input: None,
2874 output: None,
2875 cost_class: None,
2876 latency_class: None,
2877 max_concurrency: None,
2878 health: None,
2879 load: None,
2880 }
2881 }
2882
2883 #[test]
2884 fn given_an_agent_card_when_validated_then_should_enforce_the_caps() {
2885 let card = AgentCard {
2886 name: Some("rollout-planner".to_owned()),
2887 version: Some("1.4.2".to_owned()),
2888 capabilities: vec![
2889 CapabilityDescriptor {
2890 skill_id: "chat".to_owned(),
2891 input: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2892 output: Some(ContentRef::ContentType(crate::content::ContentType::Json)),
2893 cost_class: Some(2),
2894 latency_class: Some(1),
2895 max_concurrency: Some(8),
2896 health: Some(Health::Healthy),
2897 load: Some(250),
2898 },
2899 descriptor("plan_rollout"),
2900 ],
2901 ttl_micros: Some(30_000_000),
2902 };
2903 card.validate().expect("a well-formed card validates");
2904
2905 let mut crowded = card.clone();
2906 crowded.capabilities = vec![descriptor("x"); crate::limits::MAX_CARD_CAPABILITIES + 1];
2907 assert!(matches!(
2908 crowded.validate(),
2909 Err(ValidateError::TooLarge {
2910 field: "capabilities",
2911 ..
2912 })
2913 ));
2914
2915 let mut oversized = card;
2916 oversized.name = Some("n".repeat(crate::limits::MAX_AGENT_STRING_BYTES + 1));
2917 assert!(matches!(
2918 oversized.validate(),
2919 Err(ValidateError::TooLarge { field: "name", .. })
2920 ));
2921 }
2922
2923 #[cfg(feature = "cbor")]
2924 #[test]
2925 fn given_a_schema_id_named_like_a_content_type_when_round_tripped_then_should_stay_a_schema_id()
2926 {
2927 use crate::framing::{decode_named, encode_named};
2928 let reference = ContentRef::SchemaId("json".to_owned());
2931 let bytes = encode_named(&reference).expect("encodes");
2932 let back: ContentRef = decode_named(&bytes).expect("decodes");
2933 assert_eq!(back, ContentRef::SchemaId("json".to_owned()));
2934 let ct = ContentRef::ContentType(crate::content::ContentType::Json);
2936 let back: ContentRef = decode_named(&encode_named(&ct).expect("encodes")).expect("decodes");
2937 assert_eq!(back, ct);
2938 }
2939
2940 #[test]
2941 fn given_a_signature_when_validated_then_should_enforce_per_scheme_lengths() {
2942 let valid = Signature {
2943 scheme: SIGNATURE_SCHEME_ED25519,
2944 key_id: vec![1u8; 8],
2945 bytes: vec![2u8; 64],
2946 context: None,
2947 };
2948 valid
2949 .validate()
2950 .expect("a well-formed Ed25519 signature validates");
2951
2952 let mut short_key = valid.clone();
2953 short_key.key_id = vec![1u8; 4];
2954 assert!(matches!(
2955 short_key.validate(),
2956 Err(ValidateError::Invalid {
2957 field: "key_id",
2958 ..
2959 })
2960 ));
2961
2962 let mut short_signature = valid.clone();
2963 short_signature.bytes = vec![2u8; 32];
2964 assert!(matches!(
2965 short_signature.validate(),
2966 Err(ValidateError::Invalid { field: "bytes", .. })
2967 ));
2968
2969 let future = Signature {
2972 scheme: 42,
2973 key_id: vec![1u8; 3],
2974 bytes: vec![2u8; 99],
2975 context: None,
2976 };
2977 future.validate().expect("an unknown scheme flows through");
2978 }
2979
2980 #[test]
2981 fn given_a_body_ref_when_validated_then_should_enforce_reference_and_digest() {
2982 let valid = BodyRef::new("s3://transcripts/conv-1/msg-9", 4_194_304, [7u8; 32]);
2983 valid.validate().expect("a well-formed reference validates");
2984
2985 let mut empty = valid.clone();
2986 empty.reference = String::new();
2987 assert!(matches!(
2988 empty.validate(),
2989 Err(ValidateError::Invalid {
2990 field: "reference",
2991 ..
2992 })
2993 ));
2994
2995 let mut oversized = valid.clone();
2996 oversized.reference = "x".repeat(crate::limits::MAX_BODY_REFERENCE_BYTES + 1);
2997 assert!(matches!(
2998 oversized.validate(),
2999 Err(ValidateError::TooLarge {
3000 field: "reference",
3001 ..
3002 })
3003 ));
3004
3005 let mut truncated = valid;
3006 truncated.sha256 = vec![7u8; 16];
3007 assert!(matches!(
3008 truncated.validate(),
3009 Err(ValidateError::Invalid {
3010 field: "sha256",
3011 ..
3012 })
3013 ));
3014 }
3015
3016 #[test]
3017 fn given_oversized_metadata_when_validated_then_should_reject() {
3018 let (record, conversation, source, correlation) = ids();
3019 let mut command =
3020 AgentEnvelope::command(record, conversation, source, correlation, b"x".to_vec());
3021 for i in 0..crate::limits::MAX_METADATA_ENTRIES + 1 {
3022 command = command.with_metadata(format!("k{i}"), i as i64);
3023 }
3024 assert!(matches!(
3025 validate(&command),
3026 Err(ValidateError::TooLarge {
3027 field: "metadata",
3028 ..
3029 })
3030 ));
3031 }
3032
3033 fn ids() -> (RecordId, ConversationId, AgentId, CorrelationId) {
3034 (
3035 RecordId::from_u128(7),
3036 ConversationId::from_u128(11),
3037 "test-agent".parse().expect("valid agent id"),
3038 CorrelationId::from_u128(17),
3039 )
3040 }
3041}
3042
3043#[cfg(all(test, feature = "cbor"))]
3044mod wire_tests {
3045 use super::*;
3046 use crate::framing::{decode_named, encode_named};
3047
3048 #[test]
3049 fn given_a_wrong_length_locator_when_decoded_then_should_error_not_panic() {
3050 let mut valid = vec![0x40 | 20];
3053 valid.extend_from_slice(&[0u8; 20]);
3054 decode_named::<LogPosition>(&valid).expect("20 packed bytes decode");
3055
3056 for bad_len in [0u8, 19, 21, 23] {
3057 let mut bytes = vec![0x40 | bad_len];
3058 bytes.extend_from_slice(&vec![0u8; bad_len as usize]);
3059 assert!(
3060 decode_named::<LogPosition>(&bytes).is_err(),
3061 "a {bad_len}-byte locator must error, not panic"
3062 );
3063 }
3064 }
3065
3066 #[test]
3067 fn given_a_locator_when_round_tripped_then_should_preserve_every_field() {
3068 let pos = LogPosition::new(0x0102_0304, 0x0506_0708, 0x090A_0B0C, 0x0D0E_0F10_1112_1314);
3069 let bytes = encode_named(&pos).expect("encodes");
3070 let back: LogPosition = decode_named(&bytes).expect("decodes");
3071 assert_eq!(back, pos);
3072 }
3073
3074 #[test]
3075 fn given_an_envelope_when_round_tripped_then_should_preserve_every_field() {
3076 let envelope = AgentEnvelope::command(
3077 RecordId::from_u128(1),
3078 ConversationId::from_u128(2),
3079 "source-agent".parse().expect("valid agent id"),
3080 CorrelationId::from_u128(4),
3081 b"payload".to_vec(),
3082 )
3083 .with_target("target-agent".parse().expect("valid agent id"))
3084 .with_cause(RecordId::from_u128(6), Some(LogPosition::new(1, 2, 3, 44)))
3085 .with_idempotency_key("job-1".parse().expect("valid key"))
3086 .with_deadline_micros(1_700_000_000_000_000)
3087 .with_operation("chat")
3088 .with_tool("search")
3089 .with_metadata("pool", "eu-west");
3090 let bytes = encode_named(&envelope).expect("encodes");
3091 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3092 assert_eq!(back, envelope);
3093 }
3094
3095 #[test]
3096 fn given_a_must_understand_marker_when_round_tripped_then_should_preserve_bits_and_skip_zero() {
3097 let envelope = AgentEnvelope::event(
3101 RecordId::from_u128(1),
3102 ConversationId::from_u128(2),
3103 "source-agent".parse().expect("valid agent id"),
3104 b"e".to_vec(),
3105 )
3106 .requiring(0b101);
3107 let bytes = encode_named(&envelope).expect("encodes");
3108 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3109 assert_eq!(back.must_understand, 0b101);
3110 assert_eq!(back.unmet_requirements(0b001), 0b100);
3112 assert_eq!(back.unmet_requirements(0b111), 0);
3114 let plain = 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 assert_eq!(plain.unmet_requirements(features::NONE), 0);
3122 let json = serde_json::to_string(&plain).expect("json");
3123 assert!(
3124 !json.contains("must_understand"),
3125 "zero marker must be omitted: {json}"
3126 );
3127 }
3128
3129 #[test]
3130 fn given_absent_options_when_encoded_then_should_cost_zero_bytes() {
3131 let envelope = AgentEnvelope::event(
3134 RecordId::from_u128(1),
3135 ConversationId::from_u128(2),
3136 "source-agent".parse().expect("valid agent id"),
3137 b"e".to_vec(),
3138 );
3139 let bytes = encode_named(&envelope).expect("encodes");
3140 assert_eq!(bytes[0] & 0x0f, 5, "absent optionals must not be encoded");
3143 let back: AgentEnvelope = decode_named(&bytes).expect("decodes");
3144 assert!(!back.last);
3145 assert!(back.metadata.is_none());
3146 }
3147
3148 #[test]
3149 fn given_an_id_when_encoded_then_should_ride_as_one_fixed_width_byte_string() {
3150 let bytes = encode_named(&RecordId::from_u128(0x0102)).expect("encodes");
3151 assert_eq!(bytes.len(), 17);
3154 assert_eq!(bytes[0], 0x50);
3155 assert_eq!(bytes[16], 0x02);
3156 assert_eq!(bytes[15], 0x01);
3157 }
3158
3159 #[test]
3160 fn given_a_task_state_when_encoded_then_should_ride_as_a_bare_u8() {
3161 let bytes = encode_named(&TaskState::Completed).expect("encodes");
3162 assert_eq!(bytes, vec![4]);
3163 let unknown = encode_named(&42u8).expect("encodes a raw code");
3164 let back: TaskState = decode_named(&unknown).expect("unknown code decodes");
3165 assert_eq!(back, TaskState::Unrecognized(42));
3166 }
3167
3168 #[test]
3169 fn given_an_error_body_when_round_tripped_then_should_preserve_the_dictionary_code() {
3170 let body = AgentErrorBody {
3171 code: AgentErrorCode::ToolFailure,
3172 message: Some("search timed out".to_owned()),
3173 retryable: true,
3174 detail: Some(BTreeMap::from([("attempt".to_owned(), Value::Int(3))])),
3175 };
3176 let bytes = encode_named(&body).expect("encodes");
3177 let back: AgentErrorBody = decode_named(&bytes).expect("decodes");
3178 assert_eq!(back, body);
3179 }
3180
3181 #[test]
3182 fn given_a_body_ref_when_round_tripped_then_should_preserve_the_digest_as_a_byte_string() {
3183 let capsule = BodyRef::new("kv://bodies/abc", 1024, [9u8; 32]);
3184 let bytes = encode_named(&capsule).expect("encodes");
3185 let back: BodyRef = decode_named(&bytes).expect("decodes");
3186 assert_eq!(back, capsule);
3187 assert!(back.encryption.is_none(), "absent encryption costs nothing");
3188 back.validate().expect("decoded capsule validates");
3189 }
3190
3191 #[test]
3192 fn given_a_dead_letter_when_round_tripped_then_payload_should_stay_byte_identical() {
3193 let inner = AgentEnvelope::command(
3194 RecordId::from_u128(9),
3195 ConversationId::from_u128(8),
3196 "source-agent".parse().expect("valid agent id"),
3197 CorrelationId::from_u128(6),
3198 b"poison".to_vec(),
3199 );
3200 let payload = encode_named(&inner).expect("inner encodes");
3201 let capsule = AgentDeadLetter {
3202 source: LogPosition::new(1, 2, 3, 99),
3203 reason: DeadLetterReason::RetryExhausted,
3204 attempts: 5,
3205 detail: Some("handler kept failing".to_owned()),
3206 payload: payload.clone(),
3207 };
3208 let bytes = encode_named(&capsule).expect("encodes");
3209 let back: AgentDeadLetter = decode_named(&bytes).expect("decodes");
3210 assert_eq!(back.payload, payload, "redrive needs the original bytes");
3211 let redrive: AgentEnvelope = decode_named(&back.payload).expect("inner decodes");
3212 assert_eq!(redrive, inner);
3213 }
3214 #[test]
3215 fn given_a_task_status_when_validated_then_should_require_the_correlation() {
3216 let base = AgentEnvelope::status(
3218 RecordId::from_u128(1),
3219 ConversationId::from_u128(2),
3220 "runner".parse().expect("valid agent id"),
3221 OPERATION_TASK,
3222 )
3223 .with_task_state(TaskState::Working);
3224 assert!(
3225 matches!(
3226 validate(&base),
3227 Err(ValidateError::Missing {
3228 kind: AgentKind::Status,
3229 field: "correlation"
3230 })
3231 ),
3232 "a task status without a correlation is rejected"
3233 );
3234 let correlated = base.with_correlation(CorrelationId::from_u128(2));
3235 validate(&correlated)
3236 .expect("the run-lifecycle status validates once it correlates on the run");
3237 }
3238
3239 #[test]
3240 fn given_task_states_when_displayed_and_parsed_then_should_use_the_pinned_kebab_words() {
3241 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
3242 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
3243 assert_eq!(TaskState::Unrecognized(42).to_string(), "unrecognized-42");
3244 assert_eq!(
3245 "input-required".parse::<TaskState>().expect("parses"),
3246 TaskState::InputRequired
3247 );
3248 assert!("bogus".parse::<TaskState>().is_err());
3249 }
3250}