use crate::error::RunLogError;
use crate::types::{RunLog, StepKind};
use klieo_core::llm::{ChatRequest, LlmClient, Message, Role};
use klieo_core::tool::{ToolCtx, ToolInvoker};
use std::sync::Arc;
pub async fn replay(
run_log: &RunLog,
llm: Arc<dyn LlmClient>,
tools: Arc<dyn ToolInvoker>,
ctx: ToolCtx,
) -> Result<String, RunLogError> {
let mut last_llm_output: Option<String> = None;
for step in &run_log.steps {
match step.kind {
StepKind::LlmCall => {
let prompt = step
.input
.get("prompt")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| step.input.to_string());
let req = ChatRequest::new(vec![Message {
role: Role::User,
content: prompt,
tool_calls: vec![],
tool_call_id: None,
}]);
let resp = llm.complete(req).await.map_err(|e| {
RunLogError::Replay(format!("step {} llm error: {e}", step.idx))
})?;
let live = resp.message.content;
let recorded = step
.output
.as_str()
.map(String::from)
.unwrap_or_else(|| step.output.to_string());
if live != recorded {
return Err(RunLogError::Replay(format!(
"LLM mismatch at step {} (recorded={recorded:?} live={live:?})",
step.idx
)));
}
last_llm_output = Some(live);
}
StepKind::ToolCall => {
let name = step.name.as_deref().unwrap_or("");
let live = tools
.invoke(name, step.input.clone(), ctx.clone())
.await
.map_err(|e| {
RunLogError::Replay(format!("step {} tool error: {e}", step.idx))
})?;
if live != step.output {
return Err(RunLogError::Replay(format!(
"Tool mismatch at step {} (recorded={} live={})",
step.idx, step.output, live
)));
}
}
StepKind::SummaryCheckpoint => {
}
}
}
last_llm_output.ok_or_else(|| RunLogError::Replay("no LlmCall step in RunLog".into()))
}