use crate::transcript::{Block, Entry, EntryKind, Transcript};
use std::path::Path;
const MESSAGES: &str = "messages/";
const WINDOW_MAX: usize = 24_000;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Evidence {
pub goal: String,
pub window: String,
}
pub fn gather(workspace: &Path, agent: &str, since: Option<&str>, tip: &str) -> Evidence {
let dir = workspace.join("agents").join(agent);
let goal = std::fs::read_to_string(dir.join("goal.md")).unwrap_or_default();
let transcript = crate::transcript::build(workspace, agent);
let window = fold(&transcript, delta(workspace, since, tip).as_deref());
Evidence { goal, window }
}
fn delta(workspace: &Path, since: Option<&str>, tip: &str) -> Option<Vec<String>> {
let repo = workspace.join(crate::git_tree::REPO_DIR);
let names = crate::git_tree::diff_names(&repo, since?, tip, MESSAGES).ok()?;
Some(
names
.iter()
.filter_map(|p| p.strip_prefix(MESSAGES).map(str::to_owned))
.collect(),
)
}
fn fold(transcript: &Transcript, only: Option<&[String]>) -> String {
let text: String = transcript
.entries
.iter()
.filter(|e| {
matches!(e.kind, EntryKind::Compacted { .. })
|| only.is_none_or(|names| names.contains(&e.name))
})
.map(say)
.collect::<Vec<_>>()
.join("\n");
tail(&text)
}
fn say(entry: &Entry) -> String {
match &entry.kind {
EntryKind::Delivered { sender, body, .. } => format!("[message from {sender}]\n{body}\n"),
EntryKind::Model { blocks, .. } => {
let said: Vec<String> = blocks.iter().map(block).collect();
format!("[agent]\n{}\n", said.join("\n"))
}
EntryKind::ToolResult {
content, is_error, ..
} => {
let mark = if *is_error { " (error)" } else { "" };
format!("[tool result{mark}]\n{content}\n")
}
EntryKind::Compacted {
first,
last,
summary,
} => {
let span = format!("entries {first:03}\u{2013}{last:03}");
if summary.is_empty() {
format!("[record compacted here: {span} deleted; no summary on this mark]\n")
} else {
format!(
"[record compacted here: {span} deleted; litany's summary replaced \
them in the agent's context]\n{summary}\n"
)
}
}
EntryKind::Streaming { .. } | EntryKind::Raw => String::new(),
}
}
fn block(block: &Block) -> String {
match block {
Block::Text(text) => text.clone(),
Block::Thinking(text) => format!("(thinking) {text}"),
Block::ToolUse {
name,
input_summary,
..
} => format!("(tool {name}) {input_summary}"),
}
}
fn tail(text: &str) -> String {
let len = text.chars().count();
if len <= WINDOW_MAX {
return text.to_owned();
}
let kept: String = text.chars().skip(len - WINDOW_MAX).collect();
format!("[earlier work elided]\n{kept}")
}
#[cfg(test)]
mod tests;