Skip to main content

conversation_api/execution/
storage.rs

1//! Persistence contracts expressed in Agent consistency semantics.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::execution::{
8    ConversationSurface, DurableEvent, InvocationContext, Message, QueuedRun, RunId, Scope,
9    ThreadId,
10};
11
12/// Monotonic revision used for optimistic concurrency control.
13#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct Revision(pub u64);
16
17impl Revision {
18    /// Revision expected when creating a new thread record.
19    pub const INITIAL: Self = Self(0);
20
21    /// Returns the next revision, if representable.
22    #[must_use]
23    pub fn next(self) -> Option<Self> {
24        self.0.checked_add(1).map(Self)
25    }
26}
27
28/// Fully scoped key for one durable Agent thread.
29#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
30pub struct ThreadKey {
31    /// Authorization and persistence scope.
32    pub scope: Scope,
33    /// Canonical Conversation-space identity and delivery route.
34    pub surface_id: ConversationSurface,
35    /// Thread identifier within the scope.
36    pub thread_id: ThreadId,
37}
38
39/// Opaque control-state checkpoint bytes owned and versioned by Runtime.
40///
41/// Prompt-visible history is intentionally stored separately as an append-only journal so a
42/// control transition does not rewrite every preceding message.
43#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
44pub struct Checkpoint {
45    /// Runtime checkpoint schema version.
46    pub format_version: u32,
47    /// Serialized checkpoint payload.
48    pub bytes: Vec<u8>,
49}
50
51/// Materialized Runtime state used when seeding or exporting one thread generation.
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53pub struct RuntimeSnapshot {
54    /// Small mutable execution-control checkpoint.
55    pub checkpoint: Checkpoint,
56    /// Prompt-visible history materialized from the append-only journal.
57    pub history: Vec<Message>,
58}
59
60/// Current durable state of one thread.
61#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62pub struct ThreadRecord {
63    /// Current optimistic concurrency revision.
64    pub revision: Revision,
65    /// Latest Runtime checkpoint.
66    pub checkpoint: Checkpoint,
67    /// Prompt-visible history materialized from the append-only journal.
68    pub history: Vec<Message>,
69    /// Event committed with this generation but not yet acknowledged by the downstream consumer.
70    pub pending_event: Option<DurableEvent>,
71}
72
73/// Prompt-history mutation committed atomically with Runtime control state.
74#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
75#[serde(tag = "type", content = "messages", rename_all = "snake_case")]
76pub enum HistoryMutation {
77    /// Adds rows to the currently materialized history.
78    Append(Vec<Message>),
79    /// Replaces the materialized history after deterministic window truncation.
80    Replace(Vec<Message>),
81}
82
83/// One atomic state transition requested by Runtime.
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct ThreadCommit {
86    /// Lease fencing token required by distributed implementations.
87    pub fencing_token: Option<u64>,
88    /// Replacement Runtime checkpoint.
89    pub checkpoint: Checkpoint,
90    /// Atomic prompt-history append or replacement.
91    pub history: HistoryMutation,
92    /// Exact pending event durably acknowledged by the downstream consumer.
93    pub acknowledge_event: Option<crate::execution::EventId>,
94    /// Event that becomes durable with the checkpoint revision.
95    pub event: Option<DurableEvent>,
96    /// Context indexed by distributed deployments while this checkpoint or its outbox needs recovery.
97    /// `None` removes the thread from the runnable recovery index.
98    pub recovery_context: Option<InvocationContext>,
99}
100
101/// Outcome of an optimistic thread commit.
102#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
103#[serde(tag = "type", rename_all = "snake_case")]
104pub enum CommitOutcome {
105    /// The transition was committed at the returned revision.
106    Applied {
107        /// Newly committed revision.
108        revision: Revision,
109    },
110    /// Another writer advanced the thread before this commit.
111    Conflict {
112        /// Current durable revision.
113        actual: Revision,
114    },
115    /// The writer no longer owns the distributed execution lease.
116    StaleFence {
117        /// Current fencing token observed by the storage implementation.
118        actual: u64,
119    },
120}
121
122/// Stable persistence failure independent of File, `SQLite`, S3, or `DynamoDB` details.
123#[derive(Clone, Debug, Error, Eq, PartialEq)]
124pub enum StoreError {
125    /// The requested state transition violates the storage contract.
126    #[error("invalid durable state transition: {message}")]
127    InvalidInput {
128        /// Safe diagnostic message.
129        message: String,
130    },
131    /// Durable state cannot be decoded or fails integrity validation.
132    #[error("corrupt durable state: {message}")]
133    Corrupt {
134        /// Safe diagnostic message.
135        message: String,
136    },
137    /// The authenticated scope cannot access the requested state.
138    #[error("durable state access denied")]
139    PermissionDenied,
140    /// The storage dependency is temporarily unavailable.
141    #[error("durable state unavailable: {message}")]
142    Unavailable {
143        /// Safe diagnostic message.
144        message: String,
145    },
146    /// The adapter failed without a more specific stable classification.
147    #[error("durable state failure: {message}")]
148    Internal {
149        /// Safe diagnostic message.
150        message: String,
151    },
152}
153
154/// Atomic thread state repository.
155#[async_trait]
156pub trait ThreadStore: Send + Sync {
157    /// Loads the current thread record.
158    async fn load(&self, key: &ThreadKey) -> Result<Option<ThreadRecord>, StoreError>;
159
160    /// Commits a checkpoint and its optional durable event if `expected` and the fencing token are current.
161    ///
162    /// Distributed implementations must validate `fencing_token` against the authoritative lease,
163    /// so a paused former owner cannot commit after a new owner acquires the thread.
164    async fn commit(
165        &self,
166        key: &ThreadKey,
167        expected: Revision,
168        transition: ThreadCommit,
169    ) -> Result<CommitOutcome, StoreError>;
170}
171
172/// Versioned process-local execution queue kept separate from both the durable
173/// user waiting queue and the Runtime checkpoint.
174#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
175pub struct QueueSnapshot {
176    /// Current optimistic concurrency revision.
177    pub revision: Revision,
178    /// Requests waiting to enter the Runtime loop.
179    pub items: Vec<QueuedRun>,
180}
181
182/// Outcome of claiming the next queued request without destructively removing it.
183#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
184#[serde(tag = "type", rename_all = "snake_case")]
185pub enum ClaimNextOutcome {
186    /// The queue was empty at the expected revision.
187    Empty,
188    /// The head request was claimed and returned.
189    Claimed {
190        /// New queue revision after recording the claim.
191        revision: Revision,
192        /// Claimed request. It remains in process memory until `ack_started` succeeds.
193        request: Box<QueuedRun>,
194    },
195    /// Another writer changed the queue first.
196    Conflict {
197        /// Current queue revision.
198        actual: Revision,
199    },
200}
201
202/// Process-local execution admission queue for one thread.
203///
204/// The deployment-owned user waiting queue is not this interface: `MeowCore` and
205/// Lion persist it before dispatching one attempt into Runtime. Losing this
206/// store loses the attempt and must never trigger cross-process replay.
207#[async_trait]
208pub trait QueueStore: Send + Sync {
209    /// Loads the current queue snapshot.
210    async fn load(&self, key: &ThreadKey) -> Result<QueueSnapshot, StoreError>;
211
212    /// Enqueues a run idempotently by `operation_id` and `run_id`.
213    ///
214    /// Implementations preserve idempotency for the lifetime of the process.
215    async fn enqueue(
216        &self,
217        key: &ThreadKey,
218        request: QueuedRun,
219    ) -> Result<QueueSnapshot, StoreError>;
220
221    /// Claims the first unclaimed queue item at `expected` revision.
222    ///
223    /// `claimant` identifies the single process-local driver attempt. A lost
224    /// attempt is failed by the deployment owner and is never reclaimed here.
225    async fn claim_next(
226        &self,
227        key: &ThreadKey,
228        expected: Revision,
229        claimant: &str,
230    ) -> Result<ClaimNextOutcome, StoreError>;
231
232    /// Removes an item after the same run has been committed as the active thread checkpoint.
233    ///
234    /// Implementations must make repeated acknowledgements for the same run idempotent.
235    async fn ack_started(
236        &self,
237        key: &ThreadKey,
238        run_id: &RunId,
239    ) -> Result<QueueSnapshot, StoreError>;
240
241    /// Removes a queued run idempotently and returns the updated snapshot.
242    async fn remove(&self, key: &ThreadKey, run_id: &RunId) -> Result<QueueSnapshot, StoreError>;
243}
244
245/// Process-local cooperative cancellation signal for a live execution attempt.
246#[async_trait]
247pub trait RunControl: Send + Sync {
248    /// Requests cancellation idempotently.
249    ///
250    /// The full invocation context prevents collisions between users, scopes,
251    /// threads, and runs inside one process.
252    async fn request_cancel(
253        &self,
254        key: &ThreadKey,
255        context: &InvocationContext,
256    ) -> Result<(), StoreError>;
257
258    /// Returns whether cancellation has been requested.
259    async fn is_cancel_requested(
260        &self,
261        key: &ThreadKey,
262        run_id: &RunId,
263    ) -> Result<bool, StoreError>;
264
265    /// Waits until cancellation is requested, allowing Runtime to drop an in-flight external
266    /// operation and stop the execution attempt.
267    async fn wait_for_cancel(&self, key: &ThreadKey, run_id: &RunId) -> Result<(), StoreError>;
268
269    /// Clears the cancellation signal after a run has settled.
270    async fn clear_cancel(&self, key: &ThreadKey, run_id: &RunId) -> Result<(), StoreError>;
271}
272
273/// Lease proving exclusive ownership of a thread run.
274#[derive(Clone, Debug, Eq, PartialEq)]
275pub struct RunLease {
276    /// Thread guarded by the lease.
277    pub key: ThreadKey,
278    /// Owning run.
279    pub run_id: RunId,
280    /// Opaque fencing token that increases across owners.
281    pub fencing_token: u64,
282    /// Absolute Unix expiration in milliseconds.
283    pub expires_at_unix_ms: u64,
284}
285
286/// Coordinates exclusive execution of one thread inside one live process.
287#[async_trait]
288pub trait RunCoordinator: Send + Sync {
289    /// Attempts to acquire a process-local lease for a run.
290    async fn acquire(
291        &self,
292        key: &ThreadKey,
293        run_id: &RunId,
294        lease_ms: u64,
295    ) -> Result<Option<RunLease>, StoreError>;
296
297    /// Renews a lease if the caller still owns its fencing token.
298    async fn renew(&self, lease: &RunLease, lease_ms: u64) -> Result<Option<RunLease>, StoreError>;
299
300    /// Releases a lease if the caller still owns its fencing token.
301    async fn release(&self, lease: &RunLease) -> Result<(), StoreError>;
302}