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