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 policy table 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 runtime policy table.
176///
177/// Every variant the policy table dispatches on is enumerated here. New input
178/// kinds must be added to this enum AND wired into
179/// `DefaultPolicyTable::resolve_by_kind`; the compiler enforces exhaustive
180/// coverage.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183#[non_exhaustive]
184pub enum InputKind {
185    /// Operator/user prompt.
186    Prompt,
187    /// Peer message convention (or unconvented peer input).
188    PeerMessage,
189    /// Peer request convention.
190    PeerRequest,
191    /// Peer response progress convention.
192    PeerResponseProgress,
193    /// Peer response terminal convention.
194    PeerResponseTerminal,
195    /// Flow step input.
196    FlowStep,
197    /// External event input.
198    ExternalEvent,
199    /// Explicit continuation input.
200    Continuation,
201    /// Explicit operation/lifecycle input.
202    Operation,
203}
204
205impl InputKind {
206    /// Stable lowercase identifier. Wire formats and trace strings rely on
207    /// this exact spelling.
208    pub fn as_str(self) -> &'static str {
209        match self {
210            InputKind::Prompt => "prompt",
211            InputKind::PeerMessage => "peer_message",
212            InputKind::PeerRequest => "peer_request",
213            InputKind::PeerResponseProgress => "peer_response_progress",
214            InputKind::PeerResponseTerminal => "peer_response_terminal",
215            InputKind::FlowStep => "flow_step",
216            InputKind::ExternalEvent => "external_event",
217            InputKind::Continuation => "continuation",
218            InputKind::Operation => "operation",
219        }
220    }
221}
222
223impl std::fmt::Display for InputKind {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.write_str(self.as_str())
226    }
227}
228
229/// Identifier for an input kind, wrapping the typed [`InputKind`] taxonomy.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
231pub struct KindId(pub InputKind);
232
233impl KindId {
234    pub const fn new(kind: InputKind) -> Self {
235        Self(kind)
236    }
237
238    pub const fn kind(self) -> InputKind {
239        self.0
240    }
241}
242
243impl From<InputKind> for KindId {
244    fn from(kind: InputKind) -> Self {
245        Self(kind)
246    }
247}
248
249impl std::fmt::Display for KindId {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        std::fmt::Display::fmt(&self.0, f)
252    }
253}
254
255/// Identifier for a schema definition.
256#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
257pub struct SchemaId(pub String);
258
259impl SchemaId {
260    pub fn new(id: impl Into<String>) -> Self {
261        Self(id.into())
262    }
263}
264
265impl std::fmt::Display for SchemaId {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        write!(f, "{}", self.0)
268    }
269}
270
271/// Identifier for a projection rule.
272#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
273pub struct ProjectionRuleId(pub String);
274
275impl ProjectionRuleId {
276    pub fn new(id: impl Into<String>) -> Self {
277        Self(id.into())
278    }
279}
280
281impl std::fmt::Display for ProjectionRuleId {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "{}", self.0)
284    }
285}
286
287/// Stable event code for wire formats and SDK consumers.
288#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
289pub struct EventCodeId(pub String);
290
291impl EventCodeId {
292    pub fn new(id: impl Into<String>) -> Self {
293        Self(id.into())
294    }
295}
296
297impl std::fmt::Display for EventCodeId {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        write!(f, "{}", self.0)
300    }
301}
302
303#[cfg(test)]
304#[allow(clippy::unwrap_used)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn runtime_event_id_unique() {
310        let a = RuntimeEventId::new();
311        let b = RuntimeEventId::new();
312        assert_ne!(a, b);
313    }
314
315    #[test]
316    fn runtime_event_id_serde() {
317        let id = RuntimeEventId::new();
318        let json = serde_json::to_string(&id).unwrap();
319        let parsed: RuntimeEventId = serde_json::from_str(&json).unwrap();
320        assert_eq!(id, parsed);
321    }
322
323    #[test]
324    fn logical_runtime_id_serde() {
325        let id = LogicalRuntimeId::new("agent-1");
326        let json = serde_json::to_string(&id).unwrap();
327        let parsed: LogicalRuntimeId = serde_json::from_str(&json).unwrap();
328        assert_eq!(id, parsed);
329        assert_eq!(id.to_string(), "agent-1");
330    }
331
332    #[test]
333    fn conversation_id_unique() {
334        let a = ConversationId::new();
335        let b = ConversationId::new();
336        assert_ne!(a, b);
337    }
338
339    #[test]
340    fn idempotency_key_serde() {
341        let key = IdempotencyKey::new("req-abc-123");
342        let json = serde_json::to_string(&key).unwrap();
343        let parsed: IdempotencyKey = serde_json::from_str(&json).unwrap();
344        assert_eq!(key, parsed);
345    }
346
347    #[test]
348    fn supersession_key_serde() {
349        let key = SupersessionKey::new("peer-status");
350        let json = serde_json::to_string(&key).unwrap();
351        let parsed: SupersessionKey = serde_json::from_str(&json).unwrap();
352        assert_eq!(key, parsed);
353    }
354
355    #[test]
356    fn policy_version_serde() {
357        let v = PolicyVersion(42);
358        let json = serde_json::to_string(&v).unwrap();
359        let parsed: PolicyVersion = serde_json::from_str(&json).unwrap();
360        assert_eq!(v, parsed);
361    }
362
363    #[test]
364    fn kind_id_display() {
365        let id = KindId::new(InputKind::Prompt);
366        assert_eq!(id.to_string(), "prompt");
367        assert_eq!(
368            KindId::new(InputKind::PeerResponseProgress).to_string(),
369            "peer_response_progress"
370        );
371    }
372
373    #[test]
374    fn kind_id_serde_roundtrips_typed_variant() {
375        let id = KindId::new(InputKind::PeerResponseTerminal);
376        let json = serde_json::to_string(&id).unwrap();
377        let parsed: KindId = serde_json::from_str(&json).unwrap();
378        assert_eq!(id, parsed);
379    }
380}