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