use crate::pricing::PriceTable;
use crate::types::{Cost, LlmIo, RunLog, RunStatus, Step, StepKind, Usage};
use chrono::Utc;
use klieo_core::ids::RunId;
use klieo_core::memory::{Episode, ToolResult};
use std::time::Duration;
pub fn project(run_id: RunId, agent: impl Into<String>, episodes: &[Episode]) -> RunLog {
project_with_llm_io(run_id, agent, episodes, &[])
}
pub fn project_with_llm_io(
run_id: RunId,
agent: impl Into<String>,
episodes: &[Episode],
llm_io: &[LlmIo],
) -> RunLog {
project_with_price_table(run_id, agent, episodes, llm_io, &PriceTable::new())
}
pub fn project_with_price_table(
run_id: RunId,
agent: impl Into<String>,
episodes: &[Episode],
llm_io: &[LlmIo],
prices: &PriceTable,
) -> RunLog {
let now = Utc::now();
let mut log = RunLog {
run_id,
agent: agent.into(),
started_at: now,
finished_at: None,
status: RunStatus::Running,
steps: Vec::new(),
tokens: Usage::default(),
cost_estimate: None,
};
let mut idx: u32 = 0;
let mut llm_idx: usize = 0;
let mut prompt_usd_acc: f64 = 0.0;
let mut completion_usd_acc: f64 = 0.0;
let mut any_priced_step: bool = false;
for ep in episodes {
match ep {
Episode::Started { .. } => {
log.started_at = now;
}
Episode::LlmCall {
tokens,
latency_ms,
model,
prompt_tokens,
completion_tokens,
..
} => {
let io = llm_io.get(llm_idx);
let (input, output, name) = match io {
Some(io) => (
serde_json::json!({ "prompt": io.prompt }),
serde_json::Value::String(io.completion.clone()),
io.model.clone().or_else(|| model.clone()),
),
None => (
serde_json::Value::Null,
serde_json::Value::Null,
model.clone(),
),
};
log.steps.push(Step {
idx,
kind: StepKind::LlmCall,
name,
input,
output,
error: None,
latency: Duration::from_millis(u64::from(*latency_ms)),
span_id: None,
});
match (*prompt_tokens, *completion_tokens) {
(Some(pt), Some(ct)) => {
log.tokens.prompt_tokens = log.tokens.prompt_tokens.saturating_add(pt);
log.tokens.completion_tokens =
log.tokens.completion_tokens.saturating_add(ct);
}
_ => {
log.tokens.completion_tokens =
log.tokens.completion_tokens.saturating_add(*tokens);
}
}
if let Some(io) = io {
if let Some(c) = step_cost(io, prices) {
prompt_usd_acc += c.prompt_usd;
completion_usd_acc += c.completion_usd;
any_priced_step = true;
}
}
idx += 1;
llm_idx += 1;
}
Episode::ToolCall { name, args, result } => {
let (output, error) = match result {
ToolResult::Ok { value } => (value.clone(), None),
ToolResult::Err { message } => (serde_json::Value::Null, Some(message.clone())),
};
log.steps.push(Step {
idx,
kind: StepKind::ToolCall,
name: Some(name.clone()),
input: args.clone(),
output,
error,
latency: Duration::ZERO, span_id: None,
});
idx += 1;
}
Episode::BusPublish { .. } | Episode::BusReceive { .. } => {
}
Episode::Completed => {
log.status = RunStatus::Completed;
log.finished_at = Some(Utc::now());
}
Episode::Failed { .. } => {
log.status = RunStatus::Failed;
log.finished_at = Some(Utc::now());
}
Episode::SummaryCheckpoint {
input_message_count,
summary_chars,
latency_ms,
tokens,
} => {
log.steps.push(Step {
idx,
kind: StepKind::SummaryCheckpoint,
name: Some("summary".into()),
input: serde_json::json!({
"input_message_count": input_message_count,
}),
output: serde_json::json!({
"summary_chars": summary_chars,
}),
error: None,
latency: Duration::from_millis(u64::from(*latency_ms)),
span_id: None,
});
log.tokens.completion_tokens = log.tokens.completion_tokens.saturating_add(*tokens);
idx += 1;
}
Episode::Ops(payload) => {
let kind_name = payload
.get("kind")
.and_then(|k| k.as_str())
.map(str::to_owned);
log.steps.push(Step {
idx,
kind: StepKind::OpsEvent,
name: kind_name,
input: payload.clone(),
output: serde_json::Value::Null,
error: None,
latency: Duration::ZERO,
span_id: None,
});
idx += 1;
}
_ => {}
}
}
if any_priced_step {
log.cost_estimate = Some(Cost {
prompt_usd: prompt_usd_acc,
completion_usd: completion_usd_acc,
total_usd: prompt_usd_acc + completion_usd_acc,
});
}
log
}
fn step_cost(io: &LlmIo, prices: &PriceTable) -> Option<Cost> {
let provider = io.provider.as_deref()?;
let model = io.model.as_deref()?;
let pt = io.prompt_tokens?;
let ct = io.completion_tokens?;
let rates = prices.lookup(provider, model)?;
Some(rates.cost_for(pt, ct))
}