use super::types::{ToolMemoryPriority, ToolMemoryRule};
pub const TOOL_MEMORY_HEADING: &str = "## Tool-scoped rules";
pub struct ToolMemoryRulesSection {
rendered: String,
}
impl ToolMemoryRulesSection {
pub fn new(rules: Vec<ToolMemoryRule>) -> Self {
Self {
rendered: render_tool_memory_rules(&rules),
}
}
pub fn empty() -> Self {
Self {
rendered: String::new(),
}
}
pub fn is_empty(&self) -> bool {
self.rendered.trim().is_empty()
}
pub fn rendered(&self) -> &str {
&self.rendered
}
}
pub fn render_tool_memory_rules(rules: &[ToolMemoryRule]) -> String {
if rules.is_empty() {
return String::new();
}
let mut sorted: Vec<&ToolMemoryRule> = rules.iter().collect();
sorted.sort_by(|a, b| {
b.priority
.cmp(&a.priority)
.then_with(|| a.tool_name.cmp(&b.tool_name))
.then_with(|| a.rule.cmp(&b.rule))
.then_with(|| a.id.cmp(&b.id))
});
let mut out = String::new();
out.push_str(TOOL_MEMORY_HEADING);
out.push_str("\n\n");
out.push_str(
"These rules are pinned by the user or by the safety pipeline. Treat \
every entry as a hard constraint when considering the matching tool — \
do not override them silently. Lower-priority guidance lives in the \
`tool-{name}` memory namespace and can be queried via `memory_recall` \
if needed.\n\n",
);
let mut current_tool: Option<&str> = None;
for rule in sorted {
if current_tool != Some(rule.tool_name.as_str()) {
if current_tool.is_some() {
out.push('\n');
}
out.push_str("### `");
out.push_str(rule.tool_name.as_str());
out.push_str("`\n");
current_tool = Some(rule.tool_name.as_str());
}
out.push_str("- ");
out.push_str(priority_marker(rule.priority));
out.push(' ');
out.push_str(rule.rule.trim());
out.push('\n');
}
out
}
fn priority_marker(priority: ToolMemoryPriority) -> &'static str {
match priority {
ToolMemoryPriority::Critical => "**[critical]**",
ToolMemoryPriority::High => "**[high]**",
ToolMemoryPriority::Normal => "**[normal]**",
}
}
#[cfg(test)]
#[path = "render_tests.rs"]
mod tests;