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