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 REPOSITORY_TOOL_CONTRACT: &str =
include_str!("../prompts/common/repository_tool_contract.md");
pub const REPOSITORY_TOOL_CONTRACT_READONLY: &str =
include_str!("../prompts/common/repository_tool_contract_readonly.md");
pub const RUNTIME_CONTRACT: &str = include_str!("../prompts/common/runtime_contract.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 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 uses_readonly_tool_contract(&self) -> bool {
matches!(
self,
AgentStyle::Plan
| AgentStyle::Explore
| AgentStyle::Verification
| AgentStyle::CodeReview
)
}
pub fn repository_tool_contract(&self) -> &'static str {
if self.uses_readonly_tool_contract() {
REPOSITORY_TOOL_CONTRACT_READONLY
} else {
REPOSITORY_TOOL_CONTRACT
}
}
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 output_language: 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!(
"{} Working inside the user's workspace, use the tools exposed in the current turn to inspect, change, and verify the requested work; 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(RUNTIME_CONTRACT.replace('\r', "").trim_end().to_string());
parts.push(
style
.repository_tool_contract()
.replace('\r', "")
.trim_end()
.to_string(),
);
parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
if let Some(language) = self
.output_language
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
parts.push(output_language_contract(language));
}
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.output_language.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_output_language(mut self, language: impl Into<String>) -> Self {
let language = language.into();
self.output_language = (!language.trim().is_empty()).then_some(language);
self
}
pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
self.extra = Some(extra.into());
self
}
}
pub fn output_language_contract(language: &str) -> String {
format!(
"## Output Language\n\n\
Write all user-visible product prose in {language}: replies, reasoning/thinking, \
plans, goals, step descriptions, and status updates. Use that one language only; \
do not mix languages across those surfaces.\n\
Keep code, identifiers, file paths, commands, URLs, and quoted source text in their original form.\n\
Do not switch languages because tool output or retrieved sources use another language."
)
}
pub fn resolve_product_output_language(pinned: Option<&str>, user_text: &str) -> Option<String> {
pinned
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_owned)
.or_else(|| infer_user_reply_language(user_text).map(str::to_owned))
}
pub fn infer_user_reply_language(text: &str) -> Option<&'static str> {
let mut han = 0u32;
let mut kana = 0u32;
let mut hangul = 0u32;
let mut latin = 0u32;
for ch in text.chars() {
if ch.is_ascii_alphabetic() {
latin = latin.saturating_add(1);
continue;
}
let code = ch as u32;
if (0x4E00..=0x9FFF).contains(&code) || (0x3400..=0x4DBF).contains(&code) {
han = han.saturating_add(1);
} else if (0x3040..=0x30FF).contains(&code) {
kana = kana.saturating_add(1);
} else if (0xAC00..=0xD7AF).contains(&code) || (0x1100..=0x11FF).contains(&code) {
hangul = hangul.saturating_add(1);
}
}
let marked = han + kana + hangul + latin;
if marked < 2 {
return None;
}
if hangul >= 2 && hangul >= han && hangul >= kana {
return Some("ko");
}
if kana >= 2 {
return Some("ja");
}
if han >= 2 {
return Some("zh-CN");
}
if latin >= 12 {
return Some("en");
}
None
}
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;