use crate::distill::TraceEvent;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DropReason {
Failed,
NoStateEffect,
Superseded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DroppedStep {
pub index: usize,
pub action_id: Option<String>,
pub tool: Option<String>,
pub reason: DropReason,
pub superseded_keys: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Compression {
pub effective: Vec<usize>,
pub dropped: Vec<DroppedStep>,
pub total_actions: usize,
}
impl Compression {
pub fn effective_ratio(&self) -> f64 {
if self.total_actions == 0 {
return 1.0;
}
self.effective.len() as f64 / self.total_actions as f64
}
pub fn summary(&self) -> String {
format!(
"{}/{} actions left a trace in final state ({:.0}%); dropped {} \
(no-effect {}, superseded {}, failed {})",
self.effective.len(),
self.total_actions,
self.effective_ratio() * 100.0,
self.dropped.len(),
self.count(DropReason::NoStateEffect),
self.count(DropReason::Superseded),
self.count(DropReason::Failed),
)
}
fn count(&self, reason: DropReason) -> usize {
self.dropped.iter().filter(|d| d.reason == reason).count()
}
}
fn is_action(kind: &str) -> bool {
matches!(
kind,
"action_succeeded" | "action_failed" | "action_rejected" | "policy_violation"
)
}
pub fn analyze(events: &[TraceEvent]) -> Compression {
let mut last_writer: BTreeMap<&str, usize> = BTreeMap::new();
for (i, ev) in events.iter().enumerate() {
if !is_action(&ev.kind) {
continue;
}
if let Some(delta) = &ev.state_after {
for key in delta.keys() {
last_writer.insert(key.as_str(), i);
}
}
}
let mut effective = Vec::new();
let mut dropped = Vec::new();
let mut total_actions = 0usize;
for (i, ev) in events.iter().enumerate() {
if !is_action(&ev.kind) {
continue;
}
total_actions += 1;
if ev.kind != "action_succeeded" {
dropped.push(DroppedStep {
index: i,
action_id: ev.action_id.clone(),
tool: ev.tool.clone(),
reason: DropReason::Failed,
superseded_keys: Vec::new(),
});
continue;
}
let Some(delta) = &ev.state_after else {
dropped.push(DroppedStep {
index: i,
action_id: ev.action_id.clone(),
tool: ev.tool.clone(),
reason: DropReason::NoStateEffect,
superseded_keys: Vec::new(),
});
continue;
};
let mut survives = false;
let mut overwritten: BTreeSet<String> = BTreeSet::new();
for key in delta.keys() {
if last_writer.get(key.as_str()) == Some(&i) {
survives = true;
} else {
overwritten.insert(key.clone());
}
}
if survives {
effective.push(i);
} else {
dropped.push(DroppedStep {
index: i,
action_id: ev.action_id.clone(),
tool: ev.tool.clone(),
reason: DropReason::Superseded,
superseded_keys: overwritten.into_iter().collect(),
});
}
}
Compression {
effective,
dropped,
total_actions,
}
}
pub fn effective_events(events: &[TraceEvent]) -> Vec<&TraceEvent> {
analyze(events)
.effective
.into_iter()
.map(|i| &events[i])
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
fn ev(kind: &str, id: &str, tool: &str, delta: Option<&[(&str, i64)]>) -> TraceEvent {
TraceEvent {
kind: kind.to_string(),
action_id: Some(id.to_string()),
tool: Some(tool.to_string()),
data: json!({}),
duration_ms: Some(1.0),
state_before: None,
state_after: delta.map(|kvs| {
kvs.iter()
.map(|(k, v)| (k.to_string(), json!(v)))
.collect::<HashMap<String, serde_json::Value>>()
}),
reward: Some(if kind == "action_succeeded" { 1.0 } else { 0.0 }),
}
}
#[test]
fn a_superseded_write_does_not_survive() {
let events = vec![
ev("action_succeeded", "a1", "set", Some(&[("x", 1)])),
ev("action_succeeded", "a2", "set", Some(&[("x", 2)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![1]);
assert_eq!(c.dropped.len(), 1);
assert_eq!(c.dropped[0].reason, DropReason::Superseded);
assert_eq!(c.dropped[0].superseded_keys, vec!["x".to_string()]);
}
#[test]
fn a_partially_superseded_action_still_counts_as_effective() {
let events = vec![
ev("action_succeeded", "a1", "set", Some(&[("x", 1), ("y", 9)])),
ev("action_succeeded", "a2", "set", Some(&[("x", 2)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![0, 1]);
assert!(c.dropped.is_empty(), "{:?}", c.dropped);
}
#[test]
fn an_action_with_no_state_delta_is_dropped_as_no_effect() {
let events = vec![
ev("action_succeeded", "a1", "search", None),
ev("action_succeeded", "a2", "set", Some(&[("x", 1)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![1]);
assert_eq!(c.dropped[0].reason, DropReason::NoStateEffect);
assert!(c.dropped[0].superseded_keys.is_empty());
}
#[test]
fn a_failed_action_is_reported_as_failed_not_superseded() {
let events = vec![
ev("action_failed", "a1", "deploy", Some(&[("x", 1)])),
ev("action_succeeded", "a2", "deploy", Some(&[("x", 2)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![1]);
assert_eq!(c.dropped[0].reason, DropReason::Failed);
}
#[test]
fn context_events_are_not_counted_as_actions() {
let mut ctx = ev("state_changed", "c1", "n/a", Some(&[("x", 1)]));
ctx.action_id = None;
let events = vec![ctx, ev("action_succeeded", "a1", "set", Some(&[("y", 1)]))];
let c = analyze(&events);
assert_eq!(c.total_actions, 1);
assert_eq!(c.effective, vec![1]);
}
#[test]
fn the_shepherd_shape_reproduces_a_long_exploratory_prefix() {
let mut events: Vec<TraceEvent> = (0..8)
.map(|i| ev("action_succeeded", &format!("s{i}"), "search", None))
.collect();
for i in 0..3 {
events.push(ev(
"action_succeeded",
&format!("w{i}"),
"set",
Some(&[("answer", i)]),
));
}
events.push(ev(
"action_succeeded",
"final",
"set",
Some(&[("answer", 42)]),
));
let c = analyze(&events);
assert_eq!(c.total_actions, 12);
assert_eq!(c.effective.len(), 1);
assert_eq!(c.effective_ratio(), 1.0 / 12.0);
assert_eq!(c.dropped.len(), 11);
let eff = effective_events(&events);
assert_eq!(eff.len(), 1);
assert_eq!(eff[0].action_id.as_deref(), Some("final"));
}
#[test]
fn a_fully_effective_trajectory_drops_nothing() {
let events = vec![
ev("action_succeeded", "a1", "set", Some(&[("x", 1)])),
ev("action_succeeded", "a2", "set", Some(&[("y", 2)])),
ev("action_succeeded", "a3", "set", Some(&[("z", 3)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![0, 1, 2]);
assert!(c.dropped.is_empty());
assert_eq!(c.effective_ratio(), 1.0);
assert!(c.summary().contains("3/3"));
}
#[test]
fn empty_trajectory_is_vacuously_effective() {
let c = analyze(&[]);
assert_eq!(c.total_actions, 0);
assert_eq!(c.effective_ratio(), 1.0);
assert!(c.dropped.is_empty());
}
#[test]
fn rewriting_the_same_value_still_supersedes() {
let events = vec![
ev("action_succeeded", "a1", "set", Some(&[("x", 7)])),
ev("action_succeeded", "a2", "set", Some(&[("x", 7)])),
];
let c = analyze(&events);
assert_eq!(c.effective, vec![1]);
assert_eq!(c.dropped[0].reason, DropReason::Superseded);
}
#[test]
fn analysis_is_deterministic() {
let events = vec![
ev("action_succeeded", "a1", "set", Some(&[("x", 1), ("y", 2)])),
ev("action_succeeded", "a2", "set", Some(&[("x", 3)])),
ev("action_failed", "a3", "deploy", None),
];
assert_eq!(analyze(&events), analyze(&events));
}
}