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