liminal_sdk/conversation.rs
1use alloc::string::String;
2use core::future::Future;
3
4use futures_core::Stream;
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8use crate::SdkError;
9
10/// Application-visible identifier for a conversation.
11///
12/// SDK callers use this value for correlation and lifecycle observation. It is
13/// not a beamr process identifier and does not require callers to manage any
14/// supervised runtime process directly.
15#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub struct ConversationId(String);
17
18impl ConversationId {
19 /// Creates a conversation identifier from an application-visible string.
20 #[must_use]
21 pub fn new(value: impl Into<String>) -> Self {
22 Self(value.into())
23 }
24
25 /// Returns the identifier as a string slice.
26 #[must_use]
27 pub fn as_str(&self) -> &str {
28 self.0.as_str()
29 }
30}
31
32impl From<String> for ConversationId {
33 fn from(value: String) -> Self {
34 Self::new(value)
35 }
36}
37
38impl From<&'static str> for ConversationId {
39 fn from(value: &'static str) -> Self {
40 Self::new(value)
41 }
42}
43
44/// Lifecycle events emitted by a conversation.
45///
46/// Every event carries the application-visible conversation identifier so
47/// lifecycle streams can be correlated without exposing process identifiers,
48/// protocol frames, or transport details.
49#[derive(Debug)]
50pub enum ConversationEvent {
51 /// The conversation was opened.
52 Opened {
53 /// Identifier for the conversation that emitted this event.
54 conversation_id: ConversationId,
55 },
56 /// A message was observed within the conversation.
57 Message {
58 /// Identifier for the conversation that emitted this event.
59 conversation_id: ConversationId,
60 },
61 /// The conversation has begun closing.
62 Closing {
63 /// Identifier for the conversation that emitted this event.
64 conversation_id: ConversationId,
65 },
66 /// The conversation closed.
67 Closed {
68 /// Identifier for the conversation that emitted this event.
69 conversation_id: ConversationId,
70 },
71 /// The conversation encountered an error.
72 Error {
73 /// Identifier for the conversation that emitted this event.
74 conversation_id: ConversationId,
75 /// Error reported for the conversation lifecycle.
76 error: SdkError,
77 },
78}
79
80/// Application-facing typed conversation API.
81///
82/// A conversation is the fundamental messaging unit in liminal. The handle lets
83/// callers send typed messages, receive typed messages, and observe lifecycle
84/// events without handling transport details or supervised runtime process IDs.
85pub trait ConversationHandle: core::fmt::Debug + Send + Sync {
86 /// Future returned by [`receive`](Self::receive) for message type `M`.
87 type ReceiveFuture<'a, M>: Future<Output = Result<M, SdkError>> + 'a
88 where
89 Self: 'a,
90 M: DeserializeOwned + 'a;
91
92 /// Stream returned by [`lifecycle`](Self::lifecycle).
93 type LifecycleStream: Stream<Item = ConversationEvent>;
94
95 /// Sends a typed message within this conversation.
96 ///
97 /// # Errors
98 ///
99 /// Returns [`SdkError`] when the concrete conversation implementation cannot
100 /// serialize or transmit the message in the conversation context.
101 fn send<M>(&self, message: M) -> Result<(), SdkError>
102 where
103 M: Serialize;
104
105 /// Receives the next typed message from this conversation.
106 ///
107 /// The message type is owned after deserialization so callers never borrow
108 /// buffers managed by an SDK implementation.
109 ///
110 /// # Errors
111 ///
112 /// The returned future resolves to [`SdkError`] when the concrete
113 /// implementation cannot receive or deserialize the next message.
114 fn receive<M>(&self) -> Self::ReceiveFuture<'_, M>
115 where
116 M: DeserializeOwned;
117
118 /// Observes lifecycle events for this conversation.
119 fn lifecycle(&self) -> Self::LifecycleStream;
120}