struct PromptMemory {
panta_title: String,
explicit_panta: bool,
panta_manifest: Option<PathBuf>,
rhei_ids: Vec<String>,
rhei_roots: HashMap<String, PathBuf>,
rhei_titles: HashMap<String, String>,
rhei_plans: HashMap<String, PathBuf>,
content_sections: Vec<rhei_core::ast::ContentSection>,
task_sources: HashMap<String, PathBuf>,
runtime_dir: PathBuf,
run_in_flight: BTreeSet<String>,
pastes_task_inputs: bool,
absolute_paths: bool,
}
mod memory_caps {
pub const SIBLINGS: usize = 30;
pub const PARENT_BODY_LINES: usize = 200;
pub const CONTEXT_LINES: usize = 1000;
pub const PLAN_HISTORY: usize = 40;
pub const IN_FLIGHT: usize = 20;
pub const DEPENDENTS: usize = 30;
pub const RESULT_LINES: usize = 100;
pub const SUMMARY_COLUMNS: usize = 120;
}
fn prompt_memory(
loaded: &LoadedPlan,
input: &Path,
runtime_dir: &Path,
run_in_flight: BTreeSet<String>,
) -> PromptMemory {
let panta_manifest = rhei_core::workspace::panta_project_dir(input)
.map(|dir| dir.join(rhei_core::workspace::PANTA_INDEX_FILE));
PromptMemory {
panta_title: loaded.rhei.title.clone(),
explicit_panta: loaded.is_panta_project(),
panta_manifest,
rhei_ids: loaded.rhei_ids.clone(),
rhei_roots: loaded.rhei_roots.clone(),
rhei_titles: loaded.rhei_titles.clone(),
rhei_plans: loaded.rhei_plans.clone(),
content_sections: loaded.rhei.content_sections.clone(),
task_sources: loaded.task_sources.clone(),
runtime_dir: runtime_dir.to_path_buf(),
run_in_flight,
pastes_task_inputs: true,
absolute_paths: false,
}
}
fn owning_rhei_id(render_context: &RuntimeTemplateContext<'_>) -> Option<String> {
rhei_id_of(render_context.task)
}
fn memory_node_label(task: &rhei_core::ast::Task) -> String {
format!("{} {}", title_case_kind(&task.kind), task.id)
}
fn memory_state_name(
task: &rhei_core::ast::Task,
machine: &rhei_validator::StateMachine,
) -> String {
normalized_state_name(task.state.as_str(), machine)
}
fn flatten_task_slice(tasks: &[rhei_core::ast::Task]) -> Vec<&rhei_core::ast::Task> {
fn collect<'a>(task: &'a rhei_core::ast::Task, out: &mut Vec<&'a rhei_core::ast::Task>) {
out.push(task);
for child in &task.children {
collect(child, out);
}
}
let mut out = Vec::new();
for task in tasks {
collect(task, &mut out);
}
out
}
fn task_state_is_terminal(
task: &rhei_core::ast::Task,
machine: &rhei_validator::StateMachine,
) -> bool {
is_terminal_state(&memory_state_name(task, machine), machine)
}
fn memory_path(render_context: &RuntimeTemplateContext<'_>, path: &Path) -> String {
let path = canonical_spelling(path).unwrap_or_else(|| path.to_path_buf());
if render_context.memory.is_some_and(|memory| memory.absolute_paths) {
return absolute_memory_path(&path);
}
if render_context.checkout_root != render_context.workspace_root {
return spelled_path(&path);
}
let root = canonical_spelling(render_context.workspace_root)
.unwrap_or_else(|| render_context.workspace_root.to_path_buf());
match path.strip_prefix(&root) {
Ok(relative) if relative.as_os_str().is_empty() => ".".to_string(),
Ok(relative) => spelled_path(relative),
Err(_) => spelled_path(&path),
}
}
fn canonical_spelling(path: &Path) -> Option<PathBuf> {
let mut existing = path.to_path_buf();
let mut rest: Vec<std::ffi::OsString> = Vec::new();
loop {
if let Ok(canonical) = existing.canonicalize() {
return Some(rest.iter().rev().fold(canonical, |acc, part| acc.join(part)));
}
rest.push(existing.file_name()?.to_os_string());
if !existing.pop() {
return None;
}
}
}
fn spelled_path(path: &Path) -> String {
let rendered = path.display().to_string();
#[cfg(windows)]
if let Some(plain) = rendered.strip_prefix(r"\\?\") {
if !plain.starts_with("UNC") {
return plain.to_string();
}
}
rendered
}
fn absolute_memory_path(path: &Path) -> String {
spelled_path(&std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf()))
}
fn head_lines(body: &str, cap: usize) -> (String, bool) {
let lines: Vec<&str> = body.lines().collect();
if lines.len() <= cap {
return (body.to_string(), false);
}
(lines[..cap].join("\n"), true)
}
fn tail_lines(body: &str, cap: usize) -> (String, bool) {
let lines: Vec<&str> = body.lines().collect();
if lines.len() <= cap {
return (body.to_string(), false);
}
(lines[lines.len() - cap..].join("\n"), true)
}
fn cut_to_summary_columns(line: &str) -> String {
if line.chars().count() <= memory_caps::SUMMARY_COLUMNS {
return line.to_string();
}
let kept: String = line.chars().take(memory_caps::SUMMARY_COLUMNS).collect();
format!("{kept}\u{2026}")
}
fn is_result_entry_heading(line: &str) -> bool {
let trimmed = line.trim();
trimmed == "## Result" || trimmed.starts_with("## Result ")
}
fn last_result_entry_line(lines: &[&str]) -> Option<usize> {
let mut fence: Option<(char, usize)> = None;
let mut last = None;
for (index, line) in lines.iter().enumerate() {
match fence {
Some((marker, open)) => {
if let Some((character, run, bare)) = rhei_validator::code_fence_run(line) {
if character == marker && run >= open && bare {
fence = None;
}
}
}
None => match rhei_validator::code_fence_run(line) {
Some((marker, run, _)) => fence = Some((marker, run)),
None if is_result_entry_heading(line) => last = Some(index),
None => {}
},
}
}
last
}
fn result_summary_from_body(body: &str) -> Option<String> {
let lines: Vec<&str> = body.lines().collect();
let start = last_result_entry_line(&lines).map(|index| index + 1).unwrap_or(0);
lines[start..]
.iter()
.find(|line| !line.trim().is_empty())
.map(|line| cut_to_summary_columns(line.trim()))
}
fn task_history_summary(
render_context: &RuntimeTemplateContext<'_>,
task_id: &TaskId,
pasted_in_full: &BTreeSet<String>,
) -> MietteResult<String> {
if pasted_in_full.contains(&task_id.to_string()) {
return Ok("see above".to_string());
}
let Some(body) = read_task_result(render_context, task_id)? else {
return Ok("(no result)".to_string());
};
Ok(result_summary_from_body(&body).unwrap_or_else(|| "(no result)".to_string()))
}
fn read_ledger(root: &Path) -> MietteResult<Vec<(String, String, String)>> {
let path = root.join("runtime").join("state-transitions.log");
if !path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&path)
.map_err(|err| file_io_report(&path, "failed to read the transition ledger", err))?;
Ok(parse_ledger(&content))
}
fn parse_ledger(content: &str) -> Vec<(String, String, String)> {
content
.lines()
.filter_map(|line| {
let (task, transition) = line.trim().split_once(' ')?;
let (from, to) = transition.split_once('@')?;
Some((task.to_string(), from.trim().to_string(), to.trim().to_string()))
})
.collect()
}
fn results_pasted_in_full(
render_context: &RuntimeTemplateContext<'_>,
) -> MietteResult<BTreeSet<String>> {
let mut pasted = BTreeSet::new();
let pastes_inputs =
render_context.memory.is_some_and(|memory| memory.pastes_task_inputs);
if pastes_inputs {
for prior in &render_context.task.prior {
if read_task_result(render_context, prior)?.is_some() {
pasted.insert(prior.to_string());
}
}
}
let supervising = task_is_supervising(render_context.task, render_context.machine);
if !supervising {
if !pastes_inputs {
return Ok(pasted);
}
for child in &render_context.task.children {
if !task_state_is_terminal(child, render_context.machine) {
continue;
}
if read_task_result(render_context, &child.id)?.is_some() {
pasted.insert(child.id.to_string());
}
}
return Ok(pasted);
}
for checkpoint in supervision_checkpoints(render_context.metadata, &render_context.task.id) {
let qualified = checkpoint_qualified_id(render_context.task, &checkpoint.task);
let Some(descendant) = checkpoint_descendant(render_context.task, &qualified) else {
continue;
};
let to_is_terminal = render_context
.machine
.states
.get(&checkpoint.to)
.map(|def| def.terminal)
.unwrap_or(false);
if to_is_terminal && read_task_result(render_context, &descendant.id)?.is_some() {
pasted.insert(descendant.id.to_string());
}
}
Ok(pasted)
}
fn pasted_descendant_ids(
render_context: &RuntimeTemplateContext<'_>,
pasted_in_full: &BTreeSet<String>,
) -> BTreeSet<String> {
flatten_task_slice(&render_context.task.children)
.into_iter()
.map(|task| task.id.to_string())
.filter(|id| pasted_in_full.contains(id))
.collect()
}