1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt;
7use std::hash::Hash;
8use std::str::FromStr;
9use uuid::Uuid;
10
11pub trait EntityIdType:
20 Copy
21 + Clone
22 + Eq
23 + PartialEq
24 + Hash
25 + fmt::Debug
26 + fmt::Display
27 + FromStr
28 + Serialize
29 + serde::de::DeserializeOwned
30 + Send
31 + Sync
32 + 'static
33{
34 const ENTITY_NAME: &'static str;
36
37 fn new(uuid: Uuid) -> Self;
39
40 fn as_uuid(&self) -> Uuid;
42
43 fn nil() -> Self {
45 Self::new(Uuid::nil())
46 }
47
48 fn now_v7() -> Self {
50 Self::new(Uuid::now_v7())
51 }
52
53 fn new_v4() -> Self {
55 Self::new(Uuid::new_v4())
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct EntityIdParseError {
62 pub entity_name: &'static str,
63 pub input: String,
64 pub source: uuid::Error,
65}
66
67impl fmt::Display for EntityIdParseError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(
70 f,
71 "Failed to parse {} ID from '{}': {}",
72 self.entity_name, self.input, self.source
73 )
74 }
75}
76
77impl std::error::Error for EntityIdParseError {
78 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79 Some(&self.source)
80 }
81}
82
83macro_rules! define_entity_id {
88 ($name:ident, $entity:literal, $doc:literal) => {
89 #[doc = $doc]
90 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
91 #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
92 pub struct $name(Uuid);
93
94 impl EntityIdType for $name {
95 const ENTITY_NAME: &'static str = $entity;
96
97 fn new(uuid: Uuid) -> Self {
98 Self(uuid)
99 }
100
101 fn as_uuid(&self) -> Uuid {
102 self.0
103 }
104 }
105
106 impl fmt::Debug for $name {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(f, "{}({})", stringify!($name), self.0)
109 }
110 }
111
112 impl fmt::Display for $name {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "{}", self.0)
115 }
116 }
117
118 impl FromStr for $name {
119 type Err = EntityIdParseError;
120
121 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 Uuid::from_str(s)
123 .map(Self::new)
124 .map_err(|e| EntityIdParseError {
125 entity_name: Self::ENTITY_NAME,
126 input: s.to_string(),
127 source: e,
128 })
129 }
130 }
131
132 impl Default for $name {
133 fn default() -> Self {
134 Self::nil()
135 }
136 }
137
138 impl Serialize for $name {
139 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
140 where
141 S: serde::Serializer,
142 {
143 self.0.serialize(serializer)
145 }
146 }
147
148 impl<'de> Deserialize<'de> for $name {
149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150 where
151 D: serde::Deserializer<'de>,
152 {
153 Uuid::deserialize(deserializer).map(Self::new)
155 }
156 }
157 };
158}
159
160define_entity_id!(TenantId, "tenant", "Type-safe ID for tenant entities.");
165define_entity_id!(
166 TrajectoryId,
167 "trajectory",
168 "Type-safe ID for trajectory entities."
169);
170define_entity_id!(ScopeId, "scope", "Type-safe ID for scope entities.");
171define_entity_id!(
172 ArtifactId,
173 "artifact",
174 "Type-safe ID for artifact entities."
175);
176define_entity_id!(NoteId, "note", "Type-safe ID for note entities.");
177define_entity_id!(TurnId, "turn", "Type-safe ID for turn entities.");
178define_entity_id!(AgentId, "agent", "Type-safe ID for agent entities.");
179define_entity_id!(
180 PrincipalId,
181 "principal",
182 "Type-safe ID for principal entities."
183);
184define_entity_id!(
185 WorkingSetId,
186 "working_set",
187 "Type-safe ID for agent working set entities."
188);
189define_entity_id!(EdgeId, "edge", "Type-safe ID for edge entities.");
190define_entity_id!(LockId, "lock", "Type-safe ID for lock entities.");
191define_entity_id!(MessageId, "message", "Type-safe ID for message entities.");
192define_entity_id!(
193 DelegationId,
194 "delegation",
195 "Type-safe ID for delegation entities."
196);
197define_entity_id!(HandoffId, "handoff", "Type-safe ID for handoff entities.");
198define_entity_id!(ApiKeyId, "api_key", "Type-safe ID for API key entities.");
199define_entity_id!(WebhookId, "webhook", "Type-safe ID for webhook entities.");
200define_entity_id!(
201 SummarizationPolicyId,
202 "summarization_policy",
203 "Type-safe ID for summarization policy entities."
204);
205define_entity_id!(
206 SummarizationRequestId,
207 "summarization_request",
208 "Type-safe ID for summarization request entities."
209);
210define_entity_id!(
211 ToolExecutionId,
212 "tool_execution",
213 "Type-safe ID for tool execution entities."
214);
215define_entity_id!(
216 ConflictId,
217 "conflict",
218 "Type-safe ID for conflict entities."
219);
220define_entity_id!(
221 PackConfigId,
222 "pack_config",
223 "Type-safe ID for pack configuration entities."
224);
225define_entity_id!(EventId, "event", "Type-safe ID for event entities.");
226define_entity_id!(FlowId, "flow", "Type-safe ID for flow entities.");
227define_entity_id!(
228 FlowStepId,
229 "flow_step",
230 "Type-safe ID for flow step entities."
231);
232define_entity_id!(
233 SnapshotId,
234 "snapshot",
235 "Type-safe ID for snapshot entities."
236);
237
238define_entity_id!(GoalId, "goal", "Type-safe ID for agent goal entities.");
240define_entity_id!(PlanId, "plan", "Type-safe ID for agent plan entities.");
241define_entity_id!(
242 ActionId,
243 "action",
244 "Type-safe ID for agent action entities."
245);
246define_entity_id!(StepId, "step", "Type-safe ID for plan step entities.");
247define_entity_id!(
248 ObservationId,
249 "observation",
250 "Type-safe ID for agent observation entities."
251);
252define_entity_id!(
253 BeliefId,
254 "belief",
255 "Type-safe ID for agent belief entities."
256);
257define_entity_id!(
258 LearningId,
259 "learning",
260 "Type-safe ID for agent learning entities."
261);
262
263define_entity_id!(TeamId, "team", "Type-safe ID for team entities.");
265
266define_entity_id!(
268 SessionId,
269 "session",
270 "Type-safe ID for stateful LLM session entities."
271);
272
273define_entity_id!(
275 InstanceId,
276 "instance",
277 "Type-safe ID for API server instance entities."
278);
279
280pub type Timestamp = DateTime<Utc>;
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
289#[serde(transparent)]
290#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
291pub struct DurationMs(i64);
292
293impl DurationMs {
294 pub const fn new(ms: i64) -> Self {
296 Self(ms)
297 }
298
299 pub const fn as_millis(&self) -> i64 {
301 self.0
302 }
303
304 pub fn as_duration(&self) -> std::time::Duration {
306 std::time::Duration::from_millis(self.0.max(0) as u64)
307 }
308}
309
310impl fmt::Display for DurationMs {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "{}ms", self.0)
313 }
314}
315
316impl From<i64> for DurationMs {
317 fn from(ms: i64) -> Self {
318 Self(ms)
319 }
320}
321
322impl From<DurationMs> for i64 {
323 fn from(d: DurationMs) -> Self {
324 d.0
325 }
326}
327
328#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
330pub struct ContentHash([u8; 32]);
331
332impl ContentHash {
333 pub const fn new(bytes: [u8; 32]) -> Self {
335 Self(bytes)
336 }
337
338 pub fn as_bytes(&self) -> &[u8; 32] {
340 &self.0
341 }
342
343 pub fn as_slice(&self) -> &[u8] {
345 &self.0
346 }
347}
348
349impl std::ops::Deref for ContentHash {
350 type Target = [u8; 32];
351
352 fn deref(&self) -> &Self::Target {
353 &self.0
354 }
355}
356
357impl AsRef<[u8]> for ContentHash {
358 fn as_ref(&self) -> &[u8] {
359 &self.0
360 }
361}
362
363impl From<[u8; 32]> for ContentHash {
364 fn from(bytes: [u8; 32]) -> Self {
365 Self(bytes)
366 }
367}
368
369impl From<ContentHash> for [u8; 32] {
370 fn from(hash: ContentHash) -> Self {
371 hash.0
372 }
373}
374
375impl fmt::Debug for ContentHash {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 write!(f, "ContentHash({})", hex::encode(self.0))
378 }
379}
380
381impl fmt::Display for ContentHash {
382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 write!(f, "{}", hex::encode(self.0))
384 }
385}
386
387impl Serialize for ContentHash {
388 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
389 where
390 S: serde::Serializer,
391 {
392 if serializer.is_human_readable() {
393 hex::encode(self.0).serialize(serializer)
394 } else {
395 self.0.serialize(serializer)
396 }
397 }
398}
399
400impl<'de> Deserialize<'de> for ContentHash {
401 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402 where
403 D: serde::Deserializer<'de>,
404 {
405 if deserializer.is_human_readable() {
406 struct ContentHashVisitor;
407
408 impl<'de> serde::de::Visitor<'de> for ContentHashVisitor {
409 type Value = ContentHash;
410
411 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
412 formatter.write_str("a hex string or byte array of length 32")
413 }
414
415 fn visit_str<E>(self, v: &str) -> Result<ContentHash, E>
416 where
417 E: serde::de::Error,
418 {
419 let v = v.strip_prefix("\\x").unwrap_or(v);
420 let bytes = hex::decode(v).map_err(serde::de::Error::custom)?;
421 let arr: [u8; 32] = bytes
422 .try_into()
423 .map_err(|_| serde::de::Error::custom("expected 32 bytes"))?;
424 Ok(ContentHash(arr))
425 }
426
427 fn visit_seq<A>(self, mut seq: A) -> Result<ContentHash, A::Error>
428 where
429 A: serde::de::SeqAccess<'de>,
430 {
431 let mut arr = [0u8; 32];
432 for (i, byte) in arr.iter_mut().enumerate() {
433 *byte = seq
434 .next_element()?
435 .ok_or_else(|| serde::de::Error::invalid_length(i, &self))?;
436 }
437 Ok(ContentHash(arr))
438 }
439 }
440
441 deserializer.deserialize_any(ContentHashVisitor)
442 } else {
443 let bytes = <[u8; 32]>::deserialize(deserializer)?;
444 Ok(ContentHash(bytes))
445 }
446 }
447}
448
449pub type RawContent = Vec<u8>;
451
452pub fn compute_content_hash(content: &[u8]) -> ContentHash {
458 let mut hasher = Sha256::new();
459 hasher.update(content);
460 let result = hasher.finalize();
461 let mut hash = [0u8; 32];
462 hash.copy_from_slice(&result);
463 ContentHash::new(hash)
464}
465
466#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_entity_id_type_safety() {
476 let tenant_id = TenantId::now_v7();
478 let trajectory_id = TrajectoryId::now_v7();
479
480 assert_ne!(tenant_id.as_uuid(), trajectory_id.as_uuid());
484 }
485
486 #[test]
487 fn test_entity_id_display() {
488 let id = TenantId::new(Uuid::nil());
489 assert_eq!(
490 format!("{:?}", id),
491 "TenantId(00000000-0000-0000-0000-000000000000)"
492 );
493 assert_eq!(format!("{}", id), "00000000-0000-0000-0000-000000000000");
494 }
495
496 #[test]
497 fn test_entity_id_from_str() {
498 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
499 let id: TenantId = uuid_str.parse().expect("valid UUID should parse");
500 assert_eq!(id.to_string(), uuid_str);
501 }
502
503 #[test]
504 fn test_entity_id_parse_error() {
505 let result: Result<TenantId, _> = "invalid".parse();
506 assert!(result.is_err());
507 let err = result.unwrap_err();
508 assert_eq!(err.entity_name, "tenant");
509 assert_eq!(err.input, "invalid");
510 }
511
512 #[test]
513 fn test_entity_id_serde() {
514 let id = TenantId::now_v7();
515 let json = serde_json::to_string(&id).expect("serialization should succeed");
516 assert!(json.starts_with('"'));
518 assert!(json.ends_with('"'));
519
520 let deserialized: TenantId =
521 serde_json::from_str(&json).expect("deserialization should succeed");
522 assert_eq!(id, deserialized);
523 }
524
525 #[test]
526 fn test_entity_id_default() {
527 let id = TenantId::default();
528 assert_eq!(id, TenantId::nil());
529 }
530
531 #[test]
532 fn test_all_entity_types() {
533 let _tenant = TenantId::now_v7();
535 let _trajectory = TrajectoryId::now_v7();
536 let _scope = ScopeId::now_v7();
537 let _artifact = ArtifactId::now_v7();
538 let _note = NoteId::now_v7();
539 let _turn = TurnId::now_v7();
540 let _agent = AgentId::now_v7();
541 let _edge = EdgeId::now_v7();
542 let _lock = LockId::now_v7();
543 let _message = MessageId::now_v7();
544 let _delegation = DelegationId::now_v7();
545 let _handoff = HandoffId::now_v7();
546 let _api_key = ApiKeyId::now_v7();
547 let _webhook = WebhookId::now_v7();
548 let _summarization_policy = SummarizationPolicyId::now_v7();
549 }
550
551 #[test]
552 fn test_duration_ms_display() {
553 let d = DurationMs::new(1500);
554 assert_eq!(format!("{}", d), "1500ms");
555 }
556
557 #[test]
558 fn test_duration_ms_from_i64() {
559 let d: DurationMs = 42i64.into();
560 assert_eq!(d.as_millis(), 42);
561 }
562
563 #[test]
564 fn test_duration_ms_serde_roundtrip() {
565 let d = DurationMs::new(3600);
566 let json = serde_json::to_string(&d).expect("serialize");
567 assert_eq!(json, "3600");
568 let restored: DurationMs = serde_json::from_str(&json).expect("deserialize");
569 assert_eq!(d, restored);
570 }
571
572 #[test]
573 fn test_duration_ms_as_duration() {
574 let d = DurationMs::new(1500);
575 assert_eq!(d.as_duration(), std::time::Duration::from_millis(1500));
576 }
577
578 #[test]
579 fn test_duration_ms_negative_clamps() {
580 let d = DurationMs::new(-100);
581 assert_eq!(d.as_duration(), std::time::Duration::from_millis(0));
582 }
583
584 #[test]
585 fn test_content_hash_new_and_access() {
586 let bytes = [42u8; 32];
587 let hash = ContentHash::new(bytes);
588 assert_eq!(*hash.as_bytes(), bytes);
589 assert_eq!(hash.as_slice(), &bytes[..]);
590 }
591
592 #[test]
593 fn test_content_hash_default() {
594 let hash = ContentHash::default();
595 assert_eq!(*hash.as_bytes(), [0u8; 32]);
596 }
597
598 #[test]
599 fn test_content_hash_deref() {
600 let hash = ContentHash::new([1u8; 32]);
601 assert_eq!(hash.len(), 32);
603 }
604
605 #[test]
606 fn test_content_hash_from_array() {
607 let bytes = [7u8; 32];
608 let hash: ContentHash = bytes.into();
609 assert_eq!(*hash.as_bytes(), bytes);
610 }
611
612 #[test]
613 fn test_content_hash_into_array() {
614 let hash = ContentHash::new([9u8; 32]);
615 let bytes: [u8; 32] = hash.into();
616 assert_eq!(bytes, [9u8; 32]);
617 }
618
619 #[test]
620 fn test_content_hash_display() {
621 let hash = ContentHash::new([0xab; 32]);
622 let expected = "ab".repeat(32);
623 assert_eq!(format!("{}", hash), expected);
624 }
625
626 #[test]
627 fn test_content_hash_serde_json_roundtrip() {
628 let hash = ContentHash::new([0xde; 32]);
629 let json = serde_json::to_string(&hash).expect("serialize");
630 assert!(json.starts_with('"'));
632 let restored: ContentHash = serde_json::from_str(&json).expect("deserialize");
633 assert_eq!(hash, restored);
634 }
635
636 #[test]
637 fn test_content_hash_deserialize_from_array() {
638 let json_array: Vec<serde_json::Value> = (0u8..32).map(|b| serde_json::json!(b)).collect();
639 let json = serde_json::Value::Array(json_array);
640 let hash: ContentHash = serde_json::from_value(json).expect("deserialize from array");
641 for (i, &b) in hash.as_bytes().iter().enumerate() {
642 assert_eq!(b, i as u8);
643 }
644 }
645
646 #[test]
647 fn test_compute_content_hash_deterministic() {
648 let h1 = compute_content_hash(b"hello");
649 let h2 = compute_content_hash(b"hello");
650 assert_eq!(h1, h2);
651 }
652
653 #[test]
654 fn test_compute_content_hash_different_inputs() {
655 let h1 = compute_content_hash(b"hello");
656 let h2 = compute_content_hash(b"world");
657 assert_ne!(h1, h2);
658 }
659}