Skip to main content

agentic_core/storage/
conversation.rs

1//! Conversation storage operations.
2
3use std::convert::TryFrom;
4use std::sync::Arc;
5
6use super::models::{conversation, item, response};
7use super::pool::DbPool;
8use super::types::{
9    ConversationData, ConversationSnapshot, ConversationVersion, InOutItem, ResponseMetadata, StorageError, StoreResult,
10};
11use crate::utils::common::{serialize_to_string, uuid7_str};
12
13/// Conversation storage operations.
14#[derive(Clone, Debug)]
15pub struct ConversationStore {
16    pool: Option<Arc<DbPool>>,
17}
18
19impl ConversationStore {
20    /// Creates a disabled conversation store.
21    #[must_use]
22    pub fn disabled() -> Self {
23        Self { pool: None }
24    }
25
26    /// Creates a new conversation store with database pool.
27    #[must_use]
28    pub fn new(pool: Arc<DbPool>) -> Self {
29        Self { pool: Some(pool) }
30    }
31
32    /// Returns a reference to the database pool.
33    ///
34    /// # Errors
35    ///
36    /// Returns error if store is disabled (no pool configured).
37    fn pool(&self) -> StoreResult<&DbPool> {
38        self.pool.as_deref().ok_or(StorageError::NotConfigured)
39    }
40
41    /// Creates a new conversation.
42    ///
43    /// # Errors
44    ///
45    /// Returns error if database query fails.
46    pub async fn create(&self) -> StoreResult<ConversationData> {
47        let pool = self.pool()?;
48        let row = conversation::create(pool, &uuid7_str("conv_")).await?;
49        Ok(row.into())
50    }
51
52    /// Gets a conversation or creates it if it doesn't exist.
53    ///
54    /// # Errors
55    ///
56    /// Returns error if database query fails.
57    pub async fn get_or_create(&self, conversation_id: &str) -> StoreResult<ConversationData> {
58        let pool = self.pool()?;
59        let row = conversation::get_or_create(pool, conversation_id).await?;
60        Ok(row.into())
61    }
62
63    /// Gets a conversation by ID.
64    ///
65    /// # Errors
66    ///
67    /// Returns error if conversation not found or database query fails.
68    pub async fn get(&self, conversation_id: &str) -> StoreResult<ConversationData> {
69        let pool = self.pool()?;
70        let row = conversation::get(pool, conversation_id)
71            .await?
72            .ok_or_else(|| StorageError::not_found("Conversation", conversation_id))?;
73        Ok(row.into())
74    }
75
76    /// Rehydrates a conversation with all its items.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if a stored item is missing its sequence number or if the database query fails.
81    pub async fn rehydrate(&self, conversation_id: &str) -> StoreResult<Vec<InOutItem>> {
82        Ok(self.rehydrate_snapshot(conversation_id).await?.items)
83    }
84
85    /// Rehydrates a conversation with its items and storage version.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if a stored item is missing its sequence number or if the database query fails.
90    pub async fn rehydrate_snapshot(&self, conversation_id: &str) -> StoreResult<ConversationSnapshot> {
91        let pool = self.pool()?;
92        let rows = item::get_items_by_conversation(pool, conversation_id).await?;
93
94        let mut last_sequence = None;
95        for row in &rows {
96            last_sequence = Some(row.seq.ok_or_else(|| StorageError::InvalidConversationSequence {
97                conversation_id: conversation_id.to_string(),
98                item_id: row.id.clone(),
99            })?);
100        }
101
102        Ok(ConversationSnapshot {
103            items: rows.into_iter().filter_map(|row| row.as_inout()).collect(),
104            version: ConversationVersion::from_last_sequence(last_sequence),
105        })
106    }
107
108    /// Persists conversation turn with new items and response metadata.
109    ///
110    /// Creates items in the conversation and stores the associated response record.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`StorageError`] if conversation not found or database operation fails.
115    pub async fn persist(
116        &self,
117        conversation_id: &str,
118        response_id: &str,
119        previous_response_id: Option<&str>,
120        new_items: Vec<InOutItem>,
121        metadata: &ResponseMetadata,
122    ) -> StoreResult<()> {
123        self.persist_impl(
124            conversation_id,
125            None,
126            response_id,
127            previous_response_id,
128            new_items,
129            metadata,
130        )
131        .await
132    }
133
134    /// Persists a conversation turn only if its stored version still matches.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`StorageError`] if the conversation changed, was not found, or a database operation fails.
139    pub async fn persist_if_version(
140        &self,
141        conversation_id: &str,
142        expected_version: ConversationVersion,
143        response_id: &str,
144        previous_response_id: Option<&str>,
145        new_items: Vec<InOutItem>,
146        metadata: &ResponseMetadata,
147    ) -> StoreResult<()> {
148        self.persist_impl(
149            conversation_id,
150            Some(expected_version),
151            response_id,
152            previous_response_id,
153            new_items,
154            metadata,
155        )
156        .await
157    }
158
159    async fn persist_impl(
160        &self,
161        conversation_id: &str,
162        expected_version: Option<ConversationVersion>,
163        response_id: &str,
164        previous_response_id: Option<&str>,
165        new_items: Vec<InOutItem>,
166        metadata: &ResponseMetadata,
167    ) -> StoreResult<()> {
168        let pool = self.pool()?;
169
170        let mut item_ids: Vec<String> = Vec::new();
171        let mut items_: Vec<(String, String)> = Vec::new();
172        for any_item in new_items {
173            let item_id = uuid7_str("item_");
174            item_ids.push(item_id.clone());
175            let data_str = String::try_from(&any_item)?;
176            items_.push((item_id, data_str));
177        }
178        let history_item_ids_json = serialize_to_string(&item_ids)?;
179        let metadata_json = String::try_from(metadata)?;
180
181        let mut tx = pool.begin().await?;
182
183        match conversation::lock_in_tx(&mut tx, conversation_id).await {
184            Ok(()) => {}
185            Err(sqlx::Error::RowNotFound) => {
186                return Err(StorageError::not_found("Conversation", conversation_id));
187            }
188            Err(error) => return Err(error.into()),
189        }
190        if let Some(expected_version) = expected_version {
191            let current_version = ConversationVersion::from_last_sequence(
192                item::last_conversation_sequence_in_tx(&mut tx, conversation_id).await?,
193            );
194            if current_version != expected_version {
195                return Err(StorageError::ConversationConflict {
196                    conversation_id: conversation_id.to_owned(),
197                });
198            }
199        }
200        item::create_in_tx(&mut tx, items_, Some(conversation_id)).await?;
201
202        response::create_in_tx(
203            &mut tx,
204            response_id,
205            Some(conversation_id),
206            previous_response_id,
207            Some(&history_item_ids_json),
208            Some(&metadata_json),
209        )
210        .await?;
211        tx.commit().await?;
212
213        Ok(())
214    }
215}