use crate::prompt::Error;
use crate::template::GitRunner;
use brazen::Content;
use std::path::Path;
pub(super) const MESSAGES_DIR: &str = "messages";
const SEQ_WIDTH: usize = 3;
const TOOL_ORIGIN: &str = "tool";
pub(super) fn next_seq(worktree: &Path) -> Result<u32, Error> {
let dir = worktree.join(MESSAGES_DIR);
let entries = match std::fs::read_dir(&dir) {
Ok(rd) => rd,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(1),
Err(e) => return Err(Error::Io(e)),
};
let mut max = 0u32;
for entry in entries {
let name = entry.map_err(Error::Io)?.file_name();
if let Some(seq) = name
.to_string_lossy()
.split('-')
.next()
.and_then(|p| p.parse::<u32>().ok())
{
max = max.max(seq);
}
}
Ok(max + 1)
}
pub(super) fn deliver_message(
worktree: &Path,
conv_id: &str,
sender: &str,
src: &Path,
git: &dyn GitRunner,
) -> Result<(), Error> {
let seq = next_seq(worktree)?;
let rel = format!("{MESSAGES_DIR}/{seq:0w$}-{sender}.md", w = SEQ_WIDTH);
let dest = worktree.join(&rel);
std::fs::create_dir_all(dest.parent().expect("messages/ has a parent"))?;
std::fs::rename(src, &dest)?;
commit_entry(worktree, conv_id, seq, &[&rel], sender, git)
}
pub(super) fn commit_assistant(
worktree: &Path,
conv_id: &str,
model_id: &str,
staging_path: &Path,
git: &dyn GitRunner,
) -> Result<Vec<Content>, Error> {
if model_id == TOOL_ORIGIN {
return Err(Error::ReservedModelId(model_id.to_string()));
}
let seq = next_seq(worktree)?;
let rel = entry_rel(seq, model_id);
let dest = worktree.join(&rel);
std::fs::create_dir_all(dest.parent().expect("messages/ has a parent"))?;
std::fs::rename(staging_path, &dest)?;
commit_entry(worktree, conv_id, seq, &[&rel], model_id, git)?;
let bytes = std::fs::read(&dest)?;
Ok(serde_json::from_slice(&bytes).expect("model-output entry is a canonical Content array"))
}
pub(super) fn commit_tool(
worktree: &Path,
conv_id: &str,
tool_result: &Content,
git: &dyn GitRunner,
) -> Result<(), Error> {
let seq = next_seq(worktree)?;
let rel = entry_rel(seq, TOOL_ORIGIN);
let dest = worktree.join(&rel);
std::fs::create_dir_all(dest.parent().expect("messages/ has a parent"))?;
let bytes = serde_json::to_vec(std::slice::from_ref(tool_result)).expect("Content serializes");
std::fs::write(&dest, bytes)?;
commit_entry(worktree, conv_id, seq, &["-A"], TOOL_ORIGIN, git)
}
fn entry_rel(seq: u32, origin: &str) -> String {
format!("{MESSAGES_DIR}/{seq:0w$}-{origin}.json", w = SEQ_WIDTH)
}
fn commit_entry(
worktree: &Path,
conv_id: &str,
seq: u32,
add_args: &[&str],
origin: &str,
git: &dyn GitRunner,
) -> Result<(), Error> {
let mut argv = vec!["add"];
argv.extend_from_slice(add_args);
git.run(worktree, &argv).map_err(|source| Error::Git {
op: "transcript add",
source,
})?;
let msg = format!("transcript {seq:0w$}: {origin} [{conv_id}]", w = SEQ_WIDTH);
git.run(worktree, &["commit", "-m", msg.as_str()])
.map_err(|source| Error::Git {
op: "transcript commit",
source,
})
}
#[cfg(test)]
mod tests;