mod types;
use async_trait::async_trait;
use serde_json::Value;
use sha2::{Digest, Sha256};
pub use types::*;
use crate::error::{Result, TinyAgentsError};
use crate::harness::model::{ModelRequest, ModelResponse};
fn hex_digest(digest: impl AsRef<[u8]>) -> String {
digest
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn fold_canonical(hasher: &mut Sha256, tag: u8, value: Value) {
let bytes = serde_json::to_vec(&canonical_value(value)).unwrap_or_default();
hasher.update([tag]);
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
}
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 mut hasher = Sha256::new();
let mut root = serde_json::to_value(request).unwrap_or(Value::Null);
if let Value::Object(map) = &mut root {
if let Some(Value::Array(messages)) = map.remove("messages") {
hasher.update(b"M");
hasher.update((messages.len() as u64).to_le_bytes());
for message in messages {
fold_canonical(&mut hasher, b'm', message);
}
}
if let Some(Value::Array(tools)) = map.remove("tools") {
hasher.update(b"T");
hasher.update((tools.len() as u64).to_le_bytes());
for tool in tools {
fold_canonical(&mut hasher, b't', tool);
}
}
}
fold_canonical(&mut hasher, b'E', root);
hex_digest(hasher.finalize())
}
impl InMemoryResponseCache {
pub const DEFAULT_CAPACITY: usize = 1024;
pub fn new() -> Self {
Self::with_capacity(Self::DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: std::sync::Arc::new(std::sync::Mutex::new(LruResponseMap {
data: std::collections::HashMap::new(),
order: std::collections::VecDeque::new(),
capacity: capacity.max(1),
})),
}
}
}
impl Default for InMemoryResponseCache {
fn default() -> Self {
Self::new()
}
}
impl LruResponseMap {
fn touch(&mut self, key: &str) {
if let Some(pos) = self.order.iter().position(|k| k == key) {
let k = self.order.remove(pos).expect("position is valid");
self.order.push_back(k);
}
}
}
#[async_trait]
impl ResponseCache for InMemoryResponseCache {
async fn get(&self, key: &str) -> Result<Option<ModelResponse>> {
let mut inner = self
.inner
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("cache lock poisoned: {e}")))?;
let hit = inner.data.get(key).cloned();
if hit.is_some() {
inner.touch(key);
}
Ok(hit)
}
async fn put(&self, key: &str, value: ModelResponse) -> Result<()> {
let mut inner = self
.inner
.lock()
.map_err(|e| TinyAgentsError::Validation(format!("cache lock poisoned: {e}")))?;
if inner.data.insert(key.to_string(), value).is_some() {
inner.touch(key);
} else {
inner.order.push_back(key.to_string());
while inner.order.len() > inner.capacity {
if let Some(evicted) = inner.order.pop_front() {
inner.data.remove(&evicted);
}
}
}
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;