#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TranscriptRole {
User,
Assistant,
Tool,
}
#[derive(Debug, Clone)]
pub struct TranscriptEntry {
pub role: TranscriptRole,
pub content: String,
pub tool_name: Option<String>,
}
pub struct TranscriptFormatter;
impl TranscriptFormatter {
#[must_use]
pub fn render_flat(entries: &[TranscriptEntry]) -> String {
entries
.iter()
.map(Self::render_line)
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn render_line(entry: &TranscriptEntry) -> String {
match entry.role {
TranscriptRole::User => format!("user: {}", entry.content),
TranscriptRole::Assistant => format!("assistant: {}", entry.content),
TranscriptRole::Tool => {
let name = entry.tool_name.as_deref().unwrap_or("tool");
format!("[tool: {name}] {}", entry.content)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_flat_joins_lines_in_order() {
let entries = vec![
TranscriptEntry {
role: TranscriptRole::User,
content: "hi".to_owned(),
tool_name: None,
},
TranscriptEntry {
role: TranscriptRole::Assistant,
content: "hello".to_owned(),
tool_name: None,
},
];
let text = TranscriptFormatter::render_flat(&entries);
assert_eq!(text, "user: hi\nassistant: hello");
}
#[test]
fn render_line_collapses_tool_entry() {
let entry = TranscriptEntry {
role: TranscriptRole::Tool,
content: "$ ls\nfile.txt".to_owned(),
tool_name: Some("bash".to_owned()),
};
let line = TranscriptFormatter::render_line(&entry);
assert!(line.starts_with("[tool: bash]"));
}
#[test]
fn render_line_tool_without_name_falls_back() {
let entry = TranscriptEntry {
role: TranscriptRole::Tool,
content: "output".to_owned(),
tool_name: None,
};
let line = TranscriptFormatter::render_line(&entry);
assert!(line.starts_with("[tool: tool]"));
}
#[test]
fn render_flat_empty_slice_is_empty_string() {
assert_eq!(TranscriptFormatter::render_flat(&[]), "");
}
}