Skip to main content

a3s_code_core/
prompts.rs

1// Prompt Registry
2//
3// Central registry for all system prompts and prompt templates used in A3S Code.
4// Every LLM-facing prompt is externalized here as a compile-time `include_str!`
5// so the full agentic design is visible in one place.
6//
7// Directory layout:
8//   prompts/
9//   ├── common/   — shared runtime prompts used by multiple subsystems
10//   ├── analysis/ — intent classification and pre-analysis prompts
11//   ├── planning/ — planner and plan execution prompts
12//   └── agents/   — built-in delegated-agent role prompts
13
14// ============================================================================
15// Default System Prompt
16// ============================================================================
17
18use crate::llm::LlmClient;
19use anyhow::Context;
20
21/// Default agentic system prompt — injected when no system prompt is configured.
22///
23/// Instructs the LLM to behave as an autonomous coding agent: use tools to act,
24/// verify results, and keep working until the task is fully complete.
25pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/common/system_default.md");
26
27/// Continuation message — injected as a user turn when the LLM stops without
28/// completing the task (i.e. stops calling tools mid-task).
29pub const CONTINUATION: &str = include_str!("../prompts/common/continuation.md");
30
31/// Safety boundaries (injection hygiene, secret handling, malicious-code refusal).
32///
33/// Single source of truth, appended to every assembled system prompt by
34/// [`SystemPromptSlots::build_with_style`] so it applies uniformly across all
35/// agent styles and delegated subagents (which build through the same path).
36pub const BOUNDARIES: &str = include_str!("../prompts/common/boundaries.md");
37
38/// Canonical repository-tool arguments and context-efficient usage strategy.
39///
40/// Appended to every assembled system prompt so general, planning, exploration,
41/// review, and verification styles share one tool contract.
42pub const REPOSITORY_TOOL_CONTRACT: &str =
43    include_str!("../prompts/common/repository_tool_contract.md");
44
45// ============================================================================
46// Delegated Run Prompts
47// ============================================================================
48
49/// Explore delegated run — read-only codebase exploration
50pub const AGENT_EXPLORE: &str = include_str!("../prompts/agents/explore.md");
51
52/// Plan delegated run — read-only planning and analysis
53pub const AGENT_PLAN: &str = include_str!("../prompts/agents/plan.md");
54
55/// Code review delegated run — issue finding and review focus
56pub const AGENT_CODE_REVIEW: &str = include_str!("../prompts/agents/code_review.md");
57
58// ============================================================================
59// Session — Context Compaction
60// ============================================================================
61
62/// User template for context compaction. Placeholder: `{conversation}`
63pub const CONTEXT_COMPACT: &str = include_str!("../prompts/common/context_compact.md");
64
65/// Prefix for compacted summary messages
66pub const CONTEXT_SUMMARY_PREFIX: &str =
67    include_str!("../prompts/common/context_summary_prefix.md");
68
69// ============================================================================
70// LLM Planner — JSON-structured prompts
71// ============================================================================
72
73/// System prompt for LLM planner: plan creation (JSON output)
74pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/planning/llm_plan_system.md");
75
76/// System prompt for LLM planner: goal extraction (JSON output)
77pub const LLM_GOAL_EXTRACT_SYSTEM: &str =
78    include_str!("../prompts/planning/llm_goal_extract_system.md");
79
80/// System prompt for LLM planner: goal achievement check (JSON output)
81pub const LLM_GOAL_CHECK_SYSTEM: &str =
82    include_str!("../prompts/planning/llm_goal_check_system.md");
83
84/// System prompt for pre-analysis: combined intent + goal + plan + input optimization.
85pub const PRE_ANALYSIS_SYSTEM: &str = include_str!("../prompts/analysis/pre_analysis_system.md");
86
87// ============================================================================
88// Plan Execution Templates
89// ============================================================================
90
91/// Template for initial plan execution message
92pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execute_goal.md");
93
94/// Template for per-step execution prompt
95pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");
96
97/// Skill catalog header injected before listing available skill names/descriptions.
98pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
99
100// ============================================================================
101// Verification Agent
102// ============================================================================
103
104/// Verification agent — adversarial specialist that tries to break code
105pub const AGENT_VERIFICATION: &str = include_str!("../prompts/agents/verification.md");
106
107// ============================================================================
108// Intent Classification
109// ============================================================================
110
111/// System prompt for LLM-based intent classification
112pub const INTENT_CLASSIFY_SYSTEM: &str =
113    include_str!("../prompts/analysis/intent_classify_system.md");
114
115// ============================================================================
116// Planning Mode (Auto-Detection)
117// ============================================================================
118
119use serde::{Deserialize, Serialize};
120
121/// Planning mode — controls when planning phase is used.
122///
123/// When set to `Auto` (the default), the system detects from the user's
124/// message whether planning should be enabled. When explicitly `Enabled`,
125/// planning runs on every execution. When `Disabled`, planning is skipped.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
127pub enum PlanningMode {
128    /// Automatically detect from message content — enables planning when the
129    /// message benefits from structured pre-analysis. Local keyword detection is
130    /// only a fallback when pre-analysis is unavailable.
131    #[default]
132    Auto,
133    /// Explicitly disabled — never use planning phase.
134    Disabled,
135    /// Explicitly enabled — always use planning phase.
136    Enabled,
137}
138
139impl PlanningMode {
140    /// Returns true for the local no-LLM fallback path.
141    ///
142    /// Normal agent execution runs pre-analysis in `Auto` mode and uses its
143    /// structured `requires_planning` decision instead of this heuristic.
144    pub fn should_plan(&self, message: &str) -> bool {
145        match self {
146            PlanningMode::Auto => AgentStyle::detect_from_message(message).requires_planning(),
147            PlanningMode::Enabled => true,
148            PlanningMode::Disabled => false,
149        }
150    }
151}
152
153// ============================================================================
154// Agent Style (Intent-Based Prompt Selection)
155// ============================================================================
156
157/// Agent style — determines which system prompt template is used.
158///
159/// Each style has a different focus and behavior, selected based on the user's
160/// apparent intent from their message.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum AgentStyle {
163    /// Default — general purpose coding agent for research and multi-step tasks.
164    #[default]
165    GeneralPurpose,
166    /// Read-only planning and architecture analysis.
167    /// Prohibited from modifying files, focuses on design and planning.
168    Plan,
169    /// Adversarial verification specialist — tries to break code, not confirm it works.
170    Verification,
171    /// Fast file search and codebase exploration.
172    /// Read-only, optimized for finding files and patterns quickly.
173    Explore,
174    /// Code review focused — analyzes code quality, best practices, potential issues.
175    CodeReview,
176}
177
178/// Detection confidence level for style detection.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum DetectionConfidence {
181    /// High confidence — very specific keywords, skip LLM classification.
182    High,
183    /// Medium confidence — some indicators present, LLM classification helpful.
184    Medium,
185    /// Low confidence — no clear indicators, LLM classification recommended.
186    Low,
187}
188
189impl AgentStyle {
190    /// Returns the base system prompt for this style.
191    pub fn base_prompt(&self) -> &'static str {
192        match self {
193            AgentStyle::GeneralPurpose => SYSTEM_DEFAULT,
194            AgentStyle::Plan => AGENT_PLAN,
195            AgentStyle::Verification => AGENT_VERIFICATION,
196            AgentStyle::Explore => AGENT_EXPLORE,
197            AgentStyle::CodeReview => AGENT_CODE_REVIEW,
198        }
199    }
200
201    /// Returns style-specific guidelines if any.
202    pub fn guidelines(&self) -> Option<&'static str> {
203        match self {
204            AgentStyle::GeneralPurpose => None,
205            AgentStyle::Plan => None, // Already embedded in agents/plan.md
206            AgentStyle::Verification => None, // Already embedded in agents/verification.md
207            AgentStyle::Explore => None, // Already embedded in agents/explore.md
208            AgentStyle::CodeReview => None,
209        }
210    }
211
212    /// Returns a one-line description of this style.
213    pub fn description(&self) -> &'static str {
214        match self {
215            AgentStyle::GeneralPurpose => {
216                "General purpose coding agent for research and multi-step tasks"
217            }
218            AgentStyle::Plan => "Read-only planning and architecture analysis agent",
219            AgentStyle::Verification => "Adversarial verification specialist — tries to break code",
220            AgentStyle::Explore => "Fast read-only file search and codebase exploration agent",
221            AgentStyle::CodeReview => "Code review focused — analyzes quality and best practices",
222        }
223    }
224
225    /// Returns the canonical built-in delegated-agent name for this style.
226    pub fn builtin_agent_name(&self) -> &'static str {
227        match self {
228            AgentStyle::GeneralPurpose => "general",
229            AgentStyle::Plan => "plan",
230            AgentStyle::Verification => "verification",
231            AgentStyle::Explore => "explore",
232            AgentStyle::CodeReview => "review",
233        }
234    }
235
236    /// Returns the stable runtime mode label for UI/event consumers.
237    pub fn runtime_mode(&self) -> &'static str {
238        match self {
239            AgentStyle::GeneralPurpose => "general",
240            AgentStyle::Plan => "planning",
241            AgentStyle::Verification => "verification",
242            AgentStyle::Explore => "explore",
243            AgentStyle::CodeReview => "code_review",
244        }
245    }
246
247    /// Returns true if this style benefits from a planning phase.
248    ///
249    /// Planning is beneficial for styles that involve multi-step execution
250    /// or where a structured approach improves outcomes.
251    pub fn requires_planning(&self) -> bool {
252        matches!(self, AgentStyle::Plan)
253    }
254
255    /// Detects the most appropriate agent style based on user message content,
256    /// along with a confidence level.
257    ///
258    /// This is a local fallback for environments where LLM pre-analysis is
259    /// unavailable. Normal execution uses structured pre-analysis first.
260    pub fn detect_with_confidence(message: &str) -> (Self, DetectionConfidence) {
261        // Chinese text has high ambiguity in intent classification due to
262        // compound verb structures and context-dependent meaning.
263        // Bypass keyword matching entirely and route to LLM classification.
264        if message
265            .chars()
266            .any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
267        {
268            return (AgentStyle::GeneralPurpose, DetectionConfidence::Low);
269        }
270
271        let lower = message.to_lowercase();
272
273        // === HIGH CONFIDENCE: Very specific patterns ===
274
275        // Strong verification indicators
276        if lower.contains("try to break")
277            || lower.contains("find vulnerabilities")
278            || lower.contains("adversarial")
279            || lower.contains("security audit")
280        {
281            return (AgentStyle::Verification, DetectionConfidence::High);
282        }
283
284        // Strong plan indicators
285        if lower.contains("help me plan")
286            || lower.contains("help me design")
287            || lower.contains("create a plan")
288            || lower.contains("implementation plan")
289            || lower.contains("step-by-step plan")
290        {
291            return (AgentStyle::Plan, DetectionConfidence::High);
292        }
293
294        // Strong exploration indicators
295        if lower.contains("find all files")
296            || lower.contains("search for all")
297            || lower.contains("locate all")
298        {
299            return (AgentStyle::Explore, DetectionConfidence::High);
300        }
301
302        // === MEDIUM CONFIDENCE: Specific but less definitive ===
303
304        // Verification keywords
305        if lower.contains("verify")
306            || lower.contains("verification")
307            || lower.contains("break")
308            || lower.contains("debug")
309            || lower.contains("test")
310            || lower.contains("check if")
311        {
312            return (AgentStyle::Verification, DetectionConfidence::Medium);
313        }
314
315        // Plan keywords
316        if lower.contains("plan")
317            || lower.contains("design")
318            || lower.contains("architecture")
319            || lower.contains("approach")
320        {
321            return (AgentStyle::Plan, DetectionConfidence::Medium);
322        }
323
324        // Explore keywords
325        if lower.contains("find")
326            || lower.contains("search")
327            || lower.contains("where is")
328            || lower.contains("where's")
329            || lower.contains("locate")
330            || lower.contains("explore")
331            || lower.contains("look for")
332        {
333            return (AgentStyle::Explore, DetectionConfidence::Medium);
334        }
335
336        // Code review keywords
337        if lower.contains("review")
338            || lower.contains("code review")
339            || lower.contains("analyze")
340            || lower.contains("assess")
341            || lower.contains("quality")
342            || lower.contains("best practice")
343        {
344            return (AgentStyle::CodeReview, DetectionConfidence::Medium);
345        }
346
347        // No clear indicators
348        (AgentStyle::GeneralPurpose, DetectionConfidence::Low)
349    }
350
351    /// Detects the most appropriate agent style based on user message content.
352    ///
353    /// This is a local fallback heuristic. Normal execution uses structured
354    /// pre-analysis first; users can also explicitly set the style via
355    /// `SystemPromptSlots::with_style()`.
356    pub fn detect_from_message(message: &str) -> Self {
357        Self::detect_with_confidence(message).0
358    }
359
360    /// Classifies user intent using LLM when keyword confidence is low.
361    ///
362    /// This helper is available to callers that want explicit one-shot intent
363    /// classification outside the main pre-analysis path.
364    ///
365    /// Uses a lightweight classification prompt that returns a single word.
366    pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
367        use crate::llm::Message;
368
369        let system = INTENT_CLASSIFY_SYSTEM;
370        let messages = vec![Message::user(message)];
371
372        let response = llm
373            .complete(&messages, Some(system), &[])
374            .await
375            .context("LLM intent classification failed")?;
376
377        let text = response.text().trim().to_lowercase();
378
379        let style = match text.as_str() {
380            "plan" => AgentStyle::Plan,
381            "explore" => AgentStyle::Explore,
382            "verification" => AgentStyle::Verification,
383            "codereview" | "code review" => AgentStyle::CodeReview,
384            _ => AgentStyle::GeneralPurpose,
385        };
386
387        Ok(style)
388    }
389}
390
391// ============================================================================
392// System Prompt Slots
393// ============================================================================
394
395/// Slot-based system prompt customization with intent-based style selection.
396///
397/// Users can customize specific parts of the system prompt without overriding
398/// the core agentic capabilities (tool usage, autonomous behavior, completion
399/// criteria). The default agentic core is ALWAYS included.
400///
401/// ## Assembly Order
402///
403/// ```text
404/// [role]            ← Custom identity/role (e.g. "You are a Python expert")
405/// [CORE]            ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
406/// [guidelines]      ← Custom coding rules / constraints
407/// [response_style]  ← Custom response format (replaces default Response Format section)
408/// [extra]           ← Freeform additional instructions
409/// ```
410///
411/// ## Intent-Based Selection
412///
413/// When `style` is left as `AgentStyle::GeneralPurpose` (the default), the
414/// system will attempt to detect the user's intent from their first message and
415/// automatically select an appropriate style. To override this behavior, explicitly
416/// set the `style` field.
417#[derive(Debug, Clone, Default)]
418pub struct SystemPromptSlots {
419    /// Agent style — determines which base prompt template is used.
420    ///
421    /// When `None` (default), the style is auto-detected from the user's message.
422    /// Explicitly set this to force a particular style regardless of message content.
423    pub style: Option<AgentStyle>,
424
425    /// Custom role/identity prepended before the core prompt.
426    ///
427    /// Example: "You are a senior Python developer specializing in FastAPI."
428    /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
429    pub role: Option<String>,
430
431    /// Custom coding guidelines appended after the core prompt sections.
432    ///
433    /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
434    pub guidelines: Option<String>,
435
436    /// Custom response style that replaces the default "Response Format" section.
437    ///
438    /// When `None`, the default response format is used.
439    pub response_style: Option<String>,
440
441    /// Freeform extra instructions appended at the very end.
442    pub extra: Option<String>,
443}
444
445/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
446const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
447
448/// The default response format section.
449const DEFAULT_RESPONSE_FORMAT: &str =
450    include_str!("../prompts/common/system_default_response_format.md");
451
452impl SystemPromptSlots {
453    /// Build the final system prompt by assembling slots around the core prompt.
454    ///
455    /// The core agentic behavior (Core Behaviour, Tool Usage Strategy, Completion
456    /// Criteria), shared repository-tool contract, and safety boundaries are always
457    /// preserved. Users can only customize the edges.
458    ///
459    /// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
460    /// `build_with_message()` to enable automatic intent-based style detection.
461    pub fn build(&self) -> String {
462        self.build_with_style(self.style.unwrap_or_default())
463    }
464
465    /// Build the final system prompt, auto-detecting style from the initial message.
466    ///
467    /// If `self.style` is explicitly set, that style is used regardless of message content.
468    /// Otherwise, the style is detected from `initial_message` using keyword analysis.
469    pub fn build_with_message(&self, initial_message: &str) -> String {
470        let style = self
471            .style
472            .unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
473        self.build_with_style(style)
474    }
475
476    /// Build the prompt with an explicitly specified style.
477    fn build_with_style(&self, style: AgentStyle) -> String {
478        let mut parts: Vec<String> = Vec::new();
479
480        // Normalize line endings: strip \r so string matching works on Windows
481        // where include_str! may produce \r\n if the file has CRLF endings.
482        let base_prompt = style.base_prompt().replace('\r', "");
483        let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
484        let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
485
486        // 1. Role: for GeneralPurpose, replace the default role line.
487        // For other styles (Plan, Explore, Verification), prepend custom role since
488        // those prompts have their own identity embedded.
489        let core = if let Some(ref role) = self.role {
490            if style == AgentStyle::GeneralPurpose {
491                let custom_role = format!(
492                    "{}. You operate in an agentic loop: inspect, act with tools, observe results, and continue until the user's request is genuinely complete.",
493                    role.trim_end_matches('.')
494                );
495                base_prompt.replace(&default_role_line, &custom_role)
496            } else {
497                // Prepend custom role for other styles
498                format!("{}\n\n{}", role, base_prompt)
499            }
500        } else {
501            base_prompt
502        };
503
504        // 2. Core: strip the default response format section if custom one is provided
505        let core = if self.response_style.is_some() {
506            core.replace(&default_response_format, "")
507                .trim_end()
508                .to_string()
509        } else {
510            core.trim_end().to_string()
511        };
512
513        parts.push(core);
514
515        // 2b. Repository-tool contract — shared by every style so parameter and
516        // pagination guidance cannot drift between built-in agents.
517        parts.push(
518            REPOSITORY_TOOL_CONTRACT
519                .replace('\r', "")
520                .trim_end()
521                .to_string(),
522        );
523
524        // 2c. Safety boundaries — single source of truth, appended uniformly so
525        // every style and delegated subagent (which build through this path)
526        // carries injection-hygiene, secret-handling, and malware-refusal rules.
527        parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
528
529        // 3. Custom response style (replaces default Response Format)
530        if let Some(ref style) = self.response_style {
531            parts.push(format!("## Response Format\n\n{}", style));
532        }
533
534        // 4. Guidelines: style-specific + custom
535        let style_guidelines = style.guidelines();
536        if style_guidelines.is_some() || self.guidelines.is_some() {
537            let mut guidelines_parts = Vec::new();
538            if let Some(sg) = style_guidelines {
539                guidelines_parts.push(sg.to_string());
540            }
541            if let Some(ref g) = self.guidelines {
542                guidelines_parts.push(g.clone());
543            }
544            parts.push(format!(
545                "## Guidelines\n\n{}",
546                guidelines_parts.join("\n\n")
547            ));
548        }
549
550        // 5. Extra freeform instructions.
551        if let Some(ref extra) = self.extra {
552            parts.push(extra.clone());
553        }
554
555        parts.join("\n\n")
556    }
557
558    /// Returns true if all slots are empty (use pure default prompt).
559    pub fn is_empty(&self) -> bool {
560        self.style.is_none()
561            && self.role.is_none()
562            && self.guidelines.is_none()
563            && self.response_style.is_none()
564            && self.extra.is_none()
565    }
566
567    /// Set the agent style explicitly.
568    pub fn with_style(mut self, style: AgentStyle) -> Self {
569        self.style = Some(style);
570        self
571    }
572
573    /// Set the role/identity.
574    pub fn with_role(mut self, role: impl Into<String>) -> Self {
575        self.role = Some(role.into());
576        self
577    }
578
579    /// Set custom guidelines.
580    pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
581        self.guidelines = Some(guidelines.into());
582        self
583    }
584
585    /// Set custom response style.
586    pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
587        self.response_style = Some(style.into());
588        self
589    }
590
591    /// Set extra instructions.
592    pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
593        self.extra = Some(extra.into());
594        self
595    }
596}
597
598// ============================================================================
599// Helper Functions
600// ============================================================================
601
602/// Render a template by replacing `{key}` placeholders with values
603pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
604    let mut result = template.to_string();
605    for (key, value) in vars {
606        result = result.replace(&format!("{{{}}}", key), value);
607    }
608    result
609}
610
611#[cfg(test)]
612#[path = "prompts/tests.rs"]
613mod tests;