1use serde::{Deserialize, Serialize};
2use std::fmt::{self, Display, Formatter};
3use std::str::FromStr;
4use ulid::Ulid;
5
6const MAX_ID_LEN: usize = 255;
7
8const DERIVE_VERSION: u8 = 1;
11const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
12const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16pub enum IdError {
17 #[error("identifier must not be empty")]
18 Empty,
19 #[error("identifier length {got}B exceeds max {max}B")]
20 TooLong { got: usize, max: usize },
21 #[error("identifier contains invalid character `{0}`")]
22 InvalidChar(char),
23 #[error("invalid ULID `{0}`")]
24 InvalidUlid(String),
25 #[error("invalid message id `{0}`, expected `<partition_id>:<offset>`")]
26 InvalidMessageId(String),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(try_from = "String", into = "String")]
35pub struct ConversationId(Ulid);
36
37impl ConversationId {
38 pub fn new() -> Self {
40 Self(Ulid::generate())
41 }
42
43 pub fn derive(seed: &str) -> Self {
47 let high = hash_with(0x1d, seed);
48 let low = hash_with(0x9e, seed);
49 Self(Ulid((u128::from(high) << 64) | u128::from(low)))
50 }
51}
52
53fn hash_with(salt: u8, seed: &str) -> u64 {
54 let mut hash = FNV_OFFSET;
55 for byte in [DERIVE_VERSION, salt].into_iter().chain(seed.bytes()) {
56 hash ^= u64::from(byte);
57 hash = hash.wrapping_mul(FNV_PRIME);
58 }
59 hash
60}
61
62impl Default for ConversationId {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl Display for ConversationId {
69 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
70 Display::fmt(&self.0, f)
71 }
72}
73
74impl FromStr for ConversationId {
75 type Err = IdError;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 Ulid::from_string(s)
79 .map(Self)
80 .map_err(|_| IdError::InvalidUlid(s.to_owned()))
81 }
82}
83
84impl TryFrom<String> for ConversationId {
85 type Error = IdError;
86
87 fn try_from(s: String) -> Result<Self, Self::Error> {
88 s.parse()
89 }
90}
91
92impl From<ConversationId> for String {
93 fn from(id: ConversationId) -> Self {
94 id.to_string()
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(try_from = "String", into = "String")]
103pub struct IntentId(Ulid);
104
105impl IntentId {
106 pub fn new() -> Self {
108 Self(Ulid::generate())
109 }
110}
111
112impl Default for IntentId {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl Display for IntentId {
119 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
120 Display::fmt(&self.0, f)
121 }
122}
123
124impl FromStr for IntentId {
125 type Err = IdError;
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
128 Ulid::from_string(s)
129 .map(Self)
130 .map_err(|_| IdError::InvalidUlid(s.to_owned()))
131 }
132}
133
134impl TryFrom<String> for IntentId {
135 type Error = IdError;
136
137 fn try_from(s: String) -> Result<Self, Self::Error> {
138 s.parse()
139 }
140}
141
142impl From<IntentId> for String {
143 fn from(id: IntentId) -> Self {
144 id.to_string()
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
155#[serde(try_from = "String", into = "String")]
156pub struct AgentId(String);
157
158impl AgentId {
159 pub fn new(name: impl Into<String>) -> Result<Self, IdError> {
163 let name = name.into();
164 validate_id(&name)?;
165 Ok(Self(name))
166 }
167
168 pub fn as_str(&self) -> &str {
170 &self.0
171 }
172}
173
174impl Display for AgentId {
175 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
176 f.write_str(&self.0)
177 }
178}
179
180impl FromStr for AgentId {
181 type Err = IdError;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 Self::new(s)
185 }
186}
187
188impl TryFrom<&str> for AgentId {
189 type Error = IdError;
190
191 fn try_from(s: &str) -> Result<Self, Self::Error> {
192 Self::new(s)
193 }
194}
195
196impl TryFrom<String> for AgentId {
197 type Error = IdError;
198
199 fn try_from(s: String) -> Result<Self, Self::Error> {
200 Self::new(s)
201 }
202}
203
204impl From<AgentId> for String {
205 fn from(id: AgentId) -> Self {
206 id.0
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
214pub struct ConsumerGroupName(String);
215
216impl ConsumerGroupName {
217 pub fn new(name: impl Into<String>) -> Result<Self, IdError> {
219 let name = name.into();
220 validate_id(&name)?;
221 Ok(Self(name))
222 }
223
224 pub fn for_agent(agent: &AgentId) -> Self {
230 debug_assert!(validate_id(agent.as_str()).is_ok());
231 Self(agent.as_str().to_owned())
232 }
233
234 pub fn as_str(&self) -> &str {
235 &self.0
236 }
237}
238
239impl Display for ConsumerGroupName {
240 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
241 f.write_str(&self.0)
242 }
243}
244
245impl FromStr for ConsumerGroupName {
246 type Err = IdError;
247
248 fn from_str(s: &str) -> Result<Self, Self::Err> {
249 Self::new(s)
250 }
251}
252
253impl From<&AgentId> for ConsumerGroupName {
254 fn from(agent: &AgentId) -> Self {
255 Self::for_agent(agent)
256 }
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
265pub struct PrincipalId(u32);
266
267impl PrincipalId {
268 pub const fn new(value: u32) -> Self {
269 Self(value)
270 }
271
272 pub const fn get(self) -> u32 {
273 self.0
274 }
275}
276
277impl Display for PrincipalId {
278 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
279 Display::fmt(&self.0, f)
280 }
281}
282
283impl From<u32> for PrincipalId {
284 fn from(value: u32) -> Self {
285 Self::new(value)
286 }
287}
288
289impl From<PrincipalId> for u32 {
290 fn from(value: PrincipalId) -> Self {
291 value.get()
292 }
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
299pub struct MessageId {
300 pub partition_id: u32,
302 pub offset: u64,
304}
305
306impl MessageId {
307 pub fn new(partition_id: u32, offset: u64) -> Self {
309 Self {
310 partition_id,
311 offset,
312 }
313 }
314}
315
316impl Display for MessageId {
317 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
318 write!(f, "{}:{}", self.partition_id, self.offset)
319 }
320}
321
322impl FromStr for MessageId {
323 type Err = IdError;
324
325 fn from_str(s: &str) -> Result<Self, Self::Err> {
326 let (partition, offset) = s
327 .split_once(':')
328 .ok_or_else(|| IdError::InvalidMessageId(s.to_owned()))?;
329 if !is_canonical_digits(partition) || !is_canonical_digits(offset) {
334 return Err(IdError::InvalidMessageId(s.to_owned()));
335 }
336 Ok(Self {
337 partition_id: partition
338 .parse()
339 .map_err(|_| IdError::InvalidMessageId(s.to_owned()))?,
340 offset: offset
341 .parse()
342 .map_err(|_| IdError::InvalidMessageId(s.to_owned()))?,
343 })
344 }
345}
346
347fn is_canonical_digits(s: &str) -> bool {
351 let payload = s.as_bytes();
352 if payload.is_empty() {
353 return false;
354 }
355 if payload.len() > 1 && payload[0] == b'0' {
356 return false;
357 }
358 payload.iter().all(u8::is_ascii_digit)
359}
360
361fn validate_id(s: &str) -> Result<(), IdError> {
362 if s.is_empty() {
363 return Err(IdError::Empty);
364 }
365 if s.len() > MAX_ID_LEN {
366 return Err(IdError::TooLong {
367 got: s.len(),
368 max: MAX_ID_LEN,
369 });
370 }
371 if let Some(c) = s.chars().find(|c| c.is_control()) {
375 return Err(IdError::InvalidChar(c));
376 }
377 Ok(())
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn given_a_conversation_id_when_round_tripped_through_a_string_then_should_be_equal() {
386 let id = ConversationId::new();
387 let parsed = id
388 .to_string()
389 .parse::<ConversationId>()
390 .expect("a formatted conversation id should parse");
391 assert_eq!(parsed, id);
392 }
393
394 #[test]
395 fn given_seeds_when_deriving_conversation_ids_then_should_be_stable_and_distinct() {
396 assert_eq!(
397 ConversationId::derive("user-1"),
398 ConversationId::derive("user-1")
399 );
400 assert_ne!(
401 ConversationId::derive("user-1"),
402 ConversationId::derive("user-2")
403 );
404 }
405
406 #[test]
410 fn given_a_known_seed_when_derived_then_should_match_the_pinned_id() {
411 assert_eq!(
412 ConversationId::derive("user-1").to_string(),
413 "6X4VM88293CP9BFK3H58TFMMS7"
414 );
415 }
416
417 #[test]
418 fn given_an_invalid_string_when_parsing_a_conversation_id_then_should_error() {
419 assert!(matches!(
420 "not-a-ulid".parse::<ConversationId>(),
421 Err(IdError::InvalidUlid(_))
422 ));
423 }
424
425 #[test]
426 fn given_agent_id_strings_when_parsing_then_should_accept_valid_and_reject_invalid() {
427 let valid = "executor-v1"
428 .parse::<AgentId>()
429 .expect("a valid agent id should parse");
430 assert_eq!(valid.as_str(), "executor-v1");
431 assert_eq!("".parse::<AgentId>(), Err(IdError::Empty));
432 for s in [
435 "bad id",
436 "planner+eu@acme.example",
437 "team/planner",
438 "a:b",
439 "https://acme.example/agents/planner",
440 ] {
441 assert_eq!(
442 s.parse::<AgentId>()
443 .expect("a printable agent id is valid")
444 .as_str(),
445 s
446 );
447 }
448 assert_eq!(
450 "bad\nid".parse::<AgentId>(),
451 Err(IdError::InvalidChar('\n'))
452 );
453 }
454
455 #[test]
456 fn given_a_message_id_when_round_tripped_then_should_be_equal() {
457 let id = MessageId::new(3, 42);
458 assert_eq!(id.to_string(), "3:42");
459 assert_eq!(
460 "3:42"
461 .parse::<MessageId>()
462 .expect("a formatted message id should parse"),
463 id
464 );
465 assert!(matches!(
466 "nope".parse::<MessageId>(),
467 Err(IdError::InvalidMessageId(_))
468 ));
469 }
470
471 #[test]
472 fn given_an_agent_when_deriving_its_default_group_then_should_keep_distinct_types() {
473 let agent = AgentId::new("planner").expect("planner is a valid agent id");
474 let group = ConsumerGroupName::for_agent(&agent);
475 assert_eq!(group.as_str(), agent.as_str());
476 assert_eq!(group.to_string(), "planner");
477 }
478
479 #[test]
480 fn given_a_principal_when_converted_then_should_preserve_the_server_id() {
481 let principal = PrincipalId::new(42);
482 assert_eq!(principal.get(), 42);
483 assert_eq!(u32::from(principal), 42);
484 assert_eq!(principal.to_string(), "42");
485 }
486
487 #[test]
488 fn given_non_canonical_message_id_strings_when_parsed_then_should_reject() {
489 for bad in [
490 "+3:42", " 3:42", "3:42 ", "03:42", "3:042", "3:", ":42", "::", "3", "",
491 ] {
492 assert!(
493 bad.parse::<MessageId>().is_err(),
494 "expected `{bad}` to be rejected by MessageId::from_str",
495 );
496 }
497 }
498
499 #[test]
500 fn given_a_message_id_shaped_string_when_used_as_agent_id_then_should_accept() {
501 let id = AgentId::new("3:42").expect("`3:42` is a valid agent id");
502 assert_eq!(id.as_str(), "3:42");
503 }
504
505 #[test]
506 fn given_a_control_character_when_constructing_agent_id_then_should_reject() {
507 assert_eq!(AgentId::new("a\tb"), Err(IdError::InvalidChar('\t')));
508 assert_eq!(AgentId::new(""), Err(IdError::Empty));
509 }
510}
511
512pub trait MintUlid: Sized + From<u128> {
520 fn mint() -> Self {
522 Self::from(Ulid::generate().0)
523 }
524}
525
526impl MintUlid for laser_wire::agent::RecordId {}
527impl MintUlid for laser_wire::agent::ConversationId {}
528impl MintUlid for laser_wire::agent::CorrelationId {}
529impl MintUlid for laser_wire::agent::ChannelId {}
530impl MintUlid for laser_wire::query::QueryExecutionId {}
531impl MintUlid for laser_wire::destination::DestinationId {}
532impl MintUlid for laser_wire::destination::DestinationOperationId {}
533impl MintUlid for laser_wire::destination::BackendResourceId {}
534impl MintUlid for laser_wire::destination::QueryRouteId {}
535impl MintUlid for laser_wire::checkpoint::CheckpointOwnerId {}
536impl MintUlid for laser_wire::checkpoint::PreparedAttemptId {}
537impl MintUlid for laser_wire::checkpoint::CheckpointRequestId {}
538
539impl ConversationId {
540 pub fn as_u128(&self) -> u128 {
542 self.0.0
543 }
544}
545
546impl From<ConversationId> for laser_wire::agent::ConversationId {
547 fn from(id: ConversationId) -> Self {
548 Self::from_u128(id.as_u128())
549 }
550}
551
552impl From<laser_wire::agent::ConversationId> for ConversationId {
553 fn from(id: laser_wire::agent::ConversationId) -> Self {
554 Self(Ulid(id.as_u128()))
555 }
556}
557
558impl AgentId {
559 pub fn wire_id(&self) -> laser_wire::agent::AgentId {
563 laser_wire::agent::AgentId::from_str(&self.0)
564 .expect("a valid SDK agent id is a valid wire agent id")
565 }
566}
567
568impl From<AgentId> for laser_wire::agent::AgentId {
571 fn from(id: AgentId) -> Self {
572 id.wire_id()
573 }
574}
575
576impl From<&AgentId> for laser_wire::agent::AgentId {
577 fn from(id: &AgentId) -> Self {
578 id.wire_id()
579 }
580}
581
582#[cfg(test)]
583mod agdx_bridge_tests {
584 use super::*;
585
586 #[test]
587 fn given_a_conversation_id_when_bridged_to_the_wire_id_then_should_round_trip() {
588 let id = ConversationId::new();
589 let wire: laser_wire::agent::ConversationId = id.into();
590 let back: ConversationId = wire.into();
591 assert_eq!(back, id);
592 assert_eq!(wire.to_string(), id.to_string());
594 }
595
596 #[test]
597 fn given_an_agent_name_when_derived_then_wire_id_should_be_stable_and_distinct() {
598 let planner: AgentId = "planner".parse().expect("valid agent id");
599 let executor: AgentId = "executor".parse().expect("valid agent id");
600 assert_eq!(planner.wire_id(), planner.wire_id());
601 assert_ne!(planner.wire_id(), executor.wire_id());
602 }
603
604 #[test]
607 fn given_an_agent_name_when_taken_to_the_wire_then_should_be_the_same_string() {
608 let planner: AgentId = "planner".parse().expect("valid agent id");
609 assert_eq!(planner.wire_id().to_string(), "planner");
610 }
611
612 #[test]
613 fn given_minted_wire_ids_when_compared_then_should_be_distinct() {
614 use laser_wire::agent::RecordId;
615 assert_ne!(RecordId::mint(), RecordId::mint());
616 }
617}