Skip to main content

a3s_memory/repository/
store.rs

1use super::{
2    MemoryAccessEvent, MemoryChangeResult, MemoryChangeSet, MemoryNamespace,
3    MemoryNamespaceChangeToken, MemoryNamespaceSnapshot, MemoryNode, MemoryQuery,
4    MemoryQueryResult, MemoryRepositoryError, MemorySnapshotRequest, MemoryUsageSummary,
5    MAX_QUERY_LIMIT,
6};
7
8/// Repository contract for evidence-backed durable memory.
9#[async_trait::async_trait]
10pub trait MemoryRepository: Send + Sync {
11    /// Atomically apply a bounded, revision-checked change set.
12    async fn apply(
13        &self,
14        change_set: MemoryChangeSet,
15    ) -> Result<MemoryChangeResult, MemoryRepositoryError>;
16
17    /// Read one node from an exact namespace without recording access.
18    async fn get(
19        &self,
20        namespace: &MemoryNamespace,
21        node_id: &str,
22    ) -> Result<Option<MemoryNode>, MemoryRepositoryError>;
23
24    /// Query one exact namespace without mutating repository state.
25    async fn query(&self, query: MemoryQuery) -> Result<MemoryQueryResult, MemoryRepositoryError>;
26
27    /// Capture one complete, bounded, deterministic namespace view.
28    ///
29    /// The default implementation is exact below the ordinary query horizon.
30    /// Backends that can atomically enumerate larger views should override it.
31    async fn snapshot_namespace(
32        &self,
33        request: MemorySnapshotRequest,
34    ) -> Result<MemoryNamespaceSnapshot, MemoryRepositoryError> {
35        request.validate()?;
36        let query_limit = request.max_nodes.saturating_add(1).min(MAX_QUERY_LIMIT);
37        let result = self
38            .query(
39                MemoryQuery::new(request.namespace.clone())
40                    .with_statuses(request.statuses.iter().copied())
41                    .with_limit(query_limit),
42            )
43            .await?;
44        if request.max_nodes >= MAX_QUERY_LIMIT && result.hits.len() == MAX_QUERY_LIMIT {
45            return Err(MemoryRepositoryError::LimitExceeded {
46                resource: "namespace snapshot query horizon".into(),
47                limit: MAX_QUERY_LIMIT - 1,
48                actual: MAX_QUERY_LIMIT,
49            });
50        }
51        super::snapshot::snapshot_from_nodes(
52            request,
53            result.hits.into_iter().map(|hit| hit.node).collect(),
54        )
55    }
56
57    /// Return an optional exact-namespace change token.
58    ///
59    /// Some opts into the MemoryNamespaceChangeToken contract: every novel
60    /// successful apply that changes node state in this namespace must publish
61    /// a different token at the same linearization point. Tokens must remain
62    /// stable across reads, idempotent replay, access events, and durable
63    /// restart. Backends that cannot make those guarantees return None.
64    async fn namespace_change_token(
65        &self,
66        namespace: &MemoryNamespace,
67    ) -> Result<Option<MemoryNamespaceChangeToken>, MemoryRepositoryError> {
68        namespace.validate()?;
69        Ok(None)
70    }
71
72    /// Record that the host admitted the current active node revision into a model context.
73    async fn record_admission(&self, event: MemoryAccessEvent)
74        -> Result<(), MemoryRepositoryError>;
75
76    /// Record that the host cited, selected, or otherwise used a node.
77    async fn record_use(&self, event: MemoryAccessEvent) -> Result<(), MemoryRepositoryError>;
78
79    /// Return explicit admission and use counts for a node.
80    async fn usage_summary(
81        &self,
82        namespace: &MemoryNamespace,
83        node_id: &str,
84    ) -> Result<MemoryUsageSummary, MemoryRepositoryError>;
85}