Skip to main content

agent_sdk_core/domain/
ids.rs

1//! Domain primitives for stable SDK vocabulary. Use these items for IDs, refs,
2//! policy, privacy, trust, and errors that cross crate or host boundaries. They are
3//! data-only and must not perform provider, filesystem, network, or UI side effects.
4//! This file contains the ids portion of that contract.
5//!
6use core::fmt;
7use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
8
9/// Constant value for the domain::ids contract. Use it to keep SDK
10/// records and tests aligned on the same stable value.
11pub const MAX_ID_LEN: usize = 512;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14/// Enumerates the finite id validation error cases.
15/// Serialized names are part of the SDK contract; update fixtures when variants change.
16pub enum IdValidationError {
17    /// Use this variant when the contract needs to represent empty; selecting it has no side effect by itself.
18    Empty,
19    /// Use this variant when the contract needs to represent too long; selecting it has no side effect by itself.
20    TooLong {
21        /// Max used by this record or request.
22        max: usize,
23        /// Actual used by this record or request.
24        actual: usize,
25    },
26    /// Use this variant when the contract needs to represent control character; selecting it has no side effect by itself.
27    ControlCharacter {
28        /// Index used by this record or request.
29        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
52/// Validates the domain::ids invariants and returns a typed error on
53/// failure. Validation is pure and does not perform I/O, dispatch,
54/// journal appends, or adapter calls.
55pub(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            /// Creates a new domain::ids value with explicit
88            /// caller-provided inputs. This constructor is data-only
89            /// and performs no I/O or external side effects.
90            ///
91            /// # Panics
92            ///
93            /// Panics if constructor invariants fail, such as invalid identifier
94            /// text or constructor-specific bounds. Use a fallible constructor such as
95            /// `try_new` when one is available for untrusted input.
96            pub fn new(value: impl Into<String>) -> Self {
97                Self::try_new(value).expect(concat!(stringify!($name), " must be valid"))
98            }
99
100            /// Creates a new domain::ids value after validation.
101            /// Returns an SDK error instead of panicking when the
102            /// identifier or input does not satisfy the contract.
103            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            /// Returns this value as str. The accessor is side-effect
110            /// free and keeps ownership with the caller.
111            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)]
183/// Defines the journal cursor SDK value.
184/// Construction records local state only; documented runtimes, executors, or ports own side effects.
185pub struct JournalCursor(String);
186
187impl JournalCursor {
188    /// Creates a new domain::ids value with explicit caller-provided
189    /// inputs. This constructor is data-only and performs no I/O or
190    /// external side effects.
191    ///
192    /// # Panics
193    ///
194    /// Panics if constructor invariants fail, such as invalid identifier
195    /// text or constructor-specific bounds. Use a fallible constructor such as
196    /// `try_new` when one is available for untrusted input.
197    pub fn new(value: impl Into<String>) -> Self {
198        Self::try_new(value).expect("JournalCursor must be valid")
199    }
200
201    /// Creates a new domain::ids value after validation. Returns an SDK
202    /// error instead of panicking when the identifier or input does not
203    /// satisfy the contract.
204    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    /// Returns this value as str. The accessor is side-effect free and
211    /// keeps ownership with the caller.
212    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)]
240/// Defines the correlation entry SDK value.
241/// Construction records local state only; documented runtimes, executors, or ports own side effects.
242pub struct CorrelationEntry {
243    /// Key used by this record or request.
244    pub key: CorrelationKey,
245    /// Value used by this record or request.
246    pub value: CorrelationValue,
247}