use crate::engine::stable_id;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub id: String,
pub kind: &'static str,
pub payload: String,
}
#[derive(Debug, Default, Clone)]
pub struct EventLog {
events: Vec<Event>,
}
impl EventLog {
#[must_use]
pub const fn new() -> Self {
Self { events: Vec::new() }
}
pub fn append(&mut self, kind: &'static str, payload: impl Into<String>) -> String {
let payload = payload.into();
let seed = format!("{kind}:{}:{payload}", self.events.len());
let id = stable_id(kind, &seed);
self.events.push(Event {
id: id.clone(),
kind,
payload,
});
id
}
#[must_use]
pub fn events(&self) -> &[Event] {
&self.events
}
#[must_use]
pub fn first_of(&self, kind: &str) -> Option<&Event> {
self.events.iter().find(|event| event.kind == kind)
}
#[must_use]
pub fn last_of(&self, kind: &str) -> Option<&Event> {
self.events.iter().rev().find(|event| event.kind == kind)
}
#[must_use]
pub fn evidence_links(&self) -> Vec<String> {
self.events
.iter()
.map(|event| format!("{}:{}", event.kind, event.id))
.collect()
}
#[must_use]
pub fn steps_block(&self) -> String {
use std::fmt::Write as _;
let mut buffer = String::from("steps:");
for (index, event) in self.events.iter().enumerate() {
let _ = write!(
buffer,
"\n step_{index} {} {}",
event.kind,
sanitize_payload(&event.payload)
);
}
buffer
}
}
#[must_use]
pub fn build_evidence_links(prompt: &str, log: &EventLog, response_link: &str) -> Vec<String> {
let mut links: Vec<String> = Vec::new();
links.push(format!("prompt:{}", stable_id("prompt", prompt)));
for event in log.events() {
let evidence = match event.kind {
"trace:execution_failure" => format!("trace:execution_failure:{}", event.id),
"language" => format!("language:{}", event.payload),
"language_from" => format!("language_from:{}", event.payload),
"language_to" => format!("language_to:{}", event.payload),
"meaning" => format!("meaning:{}", event.payload),
"translation_gap" => format!("translation_gap:{}", event.payload),
"wikidata" => format!("wikidata:{}", event.payload),
"search:local" => format!("search:local:{}", event.id),
"search:external" => format!("search:external:{}", event.id),
"source:http" => format!("source:http:{}", event.payload.replace(' ', ":")),
"source_refresh" => format!("source_refresh:{}", event.payload),
"conflict:source_disagreement" => {
format!("conflict:source_disagreement:{}", event.id)
}
"cache_hit" => format!("cache_hit:{}", event.payload),
"network_fetch" => format!("network_fetch:{}", event.id),
"calculation:engine" => format!("calculation:engine:{}", event.payload),
"calculation:lino" => format!("calculation:lino:{}", event.payload),
"intent" => format!("intent:{}", event.payload),
"response" => event.payload.clone(),
"agent_mode:opted_in" => format!("agent_mode:opted_in:{}", event.id),
"agent_mode:active" => format!("agent_mode:active:{}", event.id),
"policy:chat_bounded_autonomy" => String::from("policy:chat_bounded_autonomy"),
"policy:add_only_history" => String::from("policy:add_only_history"),
"policy:destructive_action_requires_confirmation" => {
String::from("policy:destructive_action_requires_confirmation")
}
"policy:agent_time_budget" => format!("policy:agent_time_budget:{}", event.id),
"policy:cache_flush_requires_confirmation" => {
String::from("policy:cache_flush_requires_confirmation")
}
"policy:inappropriate_content" => String::from("policy:inappropriate_content"),
"error" => format!("error:{}", event.id),
"filter:user" => format!("filter:user:{}", event.payload),
"diagnostic_mode" => format!("diagnostic_mode:{}", event.payload),
"execution_status" => format!("execution_status:{}", event.id),
"execution_environment" => format!("execution_environment:{}", event.id),
_ => format!("{}:{}", event.kind, event.id),
};
links.push(evidence);
}
if !links.iter().any(|link| link == response_link) {
links.push(response_link.to_owned());
}
links
}
fn sanitize_payload(value: &str) -> String {
value
.replace('\r', "\\r")
.replace('\n', "\\n")
.replace('\t', "\\t")
}
#[cfg(test)]
mod tests {
use super::EventLog;
#[test]
fn append_returns_stable_ids_for_distinct_events() {
let mut log = EventLog::new();
let first = log.append("impulse", "hi");
let second = log.append("impulse", "hi");
assert_ne!(first, second, "appending twice must produce distinct ids");
assert_eq!(log.events().len(), 2);
}
#[test]
fn evidence_links_round_trip_event_kinds() {
let mut log = EventLog::new();
log.append("impulse", "hello");
log.append("intent", "greeting");
let links = log.evidence_links();
assert_eq!(links.len(), 2);
assert!(links[0].starts_with("impulse:"));
assert!(links[1].starts_with("intent:"));
}
#[test]
fn steps_block_lists_events_in_insertion_order() {
let mut log = EventLog::new();
log.append("impulse", "x");
log.append("trace", "y");
let block = log.steps_block();
assert!(block.contains("step_0 impulse x"));
assert!(block.contains("step_1 trace y"));
}
}