agent_sdk_core/domain/
ids.rs1use core::fmt;
7use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
8
9pub const MAX_ID_LEN: usize = 512;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum IdValidationError {
17 Empty,
19 TooLong {
21 max: usize,
23 actual: usize,
25 },
26 ControlCharacter {
28 index: usize,
30 },
31}
32
33impl fmt::Display for IdValidationError {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Empty => formatter.write_str("identifier is empty"),
37 Self::TooLong { max, actual } => {
38 write!(formatter, "identifier length {actual} exceeds max {max}")
39 }
40 Self::ControlCharacter { index } => {
41 write!(
42 formatter,
43 "identifier contains control character at byte {index}"
44 )
45 }
46 }
47 }
48}
49
50impl std::error::Error for IdValidationError {}
51
52pub(crate) fn validate_identifier(value: &str) -> Result<(), IdValidationError> {
56 if value.is_empty() {
57 return Err(IdValidationError::Empty);
58 }
59 if value.len() > MAX_ID_LEN {
60 return Err(IdValidationError::TooLong {
61 max: MAX_ID_LEN,
62 actual: value.len(),
63 });
64 }
65 if let Some((index, _)) = value
66 .char_indices()
67 .find(|(_, character)| character.is_control())
68 {
69 return Err(IdValidationError::ControlCharacter { index });
70 }
71 Ok(())
72}
73
74macro_rules! id_newtype {
75 ($name:ident) => {
76 #[doc = concat!(
77 "Typed SDK identifier for `",
78 stringify!($name),
79 "`. Use this newtype at public boundaries instead of a raw string; ",
80 "constructing or cloning it is data-only and performs no side effects."
81 )]
82 #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
83 #[serde(transparent)]
84 pub struct $name(String);
85
86 impl $name {
87 pub fn new(value: impl Into<String>) -> Self {
97 Self::try_new(value).expect(concat!(stringify!($name), " must be valid"))
98 }
99
100 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
104 let value = value.into();
105 validate_identifier(&value)?;
106 Ok(Self(value))
107 }
108
109 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114 }
115
116 impl From<&str> for $name {
117 fn from(value: &str) -> Self {
118 Self::try_new(value).expect(concat!(stringify!($name), " must be valid"))
119 }
120 }
121
122 impl<'de> Deserialize<'de> for $name {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: Deserializer<'de>,
126 {
127 let value = String::deserialize(deserializer)?;
128 Self::try_new(value).map_err(D::Error::custom)
129 }
130 }
131
132 impl fmt::Debug for $name {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter.write_str(concat!(stringify!($name), "(redacted)"))
135 }
136 }
137
138 impl fmt::Display for $name {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter.write_str(concat!(stringify!($name), "(redacted)"))
141 }
142 }
143 };
144}
145
146id_newtype!(AgentId);
147id_newtype!(AgentPoolId);
148id_newtype!(RunId);
149id_newtype!(TopicId);
150id_newtype!(TurnId);
151id_newtype!(AttemptId);
152id_newtype!(EventId);
153id_newtype!(MessageId);
154id_newtype!(WakeConditionId);
155id_newtype!(ApprovalRequestId);
156id_newtype!(OutputSchemaId);
157id_newtype!(ValidatedOutputId);
158id_newtype!(ValidationAttemptId);
159id_newtype!(RepairAttemptId);
160id_newtype!(ContextItemId);
161id_newtype!(ContextProjectionId);
162id_newtype!(RuntimePackageId);
163id_newtype!(EffectId);
164id_newtype!(ToolCallId);
165id_newtype!(SpanId);
166id_newtype!(TraceId);
167id_newtype!(SessionId);
168id_newtype!(ContentRef);
169id_newtype!(ArtifactRef);
170id_newtype!(ContentId);
171id_newtype!(ArtifactId);
172id_newtype!(LineageId);
173id_newtype!(IdempotencyKey);
174id_newtype!(DedupeKey);
175id_newtype!(CorrelationKey);
176id_newtype!(CorrelationValue);
177id_newtype!(EventCursorId);
178id_newtype!(JournalCursorId);
179id_newtype!(ArchiveCursorId);
180
181#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
182#[serde(transparent)]
183pub struct JournalCursor(String);
186
187impl JournalCursor {
188 pub fn new(value: impl Into<String>) -> Self {
198 Self::try_new(value).expect("JournalCursor must be valid")
199 }
200
201 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
205 let value = value.into();
206 validate_identifier(&value)?;
207 Ok(Self(value))
208 }
209
210 pub fn as_str(&self) -> &str {
213 &self.0
214 }
215}
216
217impl<'de> Deserialize<'de> for JournalCursor {
218 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219 where
220 D: Deserializer<'de>,
221 {
222 let value = String::deserialize(deserializer)?;
223 Self::try_new(value).map_err(D::Error::custom)
224 }
225}
226
227impl fmt::Debug for JournalCursor {
228 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229 formatter.write_str("JournalCursor(redacted)")
230 }
231}
232
233impl fmt::Display for JournalCursor {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 formatter.write_str("JournalCursor(redacted)")
236 }
237}
238
239#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
240pub struct CorrelationEntry {
243 pub key: CorrelationKey,
245 pub value: CorrelationValue,
247}