Skip to main content

systemprompt_identifiers/
context.rs

1//! Execution-context identifier — UUID v4 only.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use crate::GatewayConversationId;
7use crate::error::IdValidationError;
8
9crate::define_id!(ContextId, validated, schema, validate_uuid_v4);
10
11fn validate_uuid_v4(s: &str) -> Result<(), IdValidationError> {
12    uuid::Uuid::parse_str(s).map_err(|e| IdValidationError::invalid("ContextId", e.to_string()))?;
13    Ok(())
14}
15
16const GATEWAY_CONVERSATION_NAMESPACE: uuid::Uuid =
17    uuid::Uuid::from_u128(0x993f_3f2c_f4d9_463b_853a_d3f0_3e19_0898);
18
19const MESSAGING_NAMESPACE: uuid::Uuid =
20    uuid::Uuid::from_u128(0x6b1d_2a7e_9c84_4f31_b5e0_71a2_4d8c_3f06);
21
22impl ContextId {
23    pub fn generate() -> Self {
24        Self::new(uuid::Uuid::new_v4().to_string())
25    }
26
27    /// Mint a deterministic `ContextId` from a `GatewayConversationId`.
28    ///
29    /// Same gateway-conversation id always produces the same `ContextId`, so
30    /// the gateway boundary can satisfy the "every conversation has a UUID
31    /// `ContextId`" data-integrity invariant without trusting the upstream
32    /// LLM client's `x-context-id` header (which carries client-specific
33    /// non-UUID identifiers).
34    #[must_use]
35    pub fn derived_from_gateway_conversation(gw: &GatewayConversationId) -> Self {
36        Self::new(
37            uuid::Uuid::new_v5(&GATEWAY_CONVERSATION_NAMESPACE, gw.as_str().as_bytes()).to_string(),
38        )
39    }
40
41    /// Mint a deterministic `ContextId` for a chat-platform conversation.
42    ///
43    /// The same `(platform, org, channel)` triple — e.g.
44    /// `("slack", workspace_id, channel_id)` or
45    /// `("teams", tenant_id, conversation_id)` — always produces the same
46    /// `ContextId`, so the messaging dispatch boundary satisfies the "every
47    /// conversation has a UUID `ContextId`" invariant without a channel→context
48    /// mapping table.
49    #[must_use]
50    pub fn derived_from_messaging(platform: &str, org: &str, channel: &str) -> Self {
51        let key = format!("{platform}:{org}:{channel}");
52        Self::new(uuid::Uuid::new_v5(&MESSAGING_NAMESPACE, key.as_bytes()).to_string())
53    }
54}