Skip to main content

anda_core/
context.rs

1//! Execution context traits for agents and tools.
2//!
3//! This module defines the capability traits that an Anda runtime exposes to
4//! agents and tools. Context implementations provide identity, cancellation,
5//! cryptographic keys, isolated storage, caching, and HTTP calls without
6//! requiring each agent or tool to know how those services are implemented.
7//!
8//! The traits are split by capability so custom runtimes can implement only one
9//! coherent execution surface while still keeping the public API explicit:
10//!
11//! - [`BaseContext`] combines the capabilities available to agents and tools.
12//! - [`AgentContext`] extends [`BaseContext`] with completion and orchestration
13//!   features used by agents.
14//! - [`StateFeatures`], [`KeysFeatures`], [`StoreFeatures`], [`CacheFeatures`],
15//!   and [`HttpFeatures`] describe individual groups of runtime services.
16//! - [`CacheStoreFeatures`] provides convenience methods for values that should
17//!   be cached in memory and persisted to object storage.
18//!
19//! The `anda_engine` `context` module provides the default runtime
20//! implementation. Other runtimes can implement these traits for specialized
21//! environments such as tests, embedded workers, or alternative TEE backends.
22
23use async_trait::async_trait;
24use bytes::Bytes;
25use cbor2::{from_slice, to_canonical_vec};
26use serde::{Deserialize, Serialize, de::DeserializeOwned};
27use std::{future::Future, sync::Arc, time::Duration};
28
29pub use anda_db_schema::Json;
30pub use candid::Principal;
31pub use ic_oss_types::object_store::UpdateVersion;
32pub use object_store::{ObjectMeta, PutMode, PutResult, UpdateVersion as OsVersion, path::Path};
33pub use tokio_util::sync::CancellationToken;
34
35use crate::BoxError;
36use crate::model::*;
37
38/// Execution environment available to agents.
39///
40/// `AgentContext` combines the base runtime capabilities with model completion
41/// and orchestration methods for calling local or remote agents and tools.
42pub trait AgentContext: BaseContext + CompletionFeatures {
43    /// Returns definitions for available local tools.
44    ///
45    /// # Arguments
46    /// * `names` - Optional filter for specific tool names.
47    ///
48    /// # Returns
49    /// Vector of function definitions for the requested tools.
50    fn tool_definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition>;
51
52    /// Returns definitions for tools exposed by remote engines.
53    ///
54    /// # Arguments
55    /// * `endpoint` - Optional filter for specific remote engine endpoint;
56    /// * `names` - Optional filter for specific tool names.
57    ///
58    /// # Returns
59    /// Vector of function definitions for the requested tools.
60    fn remote_tool_definitions(
61        &self,
62        endpoint: Option<&str>,
63        names: Option<&[String]>,
64    ) -> impl Future<Output = Result<Vec<FunctionDefinition>, BoxError>> + Send;
65
66    /// Removes and returns resources supported by the named tool.
67    fn select_tool_resources(
68        &self,
69        name: &str,
70        resources: &mut Vec<Resource>,
71    ) -> impl Future<Output = Vec<Resource>> + Send;
72
73    /// Returns definitions for available local agents.
74    ///
75    /// # Arguments
76    /// * `names` - Optional filter for specific agent names;
77    ///
78    /// # Returns
79    /// Vector of function definitions for the requested agents.
80    fn agent_definitions(&self, names: Option<&[String]>) -> Vec<FunctionDefinition>;
81
82    /// Returns definitions for agents exposed by remote engines.
83    ///
84    /// # Arguments
85    /// * `endpoint` - Optional filter for specific remote engine endpoint;
86    /// * `names` - Optional filter for specific agent names.
87    ///
88    /// # Returns
89    /// Vector of function definitions for the requested agents.
90    fn remote_agent_definitions(
91        &self,
92        endpoint: Option<&str>,
93        names: Option<&[String]>,
94    ) -> impl Future<Output = Result<Vec<FunctionDefinition>, BoxError>> + Send;
95
96    /// Removes and returns resources supported by the named agent.
97    fn select_agent_resources(
98        &self,
99        name: &str,
100        resources: &mut Vec<Resource>,
101    ) -> impl Future<Output = Vec<Resource>> + Send;
102
103    /// Returns definitions for all available tools and agents, including remote ones.
104    ///
105    /// # Arguments
106    /// * `names` - Optional filter for specific tool or agent names;
107    ///
108    /// # Returns
109    /// Vector of function definitions for the requested tools and agents.
110    fn definitions(
111        &self,
112        names: Option<&[String]>,
113    ) -> impl Future<Output = Vec<FunctionDefinition>> + Send;
114
115    /// Executes a local tool call.
116    ///
117    /// # Arguments
118    /// * `args` - Tool input arguments, [`ToolInput`].
119    ///
120    /// # Returns
121    /// [`ToolOutput`] containing the final result.
122    fn tool_call(
123        &self,
124        args: ToolInput<Json>,
125    ) -> impl Future<Output = Result<(ToolOutput<Json>, Option<Principal>), BoxError>> + Send;
126
127    /// Runs a local agent.
128    ///
129    /// # Arguments
130    /// * `args` - Agent input arguments, [`AgentInput`].
131    ///
132    /// # Returns
133    /// [`AgentOutput`] containing the result of the agent execution.
134    fn agent_run(
135        self,
136        args: AgentInput,
137    ) -> impl Future<Output = Result<(AgentOutput, Option<Principal>), BoxError>> + Send;
138
139    /// Runs a remote agent via HTTP RPC.
140    ///
141    /// # Arguments
142    /// * `endpoint` - Remote endpoint URL;
143    /// * `args` - Agent input arguments, [`AgentInput`]. The `meta` field will be set by the runtime.
144    ///
145    /// # Returns
146    /// [`AgentOutput`] containing the result of the agent execution.
147    fn remote_agent_run(
148        &self,
149        endpoint: &str,
150        args: AgentInput,
151    ) -> impl Future<Output = Result<AgentOutput, BoxError>> + Send;
152}
153
154/// Core execution environment available to both agents and tools.
155///
156/// `BaseContext` groups state, cryptographic, storage, caching, and HTTP
157/// capabilities behind a single trait bound. Canister access is intentionally
158/// not part of this bound: runtimes that need it implement
159/// [`CanisterCaller`](ic_cose_types::CanisterCaller) separately on their context
160/// type.
161pub trait BaseContext:
162    Sized + StateFeatures + KeysFeatures + StoreFeatures + CacheFeatures + HttpFeatures
163{
164    /// Executes a remote tool call via HTTP RPC.
165    ///
166    /// # Arguments
167    /// * `endpoint` - Remote endpoint URL
168    /// * `args` - Tool input arguments, [`ToolInput`].
169    ///
170    /// # Returns
171    /// [`ToolOutput`] containing the final result.
172    fn remote_tool_call(
173        &self,
174        endpoint: &str,
175        args: ToolInput<Json>,
176    ) -> impl Future<Output = Result<ToolOutput<Json>, BoxError>> + Send;
177}
178
179/// Context metadata available during an agent or tool call.
180pub trait StateFeatures: Sized {
181    /// Returns the engine principal.
182    fn engine_id(&self) -> &Principal;
183
184    /// Returns the engine name.
185    fn engine_name(&self) -> &str;
186
187    /// Returns the verified caller principal if available.
188    /// A non-anonymous principal indicates that the request was verified
189    /// using ICP blockchain's signature verification algorithm.
190    /// Details: <https://github.com/ldclabs/ic-auth>
191    fn caller(&self) -> &Principal;
192
193    /// Returns metadata attached to the current request.
194    fn meta(&self) -> &RequestMeta;
195
196    /// Returns the cancellation token for the current execution context.
197    /// Each call level has its own token scope.
198    /// For example, when an agent calls a tool, the tool receives
199    /// a child token of the agent's token.
200    /// Cancelling the agent token cancels all child calls, while cancelling a
201    /// child token does not affect the parent context.
202    fn cancellation_token(&self) -> CancellationToken;
203
204    /// Returns the time elapsed since the context was created.
205    fn time_elapsed(&self) -> Duration;
206}
207
208/// Cryptographic key operations available to agents and tools.
209///
210/// Runtime implementations derive isolated AES, Ed25519, and Secp256k1 keys
211/// from their root key material. The active agent or tool namespace is included
212/// in derivation paths so identical user-supplied paths remain isolated across
213/// components.
214pub trait KeysFeatures: Sized {
215    /// Derives a 256-bit AES-GCM key from the given derivation path.
216    fn a256gcm_key(
217        &self,
218        derivation_path: Vec<Vec<u8>>,
219    ) -> impl Future<Output = Result<[u8; 32], BoxError>> + Send;
220
221    /// Signs a message using Ed25519 signature scheme from the given derivation path.
222    fn ed25519_sign_message(
223        &self,
224        derivation_path: Vec<Vec<u8>>,
225        message: &[u8],
226    ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
227
228    /// Verifies an Ed25519 signature from the given derivation path.
229    fn ed25519_verify(
230        &self,
231        derivation_path: Vec<Vec<u8>>,
232        message: &[u8],
233        signature: &[u8],
234    ) -> impl Future<Output = Result<(), BoxError>> + Send;
235
236    /// Returns the Ed25519 public key for the given derivation path.
237    fn ed25519_public_key(
238        &self,
239        derivation_path: Vec<Vec<u8>>,
240    ) -> impl Future<Output = Result<[u8; 32], BoxError>> + Send;
241
242    /// Signs a message using Secp256k1 BIP340 Schnorr signature from the given derivation path.
243    fn secp256k1_sign_message_bip340(
244        &self,
245        derivation_path: Vec<Vec<u8>>,
246        message: &[u8],
247    ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
248
249    /// Verifies a Secp256k1 BIP340 Schnorr signature from the given derivation path.
250    fn secp256k1_verify_bip340(
251        &self,
252        derivation_path: Vec<Vec<u8>>,
253        message: &[u8],
254        signature: &[u8],
255    ) -> impl Future<Output = Result<(), BoxError>> + Send;
256
257    /// Signs a SHA-256 digest using Secp256k1 ECDSA from the given derivation path.
258    /// The message will be hashed with SHA-256 before signing.
259    fn secp256k1_sign_message_ecdsa(
260        &self,
261        derivation_path: Vec<Vec<u8>>,
262        message: &[u8],
263    ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
264
265    /// Signs a message using Secp256k1 ECDSA signature from the given derivation path.
266    fn secp256k1_sign_digest_ecdsa(
267        &self,
268        derivation_path: Vec<Vec<u8>>,
269        message_hash: &[u8],
270    ) -> impl Future<Output = Result<[u8; 64], BoxError>> + Send;
271
272    /// Verifies a Secp256k1 ECDSA signature from the given derivation path.
273    fn secp256k1_verify_ecdsa(
274        &self,
275        derivation_path: Vec<Vec<u8>>,
276        message_hash: &[u8],
277        signature: &[u8],
278    ) -> impl Future<Output = Result<(), BoxError>> + Send;
279
280    /// Returns the compressed SEC1-encoded Secp256k1 public key for the given derivation path.
281    fn secp256k1_public_key(
282        &self,
283        derivation_path: Vec<Vec<u8>>,
284    ) -> impl Future<Output = Result<[u8; 33], BoxError>> + Send;
285}
286
287/// Persistent object storage available to agents and tools.
288///
289/// Provides persistent storage capabilities for Agents and Tools to store and manage data.
290/// All operations are asynchronous and return Result types with custom error handling.
291pub trait StoreFeatures: Sized {
292    /// Retrieves data from storage at the specified path.
293    fn store_get(
294        &self,
295        path: &Path,
296    ) -> impl Future<Output = Result<(bytes::Bytes, ObjectMeta), BoxError>> + Send;
297
298    /// Lists objects in storage with optional prefix and offset filters.
299    ///
300    /// # Arguments
301    /// * `prefix` - Optional path prefix to filter results;
302    /// * `offset` - Optional path to start listing from (exclude).
303    fn store_list(
304        &self,
305        prefix: Option<&Path>,
306        offset: &Path,
307    ) -> impl Future<Output = Result<Vec<ObjectMeta>, BoxError>> + Send;
308
309    /// Stores data at the specified path with a given write mode.
310    ///
311    /// # Arguments
312    /// * `path` - Target storage path;
313    /// * `mode` - Write mode (Create, Overwrite, etc.);
314    /// * `value` - Data to store as bytes.
315    fn store_put(
316        &self,
317        path: &Path,
318        mode: PutMode,
319        value: bytes::Bytes,
320    ) -> impl Future<Output = Result<PutResult, BoxError>> + Send;
321
322    /// Renames a storage object if the target path doesn't exist.
323    ///
324    /// # Arguments
325    /// * `from` - Source path;
326    /// * `to` - Destination path.
327    fn store_rename_if_not_exists(
328        &self,
329        from: &Path,
330        to: &Path,
331    ) -> impl Future<Output = Result<(), BoxError>> + Send;
332
333    /// Deletes data at the specified path.
334    ///
335    /// # Arguments
336    /// * `path` - Path of the object to delete.
337    fn store_delete(&self, path: &Path) -> impl Future<Output = Result<(), BoxError>> + Send;
338}
339
340/// Cache expiration policy for cached items.
341#[derive(Debug, Clone)]
342pub enum CacheExpiry {
343    /// Time-to-Live: Entry expires after duration from when it was set.
344    TTL(Duration),
345    /// Time-to-Idle: Entry expires after duration from last access.
346    TTI(Duration),
347}
348
349/// In-memory cache storage available to agents and tools.
350///
351/// Provides isolated in-memory cache storage with TTL/TTI expiration.
352/// Cache data is ephemeral and will be lost on engine restart.
353pub trait CacheFeatures: Sized {
354    /// Checks if a key exists in the cache.
355    fn cache_contains(&self, key: &str) -> bool;
356
357    /// Gets a cached value by key, returns error if not found or deserialization fails.
358    fn cache_get<T>(&self, key: &str) -> impl Future<Output = Result<T, BoxError>> + Send
359    where
360        T: DeserializeOwned;
361
362    /// Gets a cached value or initializes it if missing.
363    ///
364    /// If key doesn't exist, calls init function to create value and cache it.
365    fn cache_get_with<T, F>(
366        &self,
367        key: &str,
368        init: F,
369    ) -> impl Future<Output = Result<T, BoxError>> + Send
370    where
371        T: Sized + DeserializeOwned + Serialize + Send,
372        F: Future<Output = Result<(T, Option<CacheExpiry>), BoxError>> + Send + 'static;
373
374    /// Sets a value in cache with optional expiration policy.
375    fn cache_set<T>(
376        &self,
377        key: &str,
378        val: (T, Option<CacheExpiry>),
379    ) -> impl Future<Output = ()> + Send
380    where
381        T: Sized + Serialize + Send;
382
383    /// Sets a value in cache if key doesn't exist, returns true if set.
384    fn cache_set_if_not_exists<T>(
385        &self,
386        key: &str,
387        val: (T, Option<CacheExpiry>),
388    ) -> impl Future<Output = bool> + Send
389    where
390        T: Sized + Serialize + Send;
391
392    /// Deletes a cached value by key, returns true if key existed.
393    fn cache_delete(&self, key: &str) -> impl Future<Output = bool> + Send;
394
395    /// Returns an iterator over all cached items with raw value.
396    fn cache_raw_iter(
397        &self,
398    ) -> impl Iterator<Item = (Arc<String>, Arc<(Bytes, Option<CacheExpiry>)>)>;
399}
400
401/// HTTP request capabilities available to agents and tools.
402///
403/// All HTTP requests are managed and scheduled by the runtime. Since agents may
404/// run in WASM containers, implementations should not
405/// implement HTTP requests directly.
406pub trait HttpFeatures: Sized {
407    /// Makes an HTTPS request.
408    ///
409    /// # Arguments
410    /// * `url` - Target URL, should start with `https://`;
411    /// * `method` - HTTP method (GET, POST, etc.);
412    /// * `headers` - Optional HTTP headers;
413    /// * `body` - Optional request body (default empty).
414    fn https_call(
415        &self,
416        url: &str,
417        method: http::Method,
418        headers: Option<http::HeaderMap>,
419        body: Option<Vec<u8>>, // default is empty
420    ) -> impl Future<Output = Result<reqwest::Response, BoxError>> + Send;
421
422    /// Makes a signed HTTPS request with message authentication.
423    ///
424    /// # Arguments
425    /// * `url` - Target URL;
426    /// * `method` - HTTP method (GET, POST, etc.);
427    /// * `message_digest` - 32-byte message digest for signing;
428    /// * `headers` - Optional HTTP headers;
429    /// * `body` - Optional request body (default empty).
430    fn https_signed_call(
431        &self,
432        url: &str,
433        method: http::Method,
434        message_digest: [u8; 32],
435        headers: Option<http::HeaderMap>,
436        body: Option<Vec<u8>>,
437    ) -> impl Future<Output = Result<reqwest::Response, BoxError>> + Send;
438
439    /// Makes a signed CBOR-encoded RPC call.
440    ///
441    /// # Arguments
442    /// * `endpoint` - URL endpoint to send the request to;
443    /// * `method` - RPC method name to call;
444    /// * `args` - Arguments to serialize as CBOR and send with the request.
445    fn https_signed_rpc<T>(
446        &self,
447        endpoint: &str,
448        method: &str,
449        args: impl Serialize + Send,
450    ) -> impl Future<Output = Result<T, BoxError>> + Send
451    where
452        T: DeserializeOwned;
453}
454
455#[derive(Clone, Deserialize, Serialize)]
456struct CacheStoreValue<T>(T, UpdateVersion);
457
458/// Convenience methods for values backed by both cache and object storage.
459///
460/// # Consistency
461///
462/// These helpers coordinate a cache and a store as two separate operations and
463/// do **not** provide cross-task linearizability. Concurrent writers to the same
464/// key can interleave and leave the cache holding a stale or rolled-back value
465/// until the entry is refreshed or the process restarts. Cache entries written
466/// here never expire on their own. Callers that need last-writer-wins semantics
467/// under concurrency should use the versioned write path
468/// ([`CacheStoreFeatures::cache_store_set`] with a version), whose
469/// compare-and-swap against the store guards the cache update.
470#[async_trait]
471pub trait CacheStoreFeatures: StoreFeatures + CacheFeatures + Send + Sync + 'static {
472    /// Initializes a cached value from storage, or creates it with `init` if missing.
473    async fn cache_store_init<T, F>(&self, key: &str, init: F) -> Result<(), BoxError>
474    where
475        T: DeserializeOwned + Serialize + Send,
476        F: Future<Output = Result<T, BoxError>> + Send + 'static,
477    {
478        let p = Path::from(key);
479        match self.store_get(&p).await {
480            Ok((v, meta)) => {
481                let val: T = from_slice(&v[..])?;
482                self.cache_set(
483                    key,
484                    (
485                        CacheStoreValue(
486                            val,
487                            UpdateVersion {
488                                e_tag: meta.e_tag,
489                                version: meta.version,
490                            },
491                        ),
492                        None,
493                    ),
494                )
495                .await;
496                Ok(())
497            }
498            Err(_) => {
499                let val: T = init.await?;
500                let data = to_canonical_vec(&val)?;
501                let res = self.store_put(&p, PutMode::Create, data.into()).await?;
502                self.cache_set(
503                    key,
504                    (
505                        CacheStoreValue(
506                            val,
507                            UpdateVersion {
508                                e_tag: res.e_tag,
509                                version: res.version,
510                            },
511                        ),
512                        None,
513                    ),
514                )
515                .await;
516                Ok(())
517            }
518        }
519    }
520
521    /// Returns a value and its storage version, loading it into cache if needed.
522    async fn cache_store_get<T>(&self, key: &str) -> Result<(T, UpdateVersion), BoxError>
523    where
524        T: DeserializeOwned + Serialize + Send + Sync,
525    {
526        match self.cache_get::<CacheStoreValue<T>>(key).await {
527            Ok(CacheStoreValue(val, ver)) => Ok((val, ver)),
528            Err(_) => {
529                // fetch from store and set in cache
530                let p = Path::from(key);
531                let (v, meta) = self.store_get(&p).await?;
532                let val: T = from_slice(&v[..])?;
533                let version = UpdateVersion {
534                    e_tag: meta.e_tag,
535                    version: meta.version,
536                };
537                self.cache_set(key, (CacheStoreValue(&val, version.clone()), None))
538                    .await;
539                Ok((val, version))
540            }
541        }
542    }
543
544    /// Persists a value to storage and updates the cache on success.
545    ///
546    /// When `version` is provided, the write uses an atomic compare-and-swap
547    /// against that storage version, so the subsequent cache update reflects a
548    /// linearized write. Without a version, the value is written with overwrite
549    /// semantics: the store write and cache update are not atomic, so concurrent
550    /// unversioned writers may leave the cache on an older value (see the trait's
551    /// consistency notes). Prefer the versioned path for contended keys.
552    async fn cache_store_set<T>(
553        &self,
554        key: &str,
555        val: T,
556        version: Option<UpdateVersion>,
557    ) -> Result<UpdateVersion, BoxError>
558    where
559        T: DeserializeOwned + Serialize + Send,
560    {
561        let data = to_canonical_vec(&val)?;
562        let p = Path::from(key);
563        if let Some(ver) = version {
564            // atomic update
565            let res = self
566                .store_put(
567                    &p,
568                    PutMode::Update(OsVersion {
569                        e_tag: ver.e_tag.clone(),
570                        version: ver.version.clone(),
571                    }),
572                    data.into(),
573                )
574                .await?;
575            // we can set the cache value after atomic update succeeded
576            let ver = UpdateVersion {
577                e_tag: res.e_tag,
578                version: res.version,
579            };
580            self.cache_set(key, (CacheStoreValue(val, ver.clone()), None))
581                .await;
582            Ok(ver)
583        } else {
584            let res = self.store_put(&p, PutMode::Overwrite, data.into()).await?;
585            let ver = UpdateVersion {
586                e_tag: res.e_tag,
587                version: res.version,
588            };
589            self.cache_set(key, (CacheStoreValue(val, ver.clone()), None))
590                .await;
591            Ok(ver)
592        }
593    }
594
595    /// Deletes a value from both cache and storage.
596    ///
597    /// The store is deleted first, then the cache. Evicting the cache first would
598    /// let a concurrent [`CacheStoreFeatures::cache_store_get`] miss, read the
599    /// still-present store value, and repopulate a ghost cache entry that
600    /// survives the store deletion. Deleting the store first bounds the race to a
601    /// brief stale read that self-heals once the cache entry is removed.
602    async fn cache_store_delete(&self, key: &str) -> Result<(), BoxError> {
603        let p = Path::from(key);
604        self.store_delete(&p).await?;
605        self.cache_delete(key).await;
606        Ok(())
607    }
608}
609
610/// Prefixes a derivation path with the current context path.
611pub fn derivation_path_with(path: &Path, derivation_path: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
612    let mut dp = Vec::with_capacity(derivation_path.len() + 1);
613    dp.push(path.as_ref().as_bytes().to_vec());
614    dp.extend(derivation_path);
615    dp
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621    use futures::executor::block_on;
622    use http::Extensions;
623    use std::{
624        collections::BTreeMap,
625        sync::{
626            Arc, Mutex,
627            atomic::{AtomicUsize, Ordering},
628        },
629    };
630
631    type TestCacheValue = Arc<(Bytes, Option<CacheExpiry>)>;
632    type TestCacheMap = BTreeMap<String, TestCacheValue>;
633
634    #[derive(Default)]
635    struct TestCacheStore {
636        cache: Mutex<TestCacheMap>,
637        store: Mutex<BTreeMap<String, (Bytes, UpdateVersion)>>,
638        store_gets: AtomicUsize,
639        versions: AtomicUsize,
640    }
641
642    impl TestCacheStore {
643        fn put_serialized(&self, key: &str, value: Vec<u8>, version: UpdateVersion) {
644            self.store
645                .lock()
646                .unwrap()
647                .insert(key.to_string(), (value.into(), version));
648        }
649
650        fn next_version(&self) -> UpdateVersion {
651            let version = self.versions.fetch_add(1, Ordering::SeqCst) + 1;
652            UpdateVersion {
653                e_tag: Some(format!("etag-{version}")),
654                version: Some(version.to_string()),
655            }
656        }
657    }
658
659    impl CacheFeatures for TestCacheStore {
660        fn cache_contains(&self, key: &str) -> bool {
661            self.cache.lock().unwrap().contains_key(key)
662        }
663
664        async fn cache_get<T>(&self, key: &str) -> Result<T, BoxError>
665        where
666            T: DeserializeOwned,
667        {
668            let value = self
669                .cache
670                .lock()
671                .unwrap()
672                .get(key)
673                .cloned()
674                .ok_or_else(|| format!("key {key} not found"))?;
675            from_slice(&value.0[..]).map_err(|err| err.into())
676        }
677
678        async fn cache_get_with<T, F>(&self, key: &str, init: F) -> Result<T, BoxError>
679        where
680            T: Sized + DeserializeOwned + Serialize + Send,
681            F: Future<Output = Result<(T, Option<CacheExpiry>), BoxError>> + Send + 'static,
682        {
683            if let Some(value) = self.cache.lock().unwrap().get(key).cloned() {
684                return from_slice(&value.0[..]).map_err(|err| err.into());
685            }
686
687            let (value, expiry) = init.await?;
688            let data = to_canonical_vec(&value)?;
689            self.cache
690                .lock()
691                .unwrap()
692                .insert(key.to_string(), Arc::new((data.into(), expiry)));
693            Ok(value)
694        }
695
696        async fn cache_set<T>(&self, key: &str, val: (T, Option<CacheExpiry>))
697        where
698            T: Sized + Serialize + Send,
699        {
700            let data = to_canonical_vec(&val.0).unwrap();
701            self.cache
702                .lock()
703                .unwrap()
704                .insert(key.to_string(), Arc::new((data.into(), val.1)));
705        }
706
707        async fn cache_set_if_not_exists<T>(&self, key: &str, val: (T, Option<CacheExpiry>)) -> bool
708        where
709            T: Sized + Serialize + Send,
710        {
711            let mut cache = self.cache.lock().unwrap();
712            if cache.contains_key(key) {
713                return false;
714            }
715
716            let data = to_canonical_vec(&val.0).unwrap();
717            cache.insert(key.to_string(), Arc::new((data.into(), val.1)));
718            true
719        }
720
721        async fn cache_delete(&self, key: &str) -> bool {
722            self.cache.lock().unwrap().remove(key).is_some()
723        }
724
725        fn cache_raw_iter(
726            &self,
727        ) -> impl Iterator<Item = (Arc<String>, Arc<(Bytes, Option<CacheExpiry>)>)> {
728            self.cache
729                .lock()
730                .unwrap()
731                .iter()
732                .map(|(key, value)| (Arc::new(key.clone()), value.clone()))
733                .collect::<Vec<_>>()
734                .into_iter()
735        }
736    }
737
738    impl StoreFeatures for TestCacheStore {
739        async fn store_get(&self, path: &Path) -> Result<(bytes::Bytes, ObjectMeta), BoxError> {
740            self.store_gets.fetch_add(1, Ordering::SeqCst);
741            let (value, version) = self
742                .store
743                .lock()
744                .unwrap()
745                .get(path.as_ref())
746                .cloned()
747                .ok_or_else(|| format!("path {path} not found"))?;
748
749            Ok((
750                value.clone(),
751                ObjectMeta {
752                    location: path.clone(),
753                    last_modified: chrono::Utc::now(),
754                    size: value.len() as u64,
755                    e_tag: version.e_tag,
756                    version: version.version,
757                },
758            ))
759        }
760
761        async fn store_list(
762            &self,
763            _prefix: Option<&Path>,
764            _offset: &Path,
765        ) -> Result<Vec<ObjectMeta>, BoxError> {
766            Ok(Vec::new())
767        }
768
769        async fn store_put(
770            &self,
771            path: &Path,
772            mode: PutMode,
773            value: bytes::Bytes,
774        ) -> Result<PutResult, BoxError> {
775            let key = path.as_ref().to_string();
776            let mut store = self.store.lock().unwrap();
777            match mode {
778                PutMode::Create if store.contains_key(&key) => {
779                    return Err(format!("path {path} already exists").into());
780                }
781                PutMode::Update(expected) => {
782                    let Some((_, current)) = store.get(&key) else {
783                        return Err(format!("path {path} not found").into());
784                    };
785                    if current.e_tag != expected.e_tag || current.version != expected.version {
786                        return Err(format!("path {path} version mismatch").into());
787                    }
788                }
789                _ => {}
790            }
791
792            let version = self.next_version();
793            store.insert(key, (value, version.clone()));
794            Ok(PutResult {
795                e_tag: version.e_tag,
796                version: version.version,
797                extensions: Extensions::default(),
798            })
799        }
800
801        async fn store_rename_if_not_exists(&self, from: &Path, to: &Path) -> Result<(), BoxError> {
802            let mut store = self.store.lock().unwrap();
803            let to = to.as_ref().to_string();
804            if store.contains_key(&to) {
805                return Err(format!("path {to} already exists").into());
806            }
807            let value = store
808                .remove(from.as_ref())
809                .ok_or_else(|| format!("path {from} not found"))?;
810            store.insert(to, value);
811            Ok(())
812        }
813
814        async fn store_delete(&self, path: &Path) -> Result<(), BoxError> {
815            self.store.lock().unwrap().remove(path.as_ref());
816            Ok(())
817        }
818    }
819
820    impl CacheStoreFeatures for TestCacheStore {}
821
822    #[test]
823    fn cache_store_get_populates_cache_without_second_store_read() {
824        let ctx = TestCacheStore::default();
825        let stored_version = UpdateVersion {
826            e_tag: Some("etag-stored".to_string()),
827            version: Some("1".to_string()),
828        };
829        let data = to_canonical_vec(&123_u32).unwrap();
830        ctx.put_serialized("answer", data, stored_version.clone());
831
832        let (value, version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
833        assert_eq!(value, 123);
834        assert_eq!(version.e_tag, stored_version.e_tag);
835        assert_eq!(version.version, stored_version.version);
836        assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
837
838        let (value, _) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
839        assert_eq!(value, 123);
840        assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
841    }
842
843    #[test]
844    fn cache_store_set_overwrite_updates_cache() {
845        let ctx = TestCacheStore::default();
846
847        let version = block_on(ctx.cache_store_set("answer", 42_u32, None)).unwrap();
848        assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 0);
849
850        let (value, cached_version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
851        assert_eq!(value, 42);
852        assert_eq!(cached_version.e_tag, version.e_tag);
853        assert_eq!(cached_version.version, version.version);
854        assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 0);
855    }
856
857    #[test]
858    fn cache_store_init_loads_existing_value_and_skips_initializer() {
859        let ctx = TestCacheStore::default();
860        let stored_version = UpdateVersion {
861            e_tag: Some("etag-existing".to_string()),
862            version: Some("7".to_string()),
863        };
864        let data = to_canonical_vec(&"stored".to_string()).unwrap();
865        ctx.put_serialized("message", data, stored_version.clone());
866
867        block_on(ctx.cache_store_init("message", async {
868            Err::<String, BoxError>("initializer should not run".into())
869        }))
870        .unwrap();
871
872        let (value, version) = block_on(ctx.cache_store_get::<String>("message")).unwrap();
873        assert_eq!(value, "stored");
874        assert_eq!(version.e_tag, stored_version.e_tag);
875        assert_eq!(version.version, stored_version.version);
876        assert_eq!(ctx.store_gets.load(Ordering::SeqCst), 1);
877    }
878
879    #[test]
880    fn cache_store_init_creates_missing_value_and_delete_clears_layers() {
881        let ctx = TestCacheStore::default();
882
883        block_on(ctx.cache_store_init("message", async {
884            Ok::<_, BoxError>("created".to_string())
885        }))
886        .unwrap();
887        assert!(ctx.cache_contains("message"));
888        assert!(ctx.store.lock().unwrap().contains_key("message"));
889
890        let (value, _) = block_on(ctx.cache_store_get::<String>("message")).unwrap();
891        assert_eq!(value, "created");
892
893        block_on(ctx.cache_store_delete("message")).unwrap();
894        assert!(!ctx.cache_contains("message"));
895        assert!(!ctx.store.lock().unwrap().contains_key("message"));
896    }
897
898    #[test]
899    fn cache_store_set_update_enforces_expected_version() {
900        let ctx = TestCacheStore::default();
901
902        let version = block_on(ctx.cache_store_set("answer", 1_u32, None)).unwrap();
903        let updated = block_on(ctx.cache_store_set("answer", 2_u32, Some(version))).unwrap();
904        let (value, cached_version) = block_on(ctx.cache_store_get::<u32>("answer")).unwrap();
905        assert_eq!(value, 2);
906        assert_eq!(cached_version.version, updated.version);
907
908        let err = block_on(ctx.cache_store_set(
909            "answer",
910            3_u32,
911            Some(UpdateVersion {
912                e_tag: Some("wrong".to_string()),
913                version: Some("wrong".to_string()),
914            }),
915        ))
916        .unwrap_err();
917        assert!(err.to_string().contains("version mismatch"));
918    }
919
920    #[test]
921    fn cache_and_store_mock_helpers_cover_absent_existing_and_error_paths() {
922        let ctx = TestCacheStore::default();
923
924        let ttl = CacheExpiry::TTL(Duration::from_secs(5));
925        block_on(ctx.cache_set("ttl", ("one".to_string(), Some(ttl.clone()))));
926        assert!(ctx.cache_contains("ttl"));
927
928        let existing = block_on(ctx.cache_set_if_not_exists(
929            "ttl",
930            (
931                "two".to_string(),
932                Some(CacheExpiry::TTI(Duration::from_secs(9))),
933            ),
934        ));
935        assert!(!existing);
936
937        let inserted = block_on(ctx.cache_set_if_not_exists(
938            "tti",
939            (
940                "three".to_string(),
941                Some(CacheExpiry::TTI(Duration::from_secs(9))),
942            ),
943        ));
944        assert!(inserted);
945
946        let mut seen = ctx
947            .cache_raw_iter()
948            .map(|(key, value)| (key.to_string(), value.1.clone()))
949            .collect::<Vec<_>>();
950        seen.sort_by(|a, b| a.0.cmp(&b.0));
951        assert_eq!(seen.len(), 2);
952        let ttl_expiry = seen
953            .iter()
954            .find(|(key, _)| key == "ttl")
955            .and_then(|(_, expiry)| expiry.as_ref())
956            .unwrap();
957        let tti_expiry = seen
958            .iter()
959            .find(|(key, _)| key == "tti")
960            .and_then(|(_, expiry)| expiry.as_ref())
961            .unwrap();
962        match ttl_expiry {
963            CacheExpiry::TTL(duration) => assert_eq!(*duration, Duration::from_secs(5)),
964            CacheExpiry::TTI(_) => panic!("expected ttl"),
965        }
966        match tti_expiry {
967            CacheExpiry::TTI(duration) => assert_eq!(*duration, Duration::from_secs(9)),
968            CacheExpiry::TTL(_) => panic!("expected tti"),
969        }
970
971        let value =
972            block_on(ctx.cache_get_with("lazy", async { Ok::<_, BoxError>((99_u32, None)) }))
973                .unwrap();
974        assert_eq!(value, 99);
975        let cached =
976            block_on(ctx.cache_get_with("lazy", async { Ok::<_, BoxError>((100_u32, None)) }))
977                .unwrap();
978        assert_eq!(cached, 99);
979
980        let first =
981            block_on(ctx.store_put(&Path::from("created"), PutMode::Create, Bytes::from("a")))
982                .unwrap();
983        assert!(first.version.is_some());
984        let err =
985            block_on(ctx.store_put(&Path::from("created"), PutMode::Create, Bytes::from("b")))
986                .unwrap_err();
987        assert!(err.to_string().contains("already exists"));
988
989        block_on(ctx.store_rename_if_not_exists(&Path::from("created"), &Path::from("renamed")))
990            .unwrap();
991        assert!(ctx.store.lock().unwrap().contains_key("renamed"));
992        let err = block_on(
993            ctx.store_rename_if_not_exists(&Path::from("missing"), &Path::from("renamed")),
994        )
995        .unwrap_err();
996        assert!(err.to_string().contains("already exists"));
997        let err = block_on(
998            ctx.store_rename_if_not_exists(&Path::from("missing"), &Path::from("new-destination")),
999        )
1000        .unwrap_err();
1001        assert!(err.to_string().contains("not found"));
1002
1003        let listed =
1004            block_on(ctx.store_list(Some(&Path::from("r")), &Path::from("renamed"))).unwrap();
1005        assert!(listed.is_empty());
1006    }
1007
1008    #[test]
1009    fn derivation_path_with_prefixes_current_path() {
1010        let path = Path::from("agent/main");
1011        let derivation_path = derivation_path_with(&path, vec![b"child".to_vec()]);
1012        assert_eq!(
1013            derivation_path,
1014            vec![b"agent/main".to_vec(), b"child".to_vec()]
1015        );
1016    }
1017}