Skip to main content

behest_runtime/
store.rs

1//! Runtime store facade.
2//!
3//! Provides a unified interface for runtime persistence operations,
4//! composing session, execution, and run stores.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use behest_provider::Message;
12use behest_store::{
13    ArtifactStore, EmbeddingStore, ExecutionStore, MessageRecord, MessageRole, SessionStore,
14};
15
16use super::error::{RuntimeError, RuntimeResult};
17use super::event::AgentEvent;
18use super::extension::ExtensionPoint;
19use super::extensions::Extensions;
20use super::run::{RunId, RunRecord, RunStatus};
21use super::state::RunState;
22
23/// Persistent record of a run event with sequence number ordering.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RunEventRecord {
26    /// Monotonically increasing event sequence number within the run.
27    pub sequence: u64,
28    /// Run this event belongs to.
29    pub run_id: RunId,
30    /// The event payload.
31    pub event: AgentEvent,
32    /// When the event was recorded.
33    pub timestamp: DateTime<Utc>,
34}
35
36impl RunEventRecord {
37    /// Creates a new run event record with the current timestamp.
38    #[must_use]
39    pub fn new(sequence: u64, run_id: RunId, event: AgentEvent) -> Self {
40        Self {
41            sequence,
42            run_id,
43            event,
44            timestamp: Utc::now(),
45        }
46    }
47}
48
49/// Store for run lifecycle and events.
50///
51/// Implementations provide persistence for run metadata and the
52/// event-sourced event log. Default implementations are provided for
53/// [`get_run_state`](Self::get_run_state) and
54/// [`list_runs_filtered`](Self::list_runs_filtered), which backends
55/// may override with native projections for efficiency.
56#[async_trait]
57pub trait RunStore: Send + Sync {
58    /// Persists a new run record.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`RuntimeError::Storage`] on persistence failure.
63    async fn create_run(&self, record: RunRecord) -> RuntimeResult<()>;
64
65    /// Loads a run record by its identifier.
66    ///
67    /// Returns `None` when no run with the given ID exists.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`RuntimeError::Storage`] on persistence failure.
72    async fn get_run(&self, run_id: RunId) -> RuntimeResult<Option<RunRecord>>;
73
74    /// Gets the event-sourced state of a run by replaying its event log.
75    ///
76    /// Default implementation calls [`Self::get_run`] + [`Self::list_events`] and
77    /// folds them into a [`RunState`]. Backends may override with a
78    /// native projection for better performance.
79    async fn get_run_state(&self, run_id: RunId) -> RuntimeResult<Option<RunState>> {
80        let Some(record) = self.get_run(run_id).await? else {
81            return Ok(None);
82        };
83        let events = self.list_events(run_id).await?;
84        Ok(Some(RunState::create(&record, &events)))
85    }
86
87    /// Updates the status of an existing run.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`RuntimeError::Storage`] on persistence failure.
92    async fn update_run_status(&self, run_id: RunId, status: RunStatus) -> RuntimeResult<()>;
93
94    /// Appends an event to a run's event log.
95    ///
96    /// The event is stored as a [`RunEventRecord`] with a monotonically
97    /// increasing sequence number.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`RuntimeError::Storage`] on persistence failure.
102    async fn append_event(&self, record: RunEventRecord) -> RuntimeResult<()>;
103
104    /// Returns the full event log for a run, ordered by sequence number.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`RuntimeError::RunNotFound`] when the run does not exist,
109    /// or [`RuntimeError::Storage`] on persistence failure.
110    async fn list_events(&self, run_id: RunId) -> RuntimeResult<Vec<RunEventRecord>>;
111
112    /// Lists all runs belonging to a session.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`RuntimeError::Storage`] on persistence failure.
117    async fn list_runs(&self, session_id: Uuid) -> RuntimeResult<Vec<RunRecord>>;
118
119    /// Lists runs with optional filters and pagination.
120    ///
121    /// Default implementation iterates all sessions; backends should override
122    /// with a native query for efficiency.
123    async fn list_runs_filtered(
124        &self,
125        session_id: Option<Uuid>,
126        status: Option<RunStatus>,
127        limit: usize,
128        offset: usize,
129    ) -> RuntimeResult<Vec<RunRecord>> {
130        let _ = (session_id, status, limit, offset);
131        Err(RuntimeError::Storage(
132            behest_core::error::StorageError::BackendError {
133                backend: "run".to_owned(),
134                message: "list_runs_filtered not implemented".to_owned(),
135                source: None,
136            },
137        ))
138    }
139
140    /// Deletes a run and all its associated events.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`RuntimeError::Storage`] on persistence failure.
145    async fn delete_run(&self, run_id: RunId) -> RuntimeResult<()>;
146
147    /// Performs a health check against the underlying storage backend.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`RuntimeError::Storage`] when the backend is unhealthy.
152    async fn health_check(&self) -> RuntimeResult<()>;
153}
154
155/// Runtime store facade composing session, execution, run, embedding, and artifact stores.
156///
157/// Provides a unified interface for all runtime persistence operations.
158/// Individual sub-stores are accessed via their respective accessor methods.
159pub struct RuntimeStore {
160    sessions: Box<dyn SessionStore>,
161    executions: Box<dyn ExecutionStore>,
162    runs: Box<dyn RunStore>,
163    embeddings: Option<Box<dyn EmbeddingStore>>,
164    artifacts: Option<Box<dyn ArtifactStore>>,
165
166    sessions_ep: ExtensionPoint<dyn SessionStore>,
167    executions_ep: ExtensionPoint<dyn ExecutionStore>,
168    runs_ep: ExtensionPoint<dyn RunStore>,
169    embeddings_ep: ExtensionPoint<dyn EmbeddingStore>,
170    artifacts_ep: ExtensionPoint<dyn ArtifactStore>,
171}
172
173impl RuntimeStore {
174    /// Creates a new runtime store with session, execution, and run stores.
175    ///
176    /// Embedding and artifact stores are optional — attach them with
177    /// [`with_embeddings`](Self::with_embeddings) and
178    /// [`with_artifacts`](Self::with_artifacts).
179    #[must_use]
180    pub fn new(
181        sessions: Box<dyn SessionStore>,
182        executions: Box<dyn ExecutionStore>,
183        runs: Box<dyn RunStore>,
184    ) -> Self {
185        Self {
186            sessions,
187            executions,
188            runs,
189            embeddings: None,
190            artifacts: None,
191            sessions_ep: ExtensionPoint::new(),
192            executions_ep: ExtensionPoint::new(),
193            runs_ep: ExtensionPoint::new(),
194            embeddings_ep: ExtensionPoint::new(),
195            artifacts_ep: ExtensionPoint::new(),
196        }
197    }
198
199    /// Attaches an optional embedding store for vector search operations.
200    #[must_use]
201    pub fn with_embeddings(mut self, store: Box<dyn EmbeddingStore>) -> Self {
202        self.embeddings = Some(store);
203        self
204    }
205
206    /// Attaches an optional artifact store for file/blob storage.
207    #[must_use]
208    pub fn with_artifacts(mut self, store: Box<dyn ArtifactStore>) -> Self {
209        self.artifacts = Some(store);
210        self
211    }
212
213    /// Returns the session store.
214    #[must_use]
215    pub fn sessions(&self) -> &dyn SessionStore {
216        &*self.sessions
217    }
218
219    /// Returns the execution store.
220    #[must_use]
221    pub fn executions(&self) -> &dyn ExecutionStore {
222        &*self.executions
223    }
224
225    /// Returns the run store.
226    #[must_use]
227    pub fn runs(&self) -> &dyn RunStore {
228        &*self.runs
229    }
230
231    /// Returns the embedding store, if configured.
232    #[must_use]
233    pub fn embeddings(&self) -> Option<&dyn EmbeddingStore> {
234        self.embeddings.as_deref()
235    }
236
237    /// Returns the artifact store, if configured.
238    #[must_use]
239    pub fn artifacts(&self) -> Option<&dyn ArtifactStore> {
240        self.artifacts.as_deref()
241    }
242
243    /// Creates a `RuntimeStore` from an [`Extensions`] facade, cloning its
244    /// store extension points. Backed by default in-memory stores when no
245    /// entries are registered.
246    #[must_use]
247    pub fn from_extensions(exts: &Extensions) -> Self {
248        Self {
249            sessions: Box::new(behest_store::memory::MemorySessionStore::new()),
250            executions: Box::new(behest_store::memory::MemoryExecutionStore::new()),
251            runs: Box::new(super::memory::MemoryRunStore::new()),
252            embeddings: None,
253            artifacts: None,
254            sessions_ep: exts.session_stores.clone(),
255            executions_ep: exts.execution_stores.clone(),
256            runs_ep: exts.run_stores.clone(),
257            embeddings_ep: exts.embedding_stores.clone(),
258            artifacts_ep: exts.artifact_stores.clone(),
259        }
260    }
261
262    /// Returns the session store extension point.
263    #[must_use]
264    pub fn sessions_ep(&self) -> &ExtensionPoint<dyn SessionStore> {
265        &self.sessions_ep
266    }
267
268    /// Returns the execution store extension point.
269    #[must_use]
270    pub fn executions_ep(&self) -> &ExtensionPoint<dyn ExecutionStore> {
271        &self.executions_ep
272    }
273
274    /// Returns the run store extension point.
275    #[must_use]
276    pub fn runs_ep(&self) -> &ExtensionPoint<dyn RunStore> {
277        &self.runs_ep
278    }
279
280    /// Returns the embedding store extension point.
281    #[must_use]
282    pub fn embeddings_ep(&self) -> &ExtensionPoint<dyn EmbeddingStore> {
283        &self.embeddings_ep
284    }
285
286    /// Returns the artifact store extension point.
287    #[must_use]
288    pub fn artifacts_ep(&self) -> &ExtensionPoint<dyn ArtifactStore> {
289        &self.artifacts_ep
290    }
291
292    /// Creates or loads a session.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`RuntimeError::SessionNotFound`] when `session_id` is provided
297    /// but does not exist, or [`RuntimeError::Storage`] on persistence failure.
298    pub async fn ensure_session(&self, session_id: Option<Uuid>) -> RuntimeResult<Uuid> {
299        if let Some(id) = session_id {
300            self.sessions
301                .get_session(&id)
302                .await
303                .map_err(RuntimeError::from)?
304                .ok_or(RuntimeError::SessionNotFound(id))?;
305            Ok(id)
306        } else {
307            let session =
308                behest_store::Session::new("Agent Run", behest_provider::ModelName::new("default"));
309            self.sessions
310                .create_session(session.clone())
311                .await
312                .map_err(RuntimeError::from)?;
313            Ok(session.id)
314        }
315    }
316
317    /// Appends a message to a session.
318    ///
319    /// # Errors
320    ///
321    /// Returns [`RuntimeError::Storage`] on persistence failure.
322    pub async fn append_message(&self, session_id: Uuid, message: &Message) -> RuntimeResult<Uuid> {
323        let record = message_to_record(session_id, message);
324        let result = self
325            .sessions
326            .append_message(record)
327            .await
328            .map_err(RuntimeError::from)?;
329        Ok(result.id)
330    }
331
332    /// Lists messages for a session.
333    ///
334    /// # Errors
335    ///
336    /// Returns [`RuntimeError::Storage`] on persistence failure.
337    pub async fn list_messages(&self, session_id: Uuid) -> RuntimeResult<Vec<Message>> {
338        let records = self
339            .sessions
340            .list_messages(&session_id)
341            .await
342            .map_err(RuntimeError::from)?;
343        Ok(records.into_iter().filter_map(record_to_message).collect())
344    }
345}
346
347/// Converts a provider [`Message`] to a persisted [`MessageRecord`].
348///
349/// Maps message role variants to their corresponding store representations,
350/// preserving tool call metadata for assistant and tool messages.
351fn message_to_record(session_id: Uuid, message: &Message) -> MessageRecord {
352    match message {
353        Message::System { content } => {
354            MessageRecord::new(session_id, MessageRole::System, content.clone())
355        }
356        Message::User { content } => {
357            MessageRecord::new(session_id, MessageRole::User, content.clone())
358        }
359        Message::Assistant {
360            content,
361            tool_calls,
362        } => MessageRecord::new(session_id, MessageRole::Assistant, content.clone())
363            .with_tool_calls(tool_calls.clone()),
364        Message::Tool {
365            tool_call_id,
366            name,
367            content,
368        } => MessageRecord::new(session_id, MessageRole::Tool, content.clone())
369            .with_tool_result(tool_call_id.clone(), name.clone()),
370        _ => MessageRecord::new(session_id, MessageRole::User, vec![]),
371    }
372}
373
374/// Converts a stored [`MessageRecord`] back to a provider [`Message`].
375///
376/// Returns `None` for unrecognized role variants. Preserves tool call IDs
377/// and names for tool role messages.
378#[must_use]
379pub fn record_to_message(record: MessageRecord) -> Option<Message> {
380    match record.role {
381        MessageRole::System => Some(Message::System {
382            content: record.content,
383        }),
384        MessageRole::User => Some(Message::User {
385            content: record.content,
386        }),
387        MessageRole::Assistant => Some(Message::Assistant {
388            content: record.content,
389            tool_calls: record.tool_calls,
390        }),
391        MessageRole::Tool => Some(Message::Tool {
392            tool_call_id: record.tool_call_id.unwrap_or_default(),
393            name: record.tool_name.unwrap_or_default(),
394            content: record.content,
395        }),
396        _ => None,
397    }
398}