use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use klieo_bus_memory::MemoryBus;
use klieo_core::agent::AgentContext;
use klieo_core::error::MemoryError;
use klieo_core::llm::{Message, Role};
use klieo_core::memory::{
Episode, EpisodicMemory, Fact, LongTermMemory, RunFilter, RunSummary, Scope, ShortTermMemory,
};
use klieo_core::runtime::{run_steps, RunOptions};
use klieo_core::{FactId, RunId, ThreadId};
use klieo_runlog::replay::scripted_tools_from_runlog;
use klieo_runlog::{Capture, ReplayLlmClient};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
const REDRIVE_THREAD: &str = "klieo-eval-live";
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct LiveEvalMetrics {
pub reproduced: bool,
pub recorded_final: String,
pub replayed_final: Option<String>,
pub divergence: Option<String>,
}
pub async fn eval_capture_live(capture: &Capture) -> LiveEvalMetrics {
let llm = Arc::new(ReplayLlmClient::from_llm_io(
"klieo-eval-live",
&capture.llm_io,
));
let replay_probe = llm.clone();
let tools = Arc::new(scripted_tools_from_runlog(&capture.run_log));
let bus = MemoryBus::new();
let thread = ThreadId::new(REDRIVE_THREAD);
let initial = capture
.llm_io
.first()
.map(|io| io.prompt.clone())
.unwrap_or_default();
let short_term = Arc::new(LiveShortTerm::seeded(
thread.clone(),
Message {
role: Role::User,
content: initial,
tool_calls: vec![],
tool_call_id: None,
},
));
let ctx = AgentContext::new(
llm,
short_term,
Arc::new(NoopLongTerm),
Arc::new(NoopEpisodic),
bus.pubsub.clone(),
bus.kv.clone(),
bus.request_reply.clone(),
bus.jobs.clone(),
tools,
RunId::new(),
CancellationToken::new(),
capture.run_log.agent.clone(),
);
let recorded_final = capture
.llm_io
.last()
.map(|io| io.completion.clone())
.unwrap_or_default();
match run_steps(&ctx, "", thread, RunOptions::default()).await {
Ok(replayed) => {
let undrained = replay_probe.remaining();
if undrained > 0 {
return LiveEvalMetrics {
reproduced: false,
recorded_final,
replayed_final: Some(replayed),
divergence: Some(format!(
"re-drive consumed fewer LLM calls than recorded: {undrained} recorded \
response(s) left un-replayed"
)),
};
}
LiveEvalMetrics {
reproduced: replayed == recorded_final,
recorded_final,
replayed_final: Some(replayed),
divergence: None,
}
}
Err(err) => LiveEvalMetrics {
reproduced: false,
recorded_final,
replayed_final: None,
divergence: Some(err.to_string()),
},
}
}
#[derive(Default)]
struct LiveShortTerm {
threads: Mutex<HashMap<ThreadId, Vec<Message>>>,
}
impl LiveShortTerm {
fn seeded(thread: ThreadId, message: Message) -> Self {
let mut threads = HashMap::new();
threads.insert(thread, vec![message]);
Self {
threads: Mutex::new(threads),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<ThreadId, Vec<Message>>> {
self.threads.lock().expect("LiveShortTerm mutex poisoned")
}
}
#[async_trait]
impl ShortTermMemory for LiveShortTerm {
async fn append(&self, thread: ThreadId, msg: Message) -> Result<(), MemoryError> {
self.lock().entry(thread).or_default().push(msg);
Ok(())
}
async fn load(
&self,
thread: ThreadId,
_max_tokens: usize,
) -> Result<Vec<Message>, MemoryError> {
Ok(self.lock().get(&thread).cloned().unwrap_or_default())
}
async fn clear(&self, thread: ThreadId) -> Result<(), MemoryError> {
self.lock().remove(&thread);
Ok(())
}
}
struct NoopEpisodic;
#[async_trait]
impl EpisodicMemory for NoopEpisodic {
async fn record(&self, _run: RunId, _event: Episode) -> Result<(), MemoryError> {
Ok(())
}
async fn replay(&self, _run: RunId) -> Result<Vec<Episode>, MemoryError> {
Ok(Vec::new())
}
async fn list_runs(&self, _filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError> {
Ok(Vec::new())
}
}
struct NoopLongTerm;
#[async_trait]
impl LongTermMemory for NoopLongTerm {
async fn remember(&self, _scope: Scope, _fact: Fact) -> Result<FactId, MemoryError> {
Ok(FactId::new("klieo-eval-live-noop"))
}
async fn recall(
&self,
_scope: Scope,
_query: &str,
_k: usize,
) -> Result<Vec<Fact>, MemoryError> {
Ok(Vec::new())
}
async fn forget(&self, _id: FactId) -> Result<(), MemoryError> {
Ok(())
}
}