use crate::prompt::step::STEPS_DIR;
use crate::prompt::tool::{OUTPUT_FILE, STEP_TOOLS_SUBDIR};
pub(crate) const STREAMS: [&str; 2] = ["stdout", "stderr"];
const STREAM_SEP: char = '#';
const OFFSET_SEP: char = '@';
pub(crate) fn mint(record: &std::path::Path, stream: &str, offset: usize) -> String {
format!(
"{}{STREAM_SEP}{stream}{OFFSET_SEP}{offset}",
record.display()
)
}
pub(crate) struct Address {
record: String,
pub(crate) agent_id: String,
pub(crate) stream: String,
pub(crate) offset: usize,
}
impl Address {
pub(crate) fn parse(text: &str) -> Option<Self> {
let (record, fragment) = text.rsplit_once(STREAM_SEP)?;
let (stream, offset) = fragment.rsplit_once(OFFSET_SEP)?;
if !STREAMS.contains(&stream) {
return None;
}
let offset: usize = offset.parse().ok()?;
let segments: Vec<&str> = record.split('/').collect();
let [steps, agent_id, step, tools, tool_id, output] = segments.as_slice() else {
return None;
};
let literals = *steps == STEPS_DIR && *tools == STEP_TOOLS_SUBDIR && *output == OUTPUT_FILE;
let named = plain(agent_id) && plain(tool_id);
let numbered = !step.is_empty() && step.bytes().all(|b| b.is_ascii_digit());
(literals && named && numbered).then(|| Self {
record: record.to_string(),
agent_id: (*agent_id).to_string(),
stream: stream.to_string(),
offset,
})
}
pub(crate) fn record(&self) -> &str {
&self.record
}
pub(crate) fn at(&self, offset: usize) -> String {
mint(std::path::Path::new(&self.record), &self.stream, offset)
}
}
fn plain(segment: &str) -> bool {
!segment.is_empty() && segment != "." && segment != ".."
}