pub mod classification;
mod optimizer;
pub use optimizer::{
ContextOptimizer, OptimizationConfig, PriorityOptimizer, RecencyOptimizer, TruncationOptimizer,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Clone, Debug)]
pub struct CancelSignal(Arc<AtomicBool>);
impl CancelSignal {
pub fn new() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
pub fn cancel(&self) {
self.0.store(true, Ordering::SeqCst);
}
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
pub fn reset(&self) {
self.0.store(false, Ordering::SeqCst);
}
}
impl Default for CancelSignal {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct HookContext {
pub request_id: String,
pub started_at: Instant,
pub cancel_signal: CancelSignal,
pub span: tracing::Span,
extras: HashMap<String, serde_json::Value>,
}
impl HookContext {
pub fn new(request_id: impl Into<String>) -> Self {
let request_id = request_id.into();
Self {
span: tracing::info_span!("hook_chain", request_id = %request_id),
request_id,
started_at: Instant::now(),
cancel_signal: CancelSignal::new(),
extras: HashMap::new(),
}
}
pub fn new_with_uuid() -> Self {
Self::new(uuid::Uuid::new_v4().to_string())
}
pub fn set<T: serde::Serialize>(
&mut self,
key: &str,
value: T,
) -> Result<(), serde_json::Error> {
self.extras
.insert(key.to_string(), serde_json::to_value(value)?);
Ok(())
}
pub fn get<T: serde::de::DeserializeOwned>(
&self,
key: &str,
) -> Option<Result<T, serde_json::Error>> {
self.extras
.get(key)
.map(|v| serde_json::from_value(v.clone()))
}
pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
self.extras.remove(key)
}
pub fn contains(&self, key: &str) -> bool {
self.extras.contains_key(key)
}
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
pub fn is_cancelled(&self) -> bool {
self.cancel_signal.is_cancelled()
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.extras.keys().map(|s| s.as_str())
}
pub fn next_tool_call_id(&mut self) -> String {
const TOOL_CALL_COUNTER_KEY: &str = "__tool_call_counter";
let counter: u32 = self
.get(TOOL_CALL_COUNTER_KEY)
.and_then(|r| r.ok())
.unwrap_or(0);
let next = counter + 1;
let _ = self.set(TOOL_CALL_COUNTER_KEY, next);
format!("tool_call_{}", next)
}
}
#[derive(Debug, Default)]
pub struct SharedContext {
data: HashMap<String, serde_json::Value>,
}
impl SharedContext {
pub fn new() -> Self {
Self::default()
}
pub fn get<T: serde::de::DeserializeOwned>(
&self,
key: &str,
) -> Option<Result<T, serde_json::Error>> {
self.data
.get(key)
.map(|v| serde_json::from_value(v.clone()))
}
pub fn set<T: serde::Serialize>(
&mut self,
key: &str,
value: T,
) -> Result<(), serde_json::Error> {
self.data
.insert(key.to_string(), serde_json::to_value(value)?);
Ok(())
}
pub fn remove(&mut self, key: &str) -> Option<serde_json::Value> {
self.data.remove(key)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.data.keys().map(|s| s.as_str())
}
pub fn contains(&self, key: &str) -> bool {
self.data.contains_key(key)
}
pub fn clear(&mut self) {
self.data.clear();
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
pub type SharedContextHandle = Arc<RwLock<SharedContext>>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cancel_signal() {
let signal = CancelSignal::new();
assert!(!signal.is_cancelled());
signal.cancel();
assert!(signal.is_cancelled());
signal.reset();
assert!(!signal.is_cancelled());
}
#[test]
fn test_hook_context_set_get() {
let mut ctx = HookContext::new("test-123");
ctx.set("key1", "value1").unwrap();
ctx.set("key2", 42i32).unwrap();
assert_eq!(ctx.get::<String>("key1").unwrap().unwrap(), "value1");
assert_eq!(ctx.get::<i32>("key2").unwrap().unwrap(), 42);
assert!(ctx.get::<String>("nonexistent").is_none());
}
#[test]
fn test_hook_context_remove() {
let mut ctx = HookContext::new("test-123");
ctx.set("key", "value").unwrap();
assert!(ctx.contains("key"));
ctx.remove("key");
assert!(!ctx.contains("key"));
}
#[test]
fn test_shared_context() {
let mut ctx = SharedContext::new();
ctx.set("user_id", "user-123").unwrap();
ctx.set("count", 5i32).unwrap();
assert_eq!(ctx.get::<String>("user_id").unwrap().unwrap(), "user-123");
assert_eq!(ctx.get::<i32>("count").unwrap().unwrap(), 5);
assert_eq!(ctx.len(), 2);
ctx.remove("user_id");
assert_eq!(ctx.len(), 1);
ctx.clear();
assert!(ctx.is_empty());
}
#[test]
fn test_next_tool_call_id() {
let mut ctx = HookContext::new("test-123");
assert_eq!(ctx.next_tool_call_id(), "tool_call_1");
assert_eq!(ctx.next_tool_call_id(), "tool_call_2");
assert_eq!(ctx.next_tool_call_id(), "tool_call_3");
let mut ctx2 = HookContext::new("test-456");
assert_eq!(ctx2.next_tool_call_id(), "tool_call_1");
}
}