use crate::{instructions::InstructionFile, skills::SkillDiscovery};
use anyhow::Context;
use std::{env, path::Path};
const SYSTEM_FRAGMENT: &str = "system.md";
const TOOLS_FRAGMENT: &str = "tools.md";
const CODE_MODE_FRAGMENT: &str = "codemode.md";
const SKILLS_FRAGMENT: &str = "skills.md";
const COMPACT_FRAGMENT: &str = "compact.md";
const DEFAULT_SYSTEM_TEMPLATE: &str = include_str!("../../prompts/system.md");
const DEFAULT_TOOLS_TEMPLATE: &str = include_str!("../../prompts/tools.md");
const DEFAULT_CODE_MODE_TEMPLATE: &str = include_str!("../../prompts/codemode.md");
const DEFAULT_SKILLS_TEMPLATE: &str = include_str!("../../prompts/skills.md");
const DEFAULT_COMPACT_TEMPLATE: &str = include_str!("../../prompts/compact.md");
pub(super) fn build_system_prompt_with_prompt_dir_and_subagents(
prompt_dir: Option<&Path>,
instructions: &[InstructionFile],
skills: &SkillDiscovery,
subagents_section: Option<&str>,
code_mode_enabled: bool,
) -> anyhow::Result<String> {
Ok(build_system_prompt_sections_with_prompt_dir_and_subagents(
prompt_dir,
instructions,
skills,
subagents_section,
code_mode_enabled,
)?
.into_iter()
.map(|section| section.content)
.collect())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SystemPromptSection {
pub(crate) title: String,
pub(crate) content: String,
}
pub(crate) fn build_system_prompt_sections_with_prompt_dir_and_subagents(
prompt_dir: Option<&Path>,
instructions: &[InstructionFile],
skills: &SkillDiscovery,
subagents_section: Option<&str>,
code_mode_enabled: bool,
) -> anyhow::Result<Vec<SystemPromptSection>> {
let defaults = PromptTemplateDefaults::bundled();
let values = TemplateValues::from_skills(skills);
build_system_prompt_sections_from_templates_with_values(
prompt_dir,
&defaults,
instructions,
subagents_section,
code_mode_enabled,
&values,
)
}
pub(crate) fn load_compact_prompt(prompt_dir: Option<&Path>) -> anyhow::Result<String> {
let values = TemplateValues {
skills_list: String::new(),
runtime_facts: RuntimePromptFacts::detect(),
};
render_fragment(
&load_fragment(prompt_dir, COMPACT_FRAGMENT, DEFAULT_COMPACT_TEMPLATE)?,
&values,
)
}
fn build_system_prompt_sections_from_templates_with_values(
prompt_dir: Option<&Path>,
defaults: &PromptTemplateDefaults<'_>,
instructions: &[InstructionFile],
subagents_section: Option<&str>,
code_mode_enabled: bool,
values: &TemplateValues,
) -> anyhow::Result<Vec<SystemPromptSection>> {
let mut sections = Vec::new();
push_prompt_section(
&mut sections,
"System instructions",
render_fragment(
&load_fragment(prompt_dir, SYSTEM_FRAGMENT, defaults.system)?,
values,
)?,
);
push_prompt_section(
&mut sections,
"Tools",
render_fragment(
&load_fragment(prompt_dir, TOOLS_FRAGMENT, defaults.tools)?,
values,
)?,
);
if code_mode_enabled {
push_prompt_section(
&mut sections,
"Code mode",
render_fragment(
&load_fragment(prompt_dir, CODE_MODE_FRAGMENT, defaults.code_mode)?,
values,
)?,
);
}
push_prompt_section(
&mut sections,
"Subagents",
subagents_section.unwrap_or_default().to_string(),
);
push_prompt_section(
&mut sections,
"Skills",
render_fragment(
&load_fragment(prompt_dir, SKILLS_FRAGMENT, defaults.skills)?,
values,
)?,
);
push_instruction_sections(&mut sections, instructions);
sections
.last_mut()
.expect("instruction rendering always creates a section")
.content
.push('\n');
Ok(sections)
}
fn load_fragment(
prompt_dir: Option<&Path>,
fragment_name: &'static str,
default_template: &str,
) -> anyhow::Result<PromptFragment> {
if let Some(prompt_dir) = prompt_dir {
let path = prompt_dir.join(fragment_name);
if path.exists() {
let content = crate::prompt_file::read_prompt_file(&path, 256 * 1024, false)
.with_context(|| {
format!(
"failed to read prompt template fragment '{fragment_name}' at {}",
path.display()
)
})?
.text;
return Ok(PromptFragment {
name: fragment_name,
content,
});
}
}
Ok(PromptFragment {
name: fragment_name,
content: default_template.to_string(),
})
}
fn render_fragment(fragment: &PromptFragment, values: &TemplateValues) -> anyhow::Result<String> {
let mut rendered = String::with_capacity(fragment.content.len());
let mut rest = fragment.content.as_str();
loop {
let Some(open) = rest.find("{{") else {
if let Some(close) = rest.find("}}") {
anyhow::bail!(
"malformed prompt template variable in '{}': unexpected '}}}}' at byte {}",
fragment.name,
close
);
}
rendered.push_str(rest);
return Ok(rendered);
};
if let Some(close) = rest[..open].find("}}") {
anyhow::bail!(
"malformed prompt template variable in '{}': unexpected '}}}}' at byte {}",
fragment.name,
close
);
}
rendered.push_str(&rest[..open]);
let after_open = &rest[open + 2..];
let Some(close) = after_open.find("}}") else {
anyhow::bail!(
"malformed prompt template variable in '{}': missing closing '}}}}' for token starting at byte {}",
fragment.name,
open
);
};
let token = &after_open[..close];
let replacement = match token.trim() {
"SKILLS_LIST" if token == "SKILLS_LIST" => values.skills_list.as_str(),
"operating_system" if token == "operating_system" => {
values.runtime_facts.operating_system.as_str()
}
"terminal_environment" if token == "terminal_environment" => {
values.runtime_facts.terminal_environment.as_str()
}
supported @ ("SKILLS_LIST" | "operating_system" | "terminal_environment") => {
anyhow::bail!(
"malformed prompt template variable in '{}': use '{{{{{supported}}}}}' without extra whitespace",
fragment.name
);
}
other => anyhow::bail!(
"unsupported prompt template variable in '{}': '{{{{{other}}}}}'; supported variables are {}",
fragment.name,
supported_system_variables()
),
};
rendered.push_str(replacement);
rest = &after_open[close + 2..];
}
}
fn push_prompt_section(
sections: &mut Vec<SystemPromptSection>,
title: impl Into<String>,
content: String,
) {
let content = content.trim_end();
if content.is_empty() {
return;
}
let mut content = content.to_string();
if !sections.is_empty() {
content.insert_str(0, "\n\n");
}
sections.push(SystemPromptSection {
title: title.into(),
content,
});
}
fn push_instruction_sections(
sections: &mut Vec<SystemPromptSection>,
instructions: &[InstructionFile],
) {
if instructions.is_empty() {
push_prompt_section(
sections,
"Instructions",
"No AGENTS.md instruction files were discovered.".to_string(),
);
return;
}
for (index, instruction) in instructions.iter().enumerate() {
let mut content = String::new();
if index == 0 {
if !sections.is_empty() {
content.push_str("\n\n");
}
content.push_str("<Additional-Context-Files>\n");
content.push_str(
" These files are automatically injected and MUST ALWAYS be remembered and considered in every action, do NOT read these files again:\n",
);
}
content.push_str(&format!(
" <Context File={}>\n{}\n </Context>\n",
instruction.path.display(),
instruction.content
));
if index + 1 == instructions.len() {
content.push_str("</Additional-Context-Files>");
}
sections.push(SystemPromptSection {
title: format!("Instructions · {}", instruction.path.display()),
content,
});
}
}
fn render_skills_list(skills: &SkillDiscovery) -> String {
if skills.skills.is_empty() {
return "- none".to_string();
}
skills
.skills
.iter()
.map(|(name, skill)| {
skill
.frontmatter
.get("description")
.map(String::as_str)
.map(str::trim)
.filter(|description| !description.is_empty())
.map_or_else(
|| format!("- {name}"),
|description| format!("- {name}: {description}"),
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn supported_system_variables() -> &'static str {
"'{{SKILLS_LIST}}', '{{operating_system}}', and '{{terminal_environment}}'"
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RuntimePromptFacts {
operating_system: String,
terminal_environment: String,
}
impl RuntimePromptFacts {
fn detect() -> Self {
Self {
operating_system: operating_system_value(env::consts::OS),
terminal_environment: terminal_environment_from_process(),
}
}
}
fn operating_system_value(target_os: &str) -> String {
match target_os {
"macos" => "macOS".to_string(),
"linux" => "Linux".to_string(),
"windows" => "Windows".to_string(),
other => format!("the operating system is {other}"),
}
}
fn terminal_environment_from_process() -> String {
let indicators = [
("TERM_PROGRAM", env::var("TERM_PROGRAM").ok()),
("TERM", env::var("TERM").ok()),
("COLORTERM", env::var("COLORTERM").ok()),
];
terminal_environment_value(indicators.iter().filter_map(|(name, value)| {
value
.as_deref()
.and_then(|value| sanitize_terminal_indicator_value(value).map(|value| (*name, value)))
}))
}
fn terminal_environment_value(
indicators: impl IntoIterator<Item = (&'static str, String)>,
) -> String {
let parts = indicators
.into_iter()
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<_>>();
if parts.is_empty() {
"terminal environment is unspecified".to_string()
} else {
format!("terminal environment: {}", parts.join(", "))
}
}
fn sanitize_terminal_indicator_value(value: &str) -> Option<String> {
let sanitized = value
.trim()
.chars()
.filter(|ch| !ch.is_control())
.take(64)
.collect::<String>();
(!sanitized.is_empty()).then_some(sanitized)
}
struct PromptFragment {
name: &'static str,
content: String,
}
struct PromptTemplateDefaults<'a> {
system: &'a str,
tools: &'a str,
code_mode: &'a str,
skills: &'a str,
}
impl PromptTemplateDefaults<'static> {
fn bundled() -> Self {
Self {
system: DEFAULT_SYSTEM_TEMPLATE,
tools: DEFAULT_TOOLS_TEMPLATE,
code_mode: DEFAULT_CODE_MODE_TEMPLATE,
skills: DEFAULT_SKILLS_TEMPLATE,
}
}
}
struct TemplateValues {
skills_list: String,
runtime_facts: RuntimePromptFacts,
}
impl TemplateValues {
fn from_skills(skills: &SkillDiscovery) -> Self {
Self {
skills_list: render_skills_list(skills),
runtime_facts: RuntimePromptFacts::detect(),
}
}
}