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}