Skip to main content

meerkat_runtime/
identifiers.rs

1//! ยง6 Runtime-layer identifiers.
2//!
3//! These identifiers are used only by the runtime control-plane layer.
4//! Core-facing identifiers (RunId, InputId) live in `meerkat-core::lifecycle`.
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use meerkat_core::types::SessionId;
10
11/// Unique identifier for a runtime event.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct RuntimeEventId(pub Uuid);
14
15impl RuntimeEventId {
16    pub fn new() -> Self {
17        Self(meerkat_core::time_compat::new_uuid_v7())
18    }
19}
20
21impl Default for RuntimeEventId {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl std::fmt::Display for RuntimeEventId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33/// Logical identity of a runtime instance (survives retire/recycle).
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct LogicalRuntimeId(pub String);
36
37impl LogicalRuntimeId {
38    const SESSION_RUNTIME_PREFIX: &'static str = "rt:session:";
39
40    pub fn new(id: impl Into<String>) -> Self {
41        Self(id.into())
42    }
43
44    pub fn for_session(session_id: &SessionId) -> Self {
45        Self(format!("{}{session_id}", Self::SESSION_RUNTIME_PREFIX))
46    }
47
48    pub fn legacy_session_uuid_alias(session_id: &SessionId) -> Self {
49        Self(session_id.to_string())
50    }
51
52    pub(crate) fn session_id(&self) -> Option<SessionId> {
53        let raw = self
54            .0
55            .strip_prefix(Self::SESSION_RUNTIME_PREFIX)
56            .unwrap_or(self.0.as_str());
57        SessionId::parse(raw).ok()
58    }
59}
60
61impl std::fmt::Display for LogicalRuntimeId {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(f, "{}", self.0)
64    }
65}
66
67/// Identifier for a conversation within a session.
68#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub struct ConversationId(pub Uuid);
70
71impl ConversationId {
72    pub fn new() -> Self {
73        Self(meerkat_core::time_compat::new_uuid_v7())
74    }
75}
76
77impl Default for ConversationId {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl std::fmt::Display for ConversationId {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", self.0)
86    }
87}
88
89/// Identifier linking an event to its cause.
90#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
91pub struct CausationId(pub Uuid);
92
93impl Default for CausationId {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl CausationId {
100    pub fn new() -> Self {
101        Self(meerkat_core::time_compat::new_uuid_v7())
102    }
103
104    pub fn from_uuid(uuid: Uuid) -> Self {
105        Self(uuid)
106    }
107}
108
109impl std::fmt::Display for CausationId {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(f, "{}", self.0)
112    }
113}
114
115/// Correlation identifier for tracing related events across boundaries.
116#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
117pub struct CorrelationId(pub Uuid);
118
119impl Default for CorrelationId {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl CorrelationId {
126    pub fn new() -> Self {
127        Self(meerkat_core::time_compat::new_uuid_v7())
128    }
129
130    pub fn from_uuid(uuid: Uuid) -> Self {
131        Self(uuid)
132    }
133}
134
135impl std::fmt::Display for CorrelationId {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{}", self.0)
138    }
139}
140
141/// Client-provided key for idempotent input submission.
142#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
143pub struct IdempotencyKey(pub String);
144
145impl IdempotencyKey {
146    pub fn new(key: impl Into<String>) -> Self {
147        Self(key.into())
148    }
149}
150
151impl std::fmt::Display for IdempotencyKey {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", self.0)
154    }
155}
156
157/// Key for supersession scoping (same key = same supersession window).
158#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
159pub struct SupersessionKey(pub String);
160
161impl SupersessionKey {
162    pub fn new(key: impl Into<String>) -> Self {
163        Self(key.into())
164    }
165}
166
167impl std::fmt::Display for SupersessionKey {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(f, "{}", self.0)
170    }
171}
172
173/// Version of the generated admission policy used for a decision.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
175pub struct PolicyVersion(pub u64);
176
177impl std::fmt::Display for PolicyVersion {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        write!(f, "{}", self.0)
180    }
181}
182
183/// Typed input-kind taxonomy used by the generated admission-policy projection.
184///
185/// Every variant the generated admission authority dispatches on is enumerated
186/// here so compatibility projections can request policy decisions by typed
187/// kind instead of by string.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case")]
190#[non_exhaustive]
191pub enum InputKind {
192    /// Operator/user prompt.
193    Prompt,
194    /// Peer message convention (or unconvented peer input).
195    PeerMessage,
196    /// Peer request convention.
197    PeerRequest,
198    /// Peer response progress convention.
199    PeerResponseProgress,
200    /// Peer response terminal convention.
201    PeerResponseTerminal,
202    /// Flow step input.
203    FlowStep,
204    /// External event input.
205    ExternalEvent,
206    /// Explicit continuation input.
207    Continuation,
208    /// Explicit operation/lifecycle input.
209    Operation,
210}
211
212impl InputKind {
213    /// Stable lowercase identifier. Wire formats and trace strings rely on
214    /// this exact spelling.
215    pub fn as_str(self) -> &'static str {
216        match self {
217            InputKind::Prompt => "prompt",
218            InputKind::PeerMessage => "peer_message",
219            InputKind::PeerRequest => "peer_request",
220            InputKind::PeerResponseProgress => "peer_response_progress",
221            InputKind::PeerResponseTerminal => "peer_response_terminal",
222            InputKind::FlowStep => "flow_step",
223            InputKind::ExternalEvent => "external_event",
224            InputKind::Continuation => "continuation",
225            InputKind::Operation => "operation",
226        }
227    }
228}
229
230impl std::fmt::Display for InputKind {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        f.write_str(self.as_str())
233    }
234}
235
236/// Identifier for an input kind, wrapping the typed [`InputKind`] taxonomy.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
238pub struct KindId(pub InputKind);
239
240impl KindId {
241    pub const fn new(kind: InputKind) -> Self {
242        Self(kind)
243    }
244
245    pub const fn kind(self) -> InputKind {
246        self.0
247    }
248}
249
250impl From<InputKind> for KindId {
251    fn from(kind: InputKind) -> Self {
252        Self(kind)
253    }
254}
255
256impl std::fmt::Display for KindId {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        std::fmt::Display::fmt(&self.0, f)
259    }
260}
261
262/// Identifier for a schema definition.
263#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
264pub struct SchemaId(pub String);
265
266impl SchemaId {
267    pub fn new(id: impl Into<String>) -> Self {
268        Self(id.into())
269    }
270}
271
272impl std::fmt::Display for SchemaId {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        write!(f, "{}", self.0)
275    }
276}
277
278/// Identifier for a projection rule.
279#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
280pub struct ProjectionRuleId(pub String);
281
282impl ProjectionRuleId {
283    pub fn new(id: impl Into<String>) -> Self {
284        Self(id.into())
285    }
286}
287
288impl std::fmt::Display for ProjectionRuleId {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        write!(f, "{}", self.0)
291    }
292}
293
294/// Stable event code for wire formats and SDK consumers.
295#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
296pub struct EventCodeId(pub String);
297
298impl EventCodeId {
299    pub fn new(id: impl Into<String>) -> Self {
300        Self(id.into())
301    }
302}
303
304impl std::fmt::Display for EventCodeId {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        write!(f, "{}", self.0)
307    }
308}
309
310#[cfg(test)]
311#[allow(clippy::unwrap_used)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn runtime_event_id_unique() {
317        let a = RuntimeEventId::new();
318        let b = RuntimeEventId::new();
319        assert_ne!(a, b);
320    }
321
322    #[test]
323    fn runtime_event_id_serde() {
324        let id = RuntimeEventId::new();
325        let json = serde_json::to_string(&id).unwrap();
326        let parsed: RuntimeEventId = serde_json::from_str(&json).unwrap();
327        assert_eq!(id, parsed);
328    }
329
330    #[test]
331    fn logical_runtime_id_serde() {
332        let id = LogicalRuntimeId::new("agent-1");
333        let json = serde_json::to_string(&id).unwrap();
334        let parsed: LogicalRuntimeId = serde_json::from_str(&json).unwrap();
335        assert_eq!(id, parsed);
336        assert_eq!(id.to_string(), "agent-1");
337    }
338
339    #[test]
340    fn conversation_id_unique() {
341        let a = ConversationId::new();
342        let b = ConversationId::new();
343        assert_ne!(a, b);
344    }
345
346    #[test]
347    fn idempotency_key_serde() {
348        let key = IdempotencyKey::new("req-abc-123");
349        let json = serde_json::to_string(&key).unwrap();
350        let parsed: IdempotencyKey = serde_json::from_str(&json).unwrap();
351        assert_eq!(key, parsed);
352    }
353
354    #[test]
355    fn supersession_key_serde() {
356        let key = SupersessionKey::new("peer-status");
357        let json = serde_json::to_string(&key).unwrap();
358        let parsed: SupersessionKey = serde_json::from_str(&json).unwrap();
359        assert_eq!(key, parsed);
360    }
361
362    #[test]
363    fn policy_version_serde() {
364        let v = PolicyVersion(42);
365        let json = serde_json::to_string(&v).unwrap();
366        let parsed: PolicyVersion = serde_json::from_str(&json).unwrap();
367        assert_eq!(v, parsed);
368    }
369
370    #[test]
371    fn kind_id_display() {
372        let id = KindId::new(InputKind::Prompt);
373        assert_eq!(id.to_string(), "prompt");
374        assert_eq!(
375            KindId::new(InputKind::PeerResponseProgress).to_string(),
376            "peer_response_progress"
377        );
378    }
379
380    #[test]
381    fn kind_id_serde_roundtrips_typed_variant() {
382        let id = KindId::new(InputKind::PeerResponseTerminal);
383        let json = serde_json::to_string(&id).unwrap();
384        let parsed: KindId = serde_json::from_str(&json).unwrap();
385        assert_eq!(id, parsed);
386    }
387}