use crate::llm::LlmClient;
use anyhow::Context;
pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/common/system_default.md");
pub const CONTINUATION: &str = include_str!("../prompts/common/continuation.md");
pub const BOUNDARIES: &str = include_str!("../prompts/common/boundaries.md");
pub const AGENT_EXPLORE: &str = include_str!("../prompts/agents/explore.md");
pub const AGENT_PLAN: &str = include_str!("../prompts/agents/plan.md");
pub const AGENT_CODE_REVIEW: &str = include_str!("../prompts/agents/code_review.md");
pub const CONTEXT_COMPACT: &str = include_str!("../prompts/common/context_compact.md");
pub const CONTEXT_SUMMARY_PREFIX: &str =
include_str!("../prompts/common/context_summary_prefix.md");
pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/planning/llm_plan_system.md");
pub const LLM_GOAL_EXTRACT_SYSTEM: &str =
include_str!("../prompts/planning/llm_goal_extract_system.md");
pub const LLM_GOAL_CHECK_SYSTEM: &str =
include_str!("../prompts/planning/llm_goal_check_system.md");
pub const PRE_ANALYSIS_SYSTEM: &str = include_str!("../prompts/analysis/pre_analysis_system.md");
pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execute_goal.md");
pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");
pub const PLAN_FALLBACK_STEP: &str = include_str!("../prompts/planning/plan_fallback_step.md");
pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
pub const AGENT_VERIFICATION: &str = include_str!("../prompts/agents/verification.md");
pub const INTENT_CLASSIFY_SYSTEM: &str =
include_str!("../prompts/analysis/intent_classify_system.md");
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum PlanningMode {
#[default]
Auto,
Disabled,
Enabled,
}
impl PlanningMode {
pub fn should_plan(&self, message: &str) -> bool {
match self {
PlanningMode::Auto => AgentStyle::detect_from_message(message).requires_planning(),
PlanningMode::Enabled => true,
PlanningMode::Disabled => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AgentStyle {
#[default]
GeneralPurpose,
Plan,
Verification,
Explore,
CodeReview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectionConfidence {
High,
Medium,
Low,
}
impl AgentStyle {
pub fn base_prompt(&self) -> &'static str {
match self {
AgentStyle::GeneralPurpose => SYSTEM_DEFAULT,
AgentStyle::Plan => AGENT_PLAN,
AgentStyle::Verification => AGENT_VERIFICATION,
AgentStyle::Explore => AGENT_EXPLORE,
AgentStyle::CodeReview => AGENT_CODE_REVIEW,
}
}
pub fn guidelines(&self) -> Option<&'static str> {
match self {
AgentStyle::GeneralPurpose => None,
AgentStyle::Plan => None, AgentStyle::Verification => None, AgentStyle::Explore => None, AgentStyle::CodeReview => None,
}
}
pub fn description(&self) -> &'static str {
match self {
AgentStyle::GeneralPurpose => {
"General purpose coding agent for research and multi-step tasks"
}
AgentStyle::Plan => "Read-only planning and architecture analysis agent",
AgentStyle::Verification => "Adversarial verification specialist — tries to break code",
AgentStyle::Explore => "Fast read-only file search and codebase exploration agent",
AgentStyle::CodeReview => "Code review focused — analyzes quality and best practices",
}
}
pub fn builtin_agent_name(&self) -> &'static str {
match self {
AgentStyle::GeneralPurpose => "general",
AgentStyle::Plan => "plan",
AgentStyle::Verification => "verification",
AgentStyle::Explore => "explore",
AgentStyle::CodeReview => "review",
}
}
pub fn runtime_mode(&self) -> &'static str {
match self {
AgentStyle::GeneralPurpose => "general",
AgentStyle::Plan => "planning",
AgentStyle::Verification => "verification",
AgentStyle::Explore => "explore",
AgentStyle::CodeReview => "code_review",
}
}
pub fn requires_planning(&self) -> bool {
matches!(self, AgentStyle::Plan)
}
pub fn detect_with_confidence(message: &str) -> (Self, DetectionConfidence) {
if message
.chars()
.any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
{
return (AgentStyle::GeneralPurpose, DetectionConfidence::Low);
}
let lower = message.to_lowercase();
if lower.contains("try to break")
|| lower.contains("find vulnerabilities")
|| lower.contains("adversarial")
|| lower.contains("security audit")
{
return (AgentStyle::Verification, DetectionConfidence::High);
}
if lower.contains("help me plan")
|| lower.contains("help me design")
|| lower.contains("create a plan")
|| lower.contains("implementation plan")
|| lower.contains("step-by-step plan")
{
return (AgentStyle::Plan, DetectionConfidence::High);
}
if lower.contains("find all files")
|| lower.contains("search for all")
|| lower.contains("locate all")
{
return (AgentStyle::Explore, DetectionConfidence::High);
}
if lower.contains("verify")
|| lower.contains("verification")
|| lower.contains("break")
|| lower.contains("debug")
|| lower.contains("test")
|| lower.contains("check if")
{
return (AgentStyle::Verification, DetectionConfidence::Medium);
}
if lower.contains("plan")
|| lower.contains("design")
|| lower.contains("architecture")
|| lower.contains("approach")
{
return (AgentStyle::Plan, DetectionConfidence::Medium);
}
if lower.contains("find")
|| lower.contains("search")
|| lower.contains("where is")
|| lower.contains("where's")
|| lower.contains("locate")
|| lower.contains("explore")
|| lower.contains("look for")
{
return (AgentStyle::Explore, DetectionConfidence::Medium);
}
if lower.contains("review")
|| lower.contains("code review")
|| lower.contains("analyze")
|| lower.contains("assess")
|| lower.contains("quality")
|| lower.contains("best practice")
{
return (AgentStyle::CodeReview, DetectionConfidence::Medium);
}
(AgentStyle::GeneralPurpose, DetectionConfidence::Low)
}
pub fn detect_from_message(message: &str) -> Self {
Self::detect_with_confidence(message).0
}
pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
use crate::llm::Message;
let system = INTENT_CLASSIFY_SYSTEM;
let messages = vec![Message::user(message)];
let response = llm
.complete(&messages, Some(system), &[])
.await
.context("LLM intent classification failed")?;
let text = response.text().trim().to_lowercase();
let style = match text.as_str() {
"plan" => AgentStyle::Plan,
"explore" => AgentStyle::Explore,
"verification" => AgentStyle::Verification,
"codereview" | "code review" => AgentStyle::CodeReview,
_ => AgentStyle::GeneralPurpose,
};
Ok(style)
}
}
#[derive(Debug, Clone, Default)]
pub struct SystemPromptSlots {
pub style: Option<AgentStyle>,
pub role: Option<String>,
pub guidelines: Option<String>,
pub response_style: Option<String>,
pub extra: Option<String>,
}
const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
const DEFAULT_RESPONSE_FORMAT: &str =
include_str!("../prompts/common/system_default_response_format.md");
impl SystemPromptSlots {
pub fn build(&self) -> String {
self.build_with_style(self.style.unwrap_or_default())
}
pub fn build_with_message(&self, initial_message: &str) -> String {
let style = self
.style
.unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
self.build_with_style(style)
}
fn build_with_style(&self, style: AgentStyle) -> String {
let mut parts: Vec<String> = Vec::new();
let base_prompt = style.base_prompt().replace('\r', "");
let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
let core = if let Some(ref role) = self.role {
if style == AgentStyle::GeneralPurpose {
let custom_role = format!(
"{}. You operate in an agentic loop: inspect, act with tools, observe results, and continue until the user's request is genuinely complete.",
role.trim_end_matches('.')
);
base_prompt.replace(&default_role_line, &custom_role)
} else {
format!("{}\n\n{}", role, base_prompt)
}
} else {
base_prompt
};
let core = if self.response_style.is_some() {
core.replace(&default_response_format, "")
.trim_end()
.to_string()
} else {
core.trim_end().to_string()
};
parts.push(core);
parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
if let Some(ref style) = self.response_style {
parts.push(format!("## Response Format\n\n{}", style));
}
let style_guidelines = style.guidelines();
if style_guidelines.is_some() || self.guidelines.is_some() {
let mut guidelines_parts = Vec::new();
if let Some(sg) = style_guidelines {
guidelines_parts.push(sg.to_string());
}
if let Some(ref g) = self.guidelines {
guidelines_parts.push(g.clone());
}
parts.push(format!(
"## Guidelines\n\n{}",
guidelines_parts.join("\n\n")
));
}
if let Some(ref extra) = self.extra {
parts.push(extra.clone());
}
parts.join("\n\n")
}
pub fn is_empty(&self) -> bool {
self.style.is_none()
&& self.role.is_none()
&& self.guidelines.is_none()
&& self.response_style.is_none()
&& self.extra.is_none()
}
pub fn with_style(mut self, style: AgentStyle) -> Self {
self.style = Some(style);
self
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.role = Some(role.into());
self
}
pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
self.guidelines = Some(guidelines.into());
self
}
pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
self.response_style = Some(style.into());
self
}
pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
self.extra = Some(extra.into());
self
}
}
pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
let mut result = template.to_string();
for (key, value) in vars {
result = result.replace(&format!("{{{}}}", key), value);
}
result
}
#[cfg(test)]
#[path = "prompts/tests.rs"]
mod tests;