1use serde::{Deserialize, Serialize};
6use std::borrow::Borrow;
7use std::fmt;
8use std::str::FromStr;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct RunId(Uuid);
15
16impl RunId {
17 pub fn new() -> Self {
18 Self(Uuid::new_v4())
19 }
20
21 pub fn as_uuid(&self) -> &Uuid {
22 &self.0
23 }
24}
25
26impl Default for RunId {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl fmt::Display for RunId {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 self.0.fmt(f)
35 }
36}
37
38impl FromStr for RunId {
39 type Err = uuid::Error;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 Ok(Self(Uuid::parse_str(s)?))
43 }
44}
45
46macro_rules! string_newtype {
47 ($(#[$meta:meta])* $name:ident) => {
48 $(#[$meta])*
49 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
50 #[serde(transparent)]
51 pub struct $name(String);
52
53 impl $name {
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57 }
58
59 impl fmt::Display for $name {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 self.0.fmt(f)
62 }
63 }
64
65 impl From<String> for $name {
66 fn from(value: String) -> Self {
67 Self(value)
68 }
69 }
70
71 impl From<&str> for $name {
72 fn from(value: &str) -> Self {
73 Self(value.to_owned())
74 }
75 }
76
77 impl Borrow<str> for $name {
78 fn borrow(&self) -> &str {
79 &self.0
80 }
81 }
82
83 impl Borrow<String> for $name {
84 fn borrow(&self) -> &String {
85 &self.0
86 }
87 }
88
89 impl AsRef<str> for $name {
90 fn as_ref(&self) -> &str {
91 &self.0
92 }
93 }
94
95 impl PartialEq<String> for $name {
96 fn eq(&self, other: &String) -> bool {
97 &self.0 == other
98 }
99 }
100
101 impl PartialEq<&String> for $name {
102 fn eq(&self, other: &&String) -> bool {
103 &self.0 == *other
104 }
105 }
106
107 impl PartialEq<str> for $name {
108 fn eq(&self, other: &str) -> bool {
109 self.0.as_str() == other
110 }
111 }
112
113 impl PartialEq<&str> for $name {
114 fn eq(&self, other: &&str) -> bool {
115 self.0.as_str() == *other
116 }
117 }
118 };
119}
120
121string_newtype!(
122 MobId
124);
125
126string_newtype!(
127 FlowId
129);
130
131string_newtype!(
132 StepId
134);
135
136string_newtype!(
137 BranchId
139);
140
141pub type MeerkatId = AgentIdentity;
163
164string_newtype!(
165 ProfileName
167);
168
169string_newtype!(
170 FrameId
172);
173
174string_newtype!(
175 LoopInstanceId
177);
178
179string_newtype!(
180 FlowNodeId
182);
183
184string_newtype!(
185 LoopId
187);
188
189string_newtype!(
194 AgentIdentity
199);
200
201impl From<&AgentIdentity> for AgentIdentity {
212 fn from(identity: &AgentIdentity) -> Self {
213 Self::from(identity.as_str())
214 }
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct Generation(u64);
225
226impl Generation {
227 pub const INITIAL: Self = Self(0);
229
230 pub const fn new(value: u64) -> Self {
232 Self(value)
233 }
234
235 pub const fn get(self) -> u64 {
237 self.0
238 }
239
240 pub const fn next(self) -> Self {
242 Self(self.0 + 1)
243 }
244}
245
246impl fmt::Display for Generation {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 self.0.fmt(f)
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
258pub struct AgentRuntimeId {
259 pub identity: AgentIdentity,
261 pub generation: Generation,
263}
264
265impl AgentRuntimeId {
266 pub fn new(identity: AgentIdentity, generation: Generation) -> Self {
268 Self {
269 identity,
270 generation,
271 }
272 }
273
274 pub fn initial(identity: AgentIdentity) -> Self {
276 Self {
277 identity,
278 generation: Generation::INITIAL,
279 }
280 }
281}
282
283impl fmt::Display for AgentRuntimeId {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{}:{}", self.identity, self.generation.get())
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
295#[serde(transparent)]
296pub struct FenceToken(u64);
297
298impl FenceToken {
299 pub const fn new(value: u64) -> Self {
301 Self(value)
302 }
303
304 pub const fn get(self) -> u64 {
306 self.0
307 }
308}
309
310impl fmt::Display for FenceToken {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "fence:{}", self.0)
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
321#[serde(transparent)]
322pub struct WorkRef(Uuid);
323
324impl WorkRef {
325 pub fn new() -> Self {
327 Self(Uuid::new_v4())
328 }
329
330 pub fn as_uuid(&self) -> &Uuid {
332 &self.0
333 }
334}
335
336impl Default for WorkRef {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342impl fmt::Display for WorkRef {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 self.0.fmt(f)
345 }
346}
347
348impl FromStr for WorkRef {
349 type Err = uuid::Error;
350
351 fn from_str(s: &str) -> Result<Self, Self::Err> {
352 Ok(Self(Uuid::parse_str(s)?))
353 }
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct WorkSpec {
371 pub content: meerkat_core::types::ContentInput,
373 pub origin: WorkOrigin,
376}
377
378impl WorkSpec {
379 pub fn new(content: impl Into<meerkat_core::types::ContentInput>, origin: WorkOrigin) -> Self {
383 Self {
384 content: content.into(),
385 origin,
386 }
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
392pub enum WorkOrigin {
393 External,
395 Internal,
397}
398
399impl WorkOrigin {
400 pub const fn as_str(self) -> &'static str {
402 match self {
403 WorkOrigin::External => "External",
404 WorkOrigin::Internal => "Internal",
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_run_id_roundtrip_json() {
415 let run_id = RunId::new();
416 let encoded = serde_json::to_string(&run_id).unwrap();
417 let decoded: RunId = serde_json::from_str(&encoded).unwrap();
418 assert_eq!(decoded, run_id);
419 }
420
421 #[test]
422 fn test_run_id_roundtrip_parse_display() {
423 let run_id = RunId::new();
424 let rendered = run_id.to_string();
425 let reparsed = RunId::from_str(&rendered).unwrap();
426 assert_eq!(reparsed, run_id);
427 }
428
429 #[test]
430 fn test_flow_id_roundtrip_json() {
431 let id = FlowId::from("flow-a");
432 let encoded = serde_json::to_string(&id).unwrap();
433 let decoded: FlowId = serde_json::from_str(&encoded).unwrap();
434 assert_eq!(decoded, id);
435 }
436
437 #[test]
438 fn test_step_id_roundtrip_json() {
439 let id = StepId::from("step-a");
440 let encoded = serde_json::to_string(&id).unwrap();
441 let decoded: StepId = serde_json::from_str(&encoded).unwrap();
442 assert_eq!(decoded, id);
443 }
444
445 #[test]
446 fn test_branch_id_roundtrip_json() {
447 let id = BranchId::from("branch-a");
448 let encoded = serde_json::to_string(&id).unwrap();
449 let decoded: BranchId = serde_json::from_str(&encoded).unwrap();
450 assert_eq!(decoded, id);
451 }
452
453 #[test]
454 fn test_frame_id_roundtrip_json() {
455 let id = FrameId::from("frame-a");
456 let encoded = serde_json::to_string(&id).unwrap();
457 let decoded: FrameId = serde_json::from_str(&encoded).unwrap();
458 assert_eq!(decoded, id);
459 }
460
461 #[test]
462 fn test_loop_instance_id_roundtrip_json() {
463 let id = LoopInstanceId::from("loop-instance-a");
464 let encoded = serde_json::to_string(&id).unwrap();
465 let decoded: LoopInstanceId = serde_json::from_str(&encoded).unwrap();
466 assert_eq!(decoded, id);
467 }
468
469 #[test]
470 fn test_flow_node_id_roundtrip_json() {
471 let id = FlowNodeId::from("node-a");
472 let encoded = serde_json::to_string(&id).unwrap();
473 let decoded: FlowNodeId = serde_json::from_str(&encoded).unwrap();
474 assert_eq!(decoded, id);
475 }
476
477 #[test]
478 fn test_loop_id_roundtrip_json() {
479 let id = LoopId::from("loop-a");
480 let encoded = serde_json::to_string(&id).unwrap();
481 let decoded: LoopId = serde_json::from_str(&encoded).unwrap();
482 assert_eq!(decoded, id);
483 }
484
485 #[test]
500 fn agent_identity_to_meerkat_id_conversion_preserves_identity_string() {
501 let identity = AgentIdentity::from("singer");
502
503 let by_owned: MeerkatId = identity.clone();
505 assert_eq!(by_owned.as_str(), "singer");
506
507 let by_borrow: MeerkatId = (&identity).into();
510 assert_eq!(by_borrow.as_str(), "singer");
511
512 let back: AgentIdentity = by_owned;
516 assert_eq!(back, identity);
517 }
518
519 #[test]
520 fn test_existing_ids_roundtrip() {
521 let mob = MobId::from("mob-a");
522 let meerkat = MeerkatId::from("meerkat-a");
523 let profile = ProfileName::from("lead");
524 assert_eq!(
525 serde_json::from_str::<MobId>(&serde_json::to_string(&mob).unwrap()).unwrap(),
526 mob
527 );
528 assert_eq!(
529 serde_json::from_str::<MeerkatId>(&serde_json::to_string(&meerkat).unwrap()).unwrap(),
530 meerkat
531 );
532 assert_eq!(
533 serde_json::from_str::<ProfileName>(&serde_json::to_string(&profile).unwrap()).unwrap(),
534 profile
535 );
536 }
537
538 #[test]
541 fn test_agent_identity_roundtrip_json() {
542 let id = AgentIdentity::from("researcher");
543 let encoded = serde_json::to_string(&id).unwrap();
544 assert_eq!(encoded, "\"researcher\"");
545 let decoded: AgentIdentity = serde_json::from_str(&encoded).unwrap();
546 assert_eq!(decoded, id);
547 }
548
549 #[test]
550 fn test_agent_identity_display() {
551 let id = AgentIdentity::from("lead-agent");
552 assert_eq!(id.to_string(), "lead-agent");
553 assert_eq!(id.as_str(), "lead-agent");
554 }
555
556 #[test]
557 fn test_generation_roundtrip_json() {
558 let generation = Generation::new(42);
559 let encoded = serde_json::to_string(&generation).unwrap();
560 assert_eq!(encoded, "42");
561 let decoded: Generation = serde_json::from_str(&encoded).unwrap();
562 assert_eq!(decoded, generation);
563 }
564
565 #[test]
566 fn test_generation_initial_and_next() {
567 assert_eq!(Generation::INITIAL.get(), 0);
568 assert_eq!(Generation::INITIAL.next().get(), 1);
569 assert_eq!(Generation::new(5).next().get(), 6);
570 }
571
572 #[test]
573 fn test_generation_ordering() {
574 assert!(Generation::new(0) < Generation::new(1));
575 assert!(Generation::new(1) < Generation::new(100));
576 }
577
578 #[test]
579 fn test_agent_runtime_id_roundtrip_json() {
580 let rid = AgentRuntimeId::new(AgentIdentity::from("worker"), Generation::new(3));
581 let encoded = serde_json::to_string(&rid).unwrap();
582 let decoded: AgentRuntimeId = serde_json::from_str(&encoded).unwrap();
583 assert_eq!(decoded, rid);
584 }
585
586 #[test]
587 fn test_agent_runtime_id_initial() {
588 let rid = AgentRuntimeId::initial(AgentIdentity::from("worker"));
589 assert_eq!(rid.identity, AgentIdentity::from("worker"));
590 assert_eq!(rid.generation, Generation::INITIAL);
591 }
592
593 #[test]
594 fn test_agent_runtime_id_display() {
595 let rid = AgentRuntimeId::new(AgentIdentity::from("coder"), Generation::new(2));
596 assert_eq!(rid.to_string(), "coder:2");
597 }
598
599 #[test]
600 fn test_fence_token_roundtrip_json() {
601 let ft = FenceToken::new(99);
602 let encoded = serde_json::to_string(&ft).unwrap();
603 assert_eq!(encoded, "99");
604 let decoded: FenceToken = serde_json::from_str(&encoded).unwrap();
605 assert_eq!(decoded, ft);
606 }
607
608 #[test]
609 fn test_fence_token_display() {
610 assert_eq!(FenceToken::new(7).to_string(), "fence:7");
611 }
612
613 #[test]
614 fn test_fence_token_ordering() {
615 assert!(FenceToken::new(1) < FenceToken::new(2));
616 }
617
618 #[test]
619 fn test_work_ref_roundtrip_json() {
620 let wr = WorkRef::new();
621 let encoded = serde_json::to_string(&wr).unwrap();
622 let decoded: WorkRef = serde_json::from_str(&encoded).unwrap();
623 assert_eq!(decoded, wr);
624 }
625
626 #[test]
627 fn test_work_ref_roundtrip_parse_display() {
628 let wr = WorkRef::new();
629 let rendered = wr.to_string();
630 let reparsed = WorkRef::from_str(&rendered).unwrap();
631 assert_eq!(reparsed, wr);
632 }
633
634 #[test]
635 fn test_work_spec_roundtrip_json() {
636 let spec = WorkSpec::new("do something".to_owned(), WorkOrigin::External);
637 let encoded = serde_json::to_string(&spec).unwrap();
638 let decoded: WorkSpec = serde_json::from_str(&encoded).unwrap();
639 assert_eq!(
640 decoded.content,
641 meerkat_core::types::ContentInput::from("do something".to_string()),
642 );
643 assert_eq!(decoded.origin, WorkOrigin::External);
644 }
645
646 #[test]
647 fn test_work_origin_variants_roundtrip_json() {
648 for origin in [WorkOrigin::External, WorkOrigin::Internal] {
649 let encoded = serde_json::to_string(&origin).unwrap();
650 let decoded: WorkOrigin = serde_json::from_str(&encoded).unwrap();
651 assert_eq!(decoded, origin);
652 }
653 }
654
655 #[test]
656 fn test_work_spec_internal_origin() {
657 let spec = WorkSpec::new("coordinate".to_owned(), WorkOrigin::Internal);
658 assert_eq!(spec.origin, WorkOrigin::Internal);
659 assert_eq!(
660 spec.content,
661 meerkat_core::types::ContentInput::from("coordinate".to_string()),
662 );
663 }
664
665 #[test]
666 fn test_work_spec_accepts_multimodal_content() {
667 let image_block = meerkat_core::types::ContentBlock::Image {
672 media_type: "image/png".to_string(),
673 data: meerkat_core::ImageData::Inline {
674 data: "iVBORw0KGgo=".to_string(),
675 },
676 };
677 let content = meerkat_core::types::ContentInput::Blocks(vec![
678 meerkat_core::types::ContentBlock::Text {
679 text: "analyse this".to_string(),
680 },
681 image_block.clone(),
682 ]);
683 let spec = WorkSpec::new(content.clone(), WorkOrigin::External);
684 assert_eq!(spec.content, content);
685 }
686}