use std::cell::RefCell;
use std::collections::HashMap;
use crate::value::{DictMap, VmError, VmValue};
thread_local! {
static TURN_STABLE_HOST_CACHE: RefCell<HashMap<String, VmValue>> =
RefCell::new(HashMap::new());
}
fn is_turn_stable(capability: &str, operation: &str) -> bool {
matches!((capability, operation), ("runtime", "pipeline_input"))
}
fn cache_key(capability: &str, operation: &str, params: &DictMap) -> String {
if params.is_empty() {
return format!("{capability}.{operation}");
}
let json = crate::llm::helpers::vm_value_to_json(&VmValue::dict(params.clone()));
format!(
"{capability}.{operation}#{}",
serde_json::to_string(&json).unwrap_or_default()
)
}
pub(crate) fn cached_or<F>(
capability: &str,
operation: &str,
params: &DictMap,
dispatch: F,
) -> Result<Option<VmValue>, VmError>
where
F: FnOnce() -> Result<Option<VmValue>, VmError>,
{
if !is_turn_stable(capability, operation) {
return dispatch();
}
let key = cache_key(capability, operation, params);
if let Some(cached) = TURN_STABLE_HOST_CACHE.with(|cache| cache.borrow().get(&key).cloned()) {
return Ok(Some(cached));
}
let result = dispatch()?;
if let Some(value) = &result {
TURN_STABLE_HOST_CACHE.with(|cache| {
cache.borrow_mut().insert(key, value.clone());
});
}
Ok(result)
}
pub(crate) fn reset() {
TURN_STABLE_HOST_CACHE.with(|cache| cache.borrow_mut().clear());
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::super::{
clear_host_call_bridge, dispatch_host_operation, reset_host_state, set_host_call_bridge,
HostCallBridge,
};
use super::reset;
use crate::value::{DictMap, VmError, VmValue};
struct CountingRuntimeBridge {
counts: Arc<Mutex<std::collections::HashMap<(String, String), usize>>>,
}
impl HostCallBridge for CountingRuntimeBridge {
fn dispatch(
&self,
capability: &str,
operation: &str,
_params: &DictMap,
) -> Result<Option<VmValue>, VmError> {
*self
.counts
.lock()
.unwrap()
.entry((capability.to_string(), operation.to_string()))
.or_insert(0) += 1;
Ok(Some(VmValue::String(arcstr::ArcStr::from(format!(
"{capability}.{operation}"
)))))
}
}
fn run_async<F, Fut>(test: F)
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = ()>,
{
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
rt.block_on(async {
let local = tokio::task::LocalSet::new();
local.run_until(test()).await;
});
}
#[test]
fn turn_stable_host_capability_is_fetched_once_per_turn() {
run_async(|| async {
reset_host_state();
let counts = Arc::new(Mutex::new(std::collections::HashMap::new()));
set_host_call_bridge(Arc::new(CountingRuntimeBridge {
counts: counts.clone(),
}));
let count = |cap: &str, op: &str| -> usize {
counts
.lock()
.unwrap()
.get(&(cap.to_string(), op.to_string()))
.copied()
.unwrap_or(0)
};
for _ in 0..20 {
dispatch_host_operation("runtime", "pipeline_input", &DictMap::new())
.await
.expect("pipeline_input");
}
assert_eq!(
count("runtime", "pipeline_input"),
1,
"20 same-turn reads must hit the host exactly once"
);
for _ in 0..3 {
dispatch_host_operation("runtime", "record_run", &DictMap::new())
.await
.expect("record_run");
}
assert_eq!(
count("runtime", "record_run"),
3,
"writes/non-stable ops must never be served from the turn memo"
);
reset();
for _ in 0..20 {
dispatch_host_operation("runtime", "pipeline_input", &DictMap::new())
.await
.expect("pipeline_input");
}
assert_eq!(
count("runtime", "pipeline_input"),
2,
"a new turn must re-fetch once, not serve the prior turn's value"
);
clear_host_call_bridge();
});
}
}