fn content_section_block(section: &rhei_core::ast::ContentSection, rhei_id: &str) -> String {
let prefix = format!("Rhei {rhei_id} / ");
let title = section.title.strip_prefix(&prefix).unwrap_or(section.title.as_str());
format!("## {title}\n\n{}", section.content.trim())
}
fn scoped_content_sections(memory: &PromptMemory, rhei_id: Option<&str>) -> String {
let owner = rhei_id.filter(|_| memory.explicit_panta);
memory
.content_sections
.iter()
.filter(|section| section.rhei.as_deref() == owner)
.filter(|section| !section.content.trim().is_empty())
.map(|section| content_section_block(section, rhei_id.unwrap_or_default()))
.collect::<Vec<_>>()
.join("\n\n")
}
fn render_context_block(heading: &str, body: &str, source: &str) -> String {
if body.trim().is_empty() {
return String::new();
}
let (kept, truncated) = head_lines(body, memory_caps::CONTEXT_LINES);
let overflow =
if truncated { format!("\n\u{2026} truncated; read {source}\n") } else { String::new() };
format!("\n### {heading}\n\n{}\n{overflow}", fenced_markdown(&kept))
}
fn task_waits_on(candidate: &rhei_core::ast::Task, subject: &rhei_core::ast::Task) -> bool {
if candidate.prior.iter().any(|prior| prior == &subject.id) {
return true;
}
candidate.consumes.iter().any(|consumed| {
consumed.task == subject.id && subject.provides.contains(&consumed.name)
})
}
fn render_siblings(
render_context: &RuntimeTemplateContext<'_>,
parent: &rhei_core::ast::Task,
) -> String {
let siblings: Vec<&rhei_core::ast::Task> =
parent.children.iter().filter(|child| child.id != render_context.task.id).collect();
if siblings.is_empty() {
return String::new();
}
let mut out = String::from("\n### Siblings\n\n");
for sibling in siblings.iter().take(memory_caps::SIBLINGS) {
let marker = if task_waits_on(sibling, render_context.task) {
" \u{2014} waits on this task"
} else {
""
};
out.push_str(&format!(
"- {}: {} [{}]{marker}\n",
memory_node_label(sibling),
sibling.title,
memory_state_name(sibling, render_context.machine)
));
}
if siblings.len() > memory_caps::SIBLINGS {
out.push_str(&format!(
"\u{2026} {} more \u{2014} rhei list --parent {}\n",
siblings.len() - memory_caps::SIBLINGS,
parent.id
));
}
out
}
fn render_parent_body(
render_context: &RuntimeTemplateContext<'_>,
memory: &PromptMemory,
parent: &rhei_core::ast::Task,
) -> String {
let body = parent.content.trim();
if body.is_empty() {
return String::new();
}
let (kept, truncated) = head_lines(body, memory_caps::PARENT_BODY_LINES);
let overflow = if truncated {
let source = memory
.task_sources
.get(&parent.id.to_string())
.map(|path| memory_path(render_context, path))
.unwrap_or_else(|| memory_path(render_context, render_context.plan_path));
format!("\n\u{2026} truncated; read {source}\n")
} else {
String::new()
};
format!(
"\n### Parent: {}: {}\n\n{}\n{overflow}",
memory_node_label(parent),
parent.title,
fenced_markdown(&kept)
)
}
fn render_position_chain(
render_context: &RuntimeTemplateContext<'_>,
memory: &PromptMemory,
ancestors: &[&rhei_core::ast::Task],
) -> String {
let mut chain = format!("Panta: {}", memory.panta_title);
if let Some(rhei_id) = owning_rhei_id(render_context) {
let title = memory.rhei_titles.get(&rhei_id).cloned().unwrap_or_default();
chain.push_str(&format!(" \u{203a} rhei `{rhei_id}`: {title}"));
}
for ancestor in ancestors.iter().rev() {
chain.push_str(&format!(
" \u{203a} {}: {} [{}]",
memory_node_label(ancestor),
ancestor.title,
memory_state_name(ancestor, render_context.machine)
));
}
let visit = render_visit_count(
render_context.metadata,
&render_context.task.id,
render_context.state_name,
render_context.current_state_raw,
render_context.machine,
);
format!(
"{chain}\n\u{203a} **{}: {} [{}]** \u{2190} this invocation (visit {visit})\n",
memory_node_label(render_context.task),
render_context.task.title,
render_context.state_name
)
}
fn render_position(render_context: &RuntimeTemplateContext<'_>) -> String {
let Some(memory) = render_context.memory else { return String::new() };
let ancestors = render_context
.plan_tasks
.map(|tasks| ancestor_chain(tasks, &render_context.task.id))
.unwrap_or_default();
let mut out = String::from("\n## Position\n\n");
out.push_str(&render_position_chain(render_context, memory, &ancestors));
if let Some(parent) = ancestors.first() {
out.push_str(&render_siblings(render_context, parent));
out.push_str(&render_parent_body(render_context, memory, parent));
}
let rhei_id = owning_rhei_id(render_context);
out.push_str(&render_context_block(
"Rhei Context",
&scoped_content_sections(memory, rhei_id.as_deref()),
&rhei_id
.as_deref()
.and_then(|id| memory.rhei_plans.get(id))
.map(|path| memory_path(render_context, path))
.unwrap_or_else(|| memory_path(render_context, render_context.plan_path)),
));
if let Some(manifest) = memory.panta_manifest.as_deref() {
out.push_str(&render_context_block(
"Project Context",
&scoped_content_sections(memory, None),
&memory_path(render_context, manifest),
));
}
out
}