mod types;
use async_trait::async_trait;
use serde_json::Value;
pub use types::*;
use crate::error::{Result, RustAgentsError};
use crate::harness::model::{ModelRequest, ModelResponse};
fn fnv1a_hex(data: &[u8]) -> String {
const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
const PRIME: u64 = 1_099_511_628_211;
let mut hash = OFFSET_BASIS;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(PRIME);
}
format!("{hash:016x}")
}
fn canonical_value(v: Value) -> Value {
match v {
Value::Object(map) => {
let mut pairs: Vec<(String, Value)> = map.into_iter().collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
Value::Object(
pairs
.into_iter()
.map(|(k, val)| (k, canonical_value(val)))
.collect(),
)
}
Value::Array(arr) => Value::Array(arr.into_iter().map(canonical_value).collect()),
other => other,
}
}
pub fn cache_key(request: &ModelRequest) -> String {
let value = serde_json::to_value(request).unwrap_or(Value::Null);
let canonical = canonical_value(value);
let bytes = serde_json::to_vec(&canonical).unwrap_or_default();
fnv1a_hex(&bytes)
}
impl InMemoryResponseCache {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl ResponseCache for InMemoryResponseCache {
async fn get(&self, key: &str) -> Result<Option<ModelResponse>> {
let data = self
.data
.lock()
.map_err(|e| RustAgentsError::Validation(format!("cache lock poisoned: {e}")))?;
Ok(data.get(key).cloned())
}
async fn put(&self, key: &str, value: ModelResponse) -> Result<()> {
let mut data = self
.data
.lock()
.map_err(|e| RustAgentsError::Validation(format!("cache lock poisoned: {e}")))?;
data.insert(key.to_string(), value);
Ok(())
}
}
impl PromptCacheLayout {
pub fn from_request(request: &ModelRequest) -> Self {
let prefix_ids: Vec<String> = request.cacheable_prefix_ids();
let fingerprint = fnv1a_hex(prefix_ids.join(",").as_bytes());
Self {
prefix_ids,
fingerprint,
}
}
pub fn prefix_ids(&self) -> &[String] {
&self.prefix_ids
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn is_prefix_stable_against(&self, other: &PromptCacheLayout) -> bool {
self.prefix_ids == other.prefix_ids
}
}
impl CacheLayoutEvent {
pub fn new(before: &PromptCacheLayout, after: &PromptCacheLayout) -> Self {
Self {
changed_prefix: !before.is_prefix_stable_against(after),
volatile_only: after.prefix_ids().is_empty(),
segment_ids_before: before.prefix_ids().to_vec(),
segment_ids_after: after.prefix_ids().to_vec(),
}
}
}
#[cfg(test)]
mod test;