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/// Template for fallback plan step description
91pub const PLAN_FALLBACK_STEP: &str = include_str!("../prompts/planning/plan_fallback_step.md");
92
93/// Skill catalog header injected before listing available skill names/descriptions.
94pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
95
96// ============================================================================
97// Verification Agent
98// ============================================================================
99
100/// Verification agent — adversarial specialist that tries to break code
101pub const AGENT_VERIFICATION: &str = include_str!("../prompts/agents/verification.md");
102
103// ============================================================================
104// Intent Classification
105// ============================================================================
106
107/// System prompt for LLM-based intent classification
108pub const INTENT_CLASSIFY_SYSTEM: &str =
109    include_str!("../prompts/analysis/intent_classify_system.md");
110
111// ============================================================================
112// Planning Mode (Auto-Detection)
113// ============================================================================
114
115use serde::{Deserialize, Serialize};
116
117/// Planning mode — controls when planning phase is used.
118///
119/// When set to `Auto` (the default), the system detects from the user's
120/// message whether planning should be enabled. When explicitly `Enabled`,
121/// planning runs on every execution. When `Disabled`, planning is skipped.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
123pub enum PlanningMode {
124    /// Automatically detect from message content — enables planning when the
125    /// message benefits from structured pre-analysis. Local keyword detection is
126    /// only a fallback when pre-analysis is unavailable.
127    #[default]
128    Auto,
129    /// Explicitly disabled — never use planning phase.
130    Disabled,
131    /// Explicitly enabled — always use planning phase.
132    Enabled,
133}
134
135impl PlanningMode {
136    /// Returns true for the local no-LLM fallback path.
137    ///
138    /// Normal agent execution runs pre-analysis in `Auto` mode and uses its
139    /// structured `requires_planning` decision instead of this heuristic.
140    pub fn should_plan(&self, message: &str) -> bool {
141        match self {
142            PlanningMode::Auto => AgentStyle::detect_from_message(message).requires_planning(),
143            PlanningMode::Enabled => true,
144            PlanningMode::Disabled => false,
145        }
146    }
147}
148
149// ============================================================================
150// Agent Style (Intent-Based Prompt Selection)
151// ============================================================================
152
153/// Agent style — determines which system prompt template is used.
154///
155/// Each style has a different focus and behavior, selected based on the user's
156/// apparent intent from their message.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub enum AgentStyle {
159    /// Default — general purpose coding agent for research and multi-step tasks.
160    #[default]
161    GeneralPurpose,
162    /// Read-only planning and architecture analysis.
163    /// Prohibited from modifying files, focuses on design and planning.
164    Plan,
165    /// Adversarial verification specialist — tries to break code, not confirm it works.
166    Verification,
167    /// Fast file search and codebase exploration.
168    /// Read-only, optimized for finding files and patterns quickly.
169    Explore,
170    /// Code review focused — analyzes code quality, best practices, potential issues.
171    CodeReview,
172}
173
174/// Detection confidence level for style detection.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum DetectionConfidence {
177    /// High confidence — very specific keywords, skip LLM classification.
178    High,
179    /// Medium confidence — some indicators present, LLM classification helpful.
180    Medium,
181    /// Low confidence — no clear indicators, LLM classification recommended.
182    Low,
183}
184
185impl AgentStyle {
186    /// Returns the base system prompt for this style.
187    pub fn base_prompt(&self) -> &'static str {
188        match self {
189            AgentStyle::GeneralPurpose => SYSTEM_DEFAULT,
190            AgentStyle::Plan => AGENT_PLAN,
191            AgentStyle::Verification => AGENT_VERIFICATION,
192            AgentStyle::Explore => AGENT_EXPLORE,
193            AgentStyle::CodeReview => AGENT_CODE_REVIEW,
194        }
195    }
196
197    /// Returns style-specific guidelines if any.
198    pub fn guidelines(&self) -> Option<&'static str> {
199        match self {
200            AgentStyle::GeneralPurpose => None,
201            AgentStyle::Plan => None, // Already embedded in agents/plan.md
202            AgentStyle::Verification => None, // Already embedded in agents/verification.md
203            AgentStyle::Explore => None, // Already embedded in agents/explore.md
204            AgentStyle::CodeReview => None,
205        }
206    }
207
208    /// Returns a one-line description of this style.
209    pub fn description(&self) -> &'static str {
210        match self {
211            AgentStyle::GeneralPurpose => {
212                "General purpose coding agent for research and multi-step tasks"
213            }
214            AgentStyle::Plan => "Read-only planning and architecture analysis agent",
215            AgentStyle::Verification => "Adversarial verification specialist — tries to break code",
216            AgentStyle::Explore => "Fast read-only file search and codebase exploration agent",
217            AgentStyle::CodeReview => "Code review focused — analyzes quality and best practices",
218        }
219    }
220
221    /// Returns the canonical built-in delegated-agent name for this style.
222    pub fn builtin_agent_name(&self) -> &'static str {
223        match self {
224            AgentStyle::GeneralPurpose => "general",
225            AgentStyle::Plan => "plan",
226            AgentStyle::Verification => "verification",
227            AgentStyle::Explore => "explore",
228            AgentStyle::CodeReview => "review",
229        }
230    }
231
232    /// Returns the stable runtime mode label for UI/event consumers.
233    pub fn runtime_mode(&self) -> &'static str {
234        match self {
235            AgentStyle::GeneralPurpose => "general",
236            AgentStyle::Plan => "planning",
237            AgentStyle::Verification => "verification",
238            AgentStyle::Explore => "explore",
239            AgentStyle::CodeReview => "code_review",
240        }
241    }
242
243    /// Returns true if this style benefits from a planning phase.
244    ///
245    /// Planning is beneficial for styles that involve multi-step execution
246    /// or where a structured approach improves outcomes.
247    pub fn requires_planning(&self) -> bool {
248        matches!(self, AgentStyle::Plan)
249    }
250
251    /// Detects the most appropriate agent style based on user message content,
252    /// along with a confidence level.
253    ///
254    /// This is a local fallback for environments where LLM pre-analysis is
255    /// unavailable. Normal execution uses structured pre-analysis first.
256    pub fn detect_with_confidence(message: &str) -> (Self, DetectionConfidence) {
257        // Chinese text has high ambiguity in intent classification due to
258        // compound verb structures and context-dependent meaning.
259        // Bypass keyword matching entirely and route to LLM classification.
260        if message
261            .chars()
262            .any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
263        {
264            return (AgentStyle::GeneralPurpose, DetectionConfidence::Low);
265        }
266
267        let lower = message.to_lowercase();
268
269        // === HIGH CONFIDENCE: Very specific patterns ===
270
271        // Strong verification indicators
272        if lower.contains("try to break")
273            || lower.contains("find vulnerabilities")
274            || lower.contains("adversarial")
275            || lower.contains("security audit")
276        {
277            return (AgentStyle::Verification, DetectionConfidence::High);
278        }
279
280        // Strong plan indicators
281        if lower.contains("help me plan")
282            || lower.contains("help me design")
283            || lower.contains("create a plan")
284            || lower.contains("implementation plan")
285            || lower.contains("step-by-step plan")
286        {
287            return (AgentStyle::Plan, DetectionConfidence::High);
288        }
289
290        // Strong exploration indicators
291        if lower.contains("find all files")
292            || lower.contains("search for all")
293            || lower.contains("locate all")
294        {
295            return (AgentStyle::Explore, DetectionConfidence::High);
296        }
297
298        // === MEDIUM CONFIDENCE: Specific but less definitive ===
299
300        // Verification keywords
301        if lower.contains("verify")
302            || lower.contains("verification")
303            || lower.contains("break")
304            || lower.contains("debug")
305            || lower.contains("test")
306            || lower.contains("check if")
307        {
308            return (AgentStyle::Verification, DetectionConfidence::Medium);
309        }
310
311        // Plan keywords
312        if lower.contains("plan")
313            || lower.contains("design")
314            || lower.contains("architecture")
315            || lower.contains("approach")
316        {
317            return (AgentStyle::Plan, DetectionConfidence::Medium);
318        }
319
320        // Explore keywords
321        if lower.contains("find")
322            || lower.contains("search")
323            || lower.contains("where is")
324            || lower.contains("where's")
325            || lower.contains("locate")
326            || lower.contains("explore")
327            || lower.contains("look for")
328        {
329            return (AgentStyle::Explore, DetectionConfidence::Medium);
330        }
331
332        // Code review keywords
333        if lower.contains("review")
334            || lower.contains("code review")
335            || lower.contains("analyze")
336            || lower.contains("assess")
337            || lower.contains("quality")
338            || lower.contains("best practice")
339        {
340            return (AgentStyle::CodeReview, DetectionConfidence::Medium);
341        }
342
343        // No clear indicators
344        (AgentStyle::GeneralPurpose, DetectionConfidence::Low)
345    }
346
347    /// Detects the most appropriate agent style based on user message content.
348    ///
349    /// This is a local fallback heuristic. Normal execution uses structured
350    /// pre-analysis first; users can also explicitly set the style via
351    /// `SystemPromptSlots::with_style()`.
352    pub fn detect_from_message(message: &str) -> Self {
353        Self::detect_with_confidence(message).0
354    }
355
356    /// Classifies user intent using LLM when keyword confidence is low.
357    ///
358    /// This helper is available to callers that want explicit one-shot intent
359    /// classification outside the main pre-analysis path.
360    ///
361    /// Uses a lightweight classification prompt that returns a single word.
362    pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
363        use crate::llm::Message;
364
365        let system = INTENT_CLASSIFY_SYSTEM;
366        let messages = vec![Message::user(message)];
367
368        let response = llm
369            .complete(&messages, Some(system), &[])
370            .await
371            .context("LLM intent classification failed")?;
372
373        let text = response.text().trim().to_lowercase();
374
375        let style = match text.as_str() {
376            "plan" => AgentStyle::Plan,
377            "explore" => AgentStyle::Explore,
378            "verification" => AgentStyle::Verification,
379            "codereview" | "code review" => AgentStyle::CodeReview,
380            _ => AgentStyle::GeneralPurpose,
381        };
382
383        Ok(style)
384    }
385}
386
387// ============================================================================
388// System Prompt Slots
389// ============================================================================
390
391/// Slot-based system prompt customization with intent-based style selection.
392///
393/// Users can customize specific parts of the system prompt without overriding
394/// the core agentic capabilities (tool usage, autonomous behavior, completion
395/// criteria). The default agentic core is ALWAYS included.
396///
397/// ## Assembly Order
398///
399/// ```text
400/// [role]            ← Custom identity/role (e.g. "You are a Python expert")
401/// [CORE]            ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
402/// [guidelines]      ← Custom coding rules / constraints
403/// [response_style]  ← Custom response format (replaces default Response Format section)
404/// [extra]           ← Freeform additional instructions
405/// ```
406///
407/// ## Intent-Based Selection
408///
409/// When `style` is left as `AgentStyle::GeneralPurpose` (the default), the
410/// system will attempt to detect the user's intent from their first message and
411/// automatically select an appropriate style. To override this behavior, explicitly
412/// set the `style` field.
413#[derive(Debug, Clone, Default)]
414pub struct SystemPromptSlots {
415    /// Agent style — determines which base prompt template is used.
416    ///
417    /// When `None` (default), the style is auto-detected from the user's message.
418    /// Explicitly set this to force a particular style regardless of message content.
419    pub style: Option<AgentStyle>,
420
421    /// Custom role/identity prepended before the core prompt.
422    ///
423    /// Example: "You are a senior Python developer specializing in FastAPI."
424    /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
425    pub role: Option<String>,
426
427    /// Custom coding guidelines appended after the core prompt sections.
428    ///
429    /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
430    pub guidelines: Option<String>,
431
432    /// Custom response style that replaces the default "Response Format" section.
433    ///
434    /// When `None`, the default response format is used.
435    pub response_style: Option<String>,
436
437    /// Freeform extra instructions appended at the very end.
438    pub extra: Option<String>,
439}
440
441/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
442const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
443
444/// The default response format section.
445const DEFAULT_RESPONSE_FORMAT: &str =
446    include_str!("../prompts/common/system_default_response_format.md");
447
448impl SystemPromptSlots {
449    /// Build the final system prompt by assembling slots around the core prompt.
450    ///
451    /// The core agentic behavior (Core Behaviour, Tool Usage Strategy, Completion
452    /// Criteria) is always preserved. Users can only customize the edges.
453    ///
454    /// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
455    /// `build_with_message()` to enable automatic intent-based style detection.
456    pub fn build(&self) -> String {
457        self.build_with_style(self.style.unwrap_or_default())
458    }
459
460    /// Build the final system prompt, auto-detecting style from the initial message.
461    ///
462    /// If `self.style` is explicitly set, that style is used regardless of message content.
463    /// Otherwise, the style is detected from `initial_message` using keyword analysis.
464    pub fn build_with_message(&self, initial_message: &str) -> String {
465        let style = self
466            .style
467            .unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
468        self.build_with_style(style)
469    }
470
471    /// Build the prompt with an explicitly specified style.
472    fn build_with_style(&self, style: AgentStyle) -> String {
473        let mut parts: Vec<String> = Vec::new();
474
475        // Normalize line endings: strip \r so string matching works on Windows
476        // where include_str! may produce \r\n if the file has CRLF endings.
477        let base_prompt = style.base_prompt().replace('\r', "");
478        let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
479        let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
480
481        // 1. Role: for GeneralPurpose, replace the default role line.
482        // For other styles (Plan, Explore, Verification), prepend custom role since
483        // those prompts have their own identity embedded.
484        let core = if let Some(ref role) = self.role {
485            if style == AgentStyle::GeneralPurpose {
486                let custom_role = format!(
487                    "{}. You operate in an agentic loop: inspect, act with tools, observe results, and continue until the user's request is genuinely complete.",
488                    role.trim_end_matches('.')
489                );
490                base_prompt.replace(&default_role_line, &custom_role)
491            } else {
492                // Prepend custom role for other styles
493                format!("{}\n\n{}", role, base_prompt)
494            }
495        } else {
496            base_prompt
497        };
498
499        // 2. Core: strip the default response format section if custom one is provided
500        let core = if self.response_style.is_some() {
501            core.replace(&default_response_format, "")
502                .trim_end()
503                .to_string()
504        } else {
505            core.trim_end().to_string()
506        };
507
508        parts.push(core);
509
510        // 2b. Safety boundaries — single source of truth, appended uniformly so
511        // every style and delegated subagent (which build through this path)
512        // carries injection-hygiene, secret-handling, and malware-refusal rules.
513        parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
514
515        // 3. Custom response style (replaces default Response Format)
516        if let Some(ref style) = self.response_style {
517            parts.push(format!("## Response Format\n\n{}", style));
518        }
519
520        // 4. Guidelines: style-specific + custom
521        let style_guidelines = style.guidelines();
522        if style_guidelines.is_some() || self.guidelines.is_some() {
523            let mut guidelines_parts = Vec::new();
524            if let Some(sg) = style_guidelines {
525                guidelines_parts.push(sg.to_string());
526            }
527            if let Some(ref g) = self.guidelines {
528                guidelines_parts.push(g.clone());
529            }
530            parts.push(format!(
531                "## Guidelines\n\n{}",
532                guidelines_parts.join("\n\n")
533            ));
534        }
535
536        // 5. Extra freeform instructions.
537        if let Some(ref extra) = self.extra {
538            parts.push(extra.clone());
539        }
540
541        parts.join("\n\n")
542    }
543
544    /// Returns true if all slots are empty (use pure default prompt).
545    pub fn is_empty(&self) -> bool {
546        self.style.is_none()
547            && self.role.is_none()
548            && self.guidelines.is_none()
549            && self.response_style.is_none()
550            && self.extra.is_none()
551    }
552
553    /// Set the agent style explicitly.
554    pub fn with_style(mut self, style: AgentStyle) -> Self {
555        self.style = Some(style);
556        self
557    }
558
559    /// Set the role/identity.
560    pub fn with_role(mut self, role: impl Into<String>) -> Self {
561        self.role = Some(role.into());
562        self
563    }
564
565    /// Set custom guidelines.
566    pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
567        self.guidelines = Some(guidelines.into());
568        self
569    }
570
571    /// Set custom response style.
572    pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
573        self.response_style = Some(style.into());
574        self
575    }
576
577    /// Set extra instructions.
578    pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
579        self.extra = Some(extra.into());
580        self
581    }
582}
583
584// ============================================================================
585// Helper Functions
586// ============================================================================
587
588/// Render a template by replacing `{key}` placeholders with values
589pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
590    let mut result = template.to_string();
591    for (key, value) in vars {
592        result = result.replace(&format!("{{{}}}", key), value);
593    }
594    result
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    #[test]
602    fn test_all_prompts_loaded() {
603        // Verify all prompts are non-empty at compile time
604        assert!(!SYSTEM_DEFAULT.is_empty());
605        assert!(!CONTINUATION.is_empty());
606        assert!(!BOUNDARIES.is_empty());
607        assert!(!AGENT_EXPLORE.is_empty());
608        assert!(!AGENT_PLAN.is_empty());
609        assert!(!AGENT_CODE_REVIEW.is_empty());
610        assert!(!CONTEXT_COMPACT.is_empty());
611        assert!(!LLM_PLAN_SYSTEM.is_empty());
612        assert!(!LLM_GOAL_EXTRACT_SYSTEM.is_empty());
613        assert!(!LLM_GOAL_CHECK_SYSTEM.is_empty());
614        assert!(!SKILLS_CATALOG_HEADER.is_empty());
615        assert!(!PLAN_EXECUTE_GOAL.is_empty());
616        assert!(!PLAN_EXECUTE_STEP.is_empty());
617        assert!(!PLAN_FALLBACK_STEP.is_empty());
618    }
619
620    #[test]
621    fn test_render_template() {
622        let result = render(
623            PLAN_EXECUTE_GOAL,
624            &[("goal", "Build app"), ("steps", "1. Init")],
625        );
626        assert!(result.contains("Build app"));
627        assert!(!result.contains("{goal}"));
628    }
629
630    #[test]
631    fn test_render_multiple_placeholders() {
632        let template = "Goal: {goal}\nCriteria: {criteria}\nState: {current_state}";
633        let result = render(
634            template,
635            &[
636                ("goal", "Build a REST API"),
637                ("criteria", "- Endpoint works\n- Tests pass"),
638                ("current_state", "API is deployed"),
639            ],
640        );
641        assert!(result.contains("Build a REST API"));
642        assert!(result.contains("Endpoint works"));
643        assert!(result.contains("API is deployed"));
644    }
645
646    #[test]
647    fn test_delegated_agent_prompts_contain_guidelines() {
648        assert!(AGENT_EXPLORE.contains("Guidelines"));
649        assert!(AGENT_EXPLORE.contains("read-only"));
650        assert!(AGENT_PLAN.contains("Guidelines"));
651        assert!(AGENT_PLAN.contains("read-only"));
652    }
653
654    #[test]
655    fn test_context_summary_prefix() {
656        assert!(CONTEXT_SUMMARY_PREFIX.contains("Context Summary"));
657    }
658
659    // ── SystemPromptSlots tests ──
660
661    #[test]
662    fn test_slots_default_builds_system_default() {
663        let slots = SystemPromptSlots::default();
664        let built = slots.build();
665        assert!(built.contains("Core Behaviour"));
666        assert!(built.contains("Tool Usage Strategy"));
667        assert!(built.contains("Completion Criteria"));
668        assert!(built.contains("Response Format"));
669        assert!(built.contains("A3S Code"));
670        // Safety boundaries are injected even though they no longer live inline
671        // in system_default.md.
672        assert!(built.contains("## Boundaries"));
673        assert!(built.contains("untrusted data"));
674    }
675
676    #[test]
677    fn test_boundaries_injected_for_every_style() {
678        for style in [
679            AgentStyle::GeneralPurpose,
680            AgentStyle::Plan,
681            AgentStyle::Verification,
682            AgentStyle::Explore,
683            AgentStyle::CodeReview,
684        ] {
685            let built = SystemPromptSlots::default().with_style(style).build();
686            assert!(
687                built.contains("## Boundaries"),
688                "style {style:?} missing Boundaries section"
689            );
690        }
691    }
692
693    #[test]
694    fn test_boundaries_not_duplicated_in_general_purpose() {
695        // system_default.md must NOT carry an inline copy (single source of truth
696        // is boundaries.md, injected by build_with_style).
697        assert!(!SYSTEM_DEFAULT.contains("## Boundaries"));
698        let built = SystemPromptSlots::default().build();
699        assert_eq!(built.matches("## Boundaries").count(), 1);
700    }
701
702    #[test]
703    fn test_slots_custom_role_replaces_default() {
704        let slots = SystemPromptSlots {
705            role: Some("You are a senior Python developer".to_string()),
706            ..Default::default()
707        };
708        let built = slots.build();
709        assert!(built.contains("You are a senior Python developer"));
710        assert!(!built.contains("You are A3S Code"));
711        // Core sections still present
712        assert!(built.contains("Core Behaviour"));
713        assert!(built.contains("Tool Usage Strategy"));
714    }
715
716    #[test]
717    fn test_slots_custom_guidelines_appended() {
718        let slots = SystemPromptSlots {
719            guidelines: Some("Always use type hints. Follow PEP 8.".to_string()),
720            ..Default::default()
721        };
722        let built = slots.build();
723        assert!(built.contains("## Guidelines"));
724        assert!(built.contains("Always use type hints"));
725        assert!(built.contains("Core Behaviour"));
726    }
727
728    #[test]
729    fn test_slots_custom_response_style_replaces_default() {
730        let slots = SystemPromptSlots {
731            response_style: Some("Be concise. Use bullet points.".to_string()),
732            ..Default::default()
733        };
734        let built = slots.build();
735        assert!(built.contains("Be concise. Use bullet points."));
736        // Default response format content should be gone
737        assert!(!built.contains("keep progress notes brief and useful"));
738        // But core is still there
739        assert!(built.contains("Core Behaviour"));
740    }
741
742    #[test]
743    fn test_slots_extra_appended() {
744        let slots = SystemPromptSlots {
745            extra: Some("Remember: always write tests first.".to_string()),
746            ..Default::default()
747        };
748        let built = slots.build();
749        assert!(built.contains("Remember: always write tests first."));
750        assert!(built.contains("Core Behaviour"));
751    }
752
753    #[test]
754    fn test_slots_with_extra() {
755        let slots = SystemPromptSlots::default().with_extra("You are a helpful assistant.");
756        let built = slots.build();
757        assert!(built.contains("You are a helpful assistant."));
758        assert!(built.contains("Core Behaviour"));
759        assert!(built.contains("Tool Usage Strategy"));
760    }
761
762    #[test]
763    fn test_slots_all_slots_combined() {
764        let slots = SystemPromptSlots {
765            style: None,
766            role: Some("You are a Rust expert".to_string()),
767            guidelines: Some("Use clippy. No unwrap.".to_string()),
768            response_style: Some("Short answers only.".to_string()),
769            extra: Some("Project uses tokio.".to_string()),
770        };
771        let built = slots.build();
772        assert!(built.contains("You are a Rust expert"));
773        assert!(built.contains("Core Behaviour"));
774        assert!(built.contains("## Guidelines"));
775        assert!(built.contains("Use clippy"));
776        assert!(built.contains("Short answers only"));
777        assert!(built.contains("Project uses tokio"));
778        // Default response format replaced
779        assert!(!built.contains("keep progress notes brief and useful"));
780    }
781
782    #[test]
783    fn test_slots_is_empty() {
784        assert!(SystemPromptSlots::default().is_empty());
785        assert!(!SystemPromptSlots {
786            role: Some("test".to_string()),
787            ..Default::default()
788        }
789        .is_empty());
790        assert!(!SystemPromptSlots {
791            style: Some(AgentStyle::Plan),
792            ..Default::default()
793        }
794        .is_empty());
795    }
796
797    // ── AgentStyle tests ──
798
799    #[test]
800    fn test_agent_style_default_is_general_purpose() {
801        assert_eq!(AgentStyle::default(), AgentStyle::GeneralPurpose);
802    }
803
804    #[test]
805    fn test_agent_style_base_prompt() {
806        assert_eq!(AgentStyle::GeneralPurpose.base_prompt(), SYSTEM_DEFAULT);
807        assert_eq!(AgentStyle::Plan.base_prompt(), AGENT_PLAN);
808        assert_eq!(AgentStyle::Explore.base_prompt(), AGENT_EXPLORE);
809        assert_eq!(AgentStyle::Verification.base_prompt(), AGENT_VERIFICATION);
810        assert_eq!(AgentStyle::CodeReview.base_prompt(), AGENT_CODE_REVIEW);
811    }
812
813    #[test]
814    fn test_agent_style_guidelines() {
815        assert!(AgentStyle::GeneralPurpose.guidelines().is_none());
816        assert!(AgentStyle::Plan.guidelines().is_none()); // embedded in prompt
817        assert!(AgentStyle::Explore.guidelines().is_none());
818        assert!(AgentStyle::Verification.guidelines().is_none());
819        assert!(AgentStyle::CodeReview.guidelines().is_none());
820    }
821
822    #[test]
823    fn test_agent_style_builtin_agent_name_mapping() {
824        assert_eq!(AgentStyle::GeneralPurpose.builtin_agent_name(), "general");
825        assert_eq!(AgentStyle::Plan.builtin_agent_name(), "plan");
826        assert_eq!(AgentStyle::Explore.builtin_agent_name(), "explore");
827        assert_eq!(
828            AgentStyle::Verification.builtin_agent_name(),
829            "verification"
830        );
831        assert_eq!(AgentStyle::CodeReview.builtin_agent_name(), "review");
832    }
833
834    #[test]
835    fn test_agent_style_runtime_mode_mapping() {
836        assert_eq!(AgentStyle::GeneralPurpose.runtime_mode(), "general");
837        assert_eq!(AgentStyle::Plan.runtime_mode(), "planning");
838        assert_eq!(AgentStyle::Explore.runtime_mode(), "explore");
839        assert_eq!(AgentStyle::Verification.runtime_mode(), "verification");
840        assert_eq!(AgentStyle::CodeReview.runtime_mode(), "code_review");
841    }
842
843    #[test]
844    fn test_agent_style_detect_plan() {
845        assert_eq!(
846            AgentStyle::detect_from_message("Help me plan a new feature"),
847            AgentStyle::Plan
848        );
849        assert_eq!(
850            AgentStyle::detect_from_message("Design the architecture for this"),
851            AgentStyle::Plan
852        );
853        assert_eq!(
854            AgentStyle::detect_from_message("What's the implementation approach?"),
855            AgentStyle::Plan
856        );
857    }
858
859    #[test]
860    fn test_agent_style_detect_verification() {
861        assert_eq!(
862            AgentStyle::detect_from_message("Verify that this works correctly"),
863            AgentStyle::Verification
864        );
865        assert_eq!(
866            AgentStyle::detect_from_message("Test the login flow"),
867            AgentStyle::Verification
868        );
869        assert_eq!(
870            AgentStyle::detect_from_message("Check if the API handles edge cases"),
871            AgentStyle::Verification
872        );
873    }
874
875    #[test]
876    fn test_agent_style_detect_explore() {
877        assert_eq!(
878            AgentStyle::detect_from_message("Find all files related to auth"),
879            AgentStyle::Explore
880        );
881        assert_eq!(
882            AgentStyle::detect_from_message("Where is the user model defined?"),
883            AgentStyle::Explore
884        );
885        assert_eq!(
886            AgentStyle::detect_from_message("Search for password hashing code"),
887            AgentStyle::Explore
888        );
889    }
890
891    #[test]
892    fn test_agent_style_detect_code_review() {
893        assert_eq!(
894            AgentStyle::detect_from_message("Review the PR changes"),
895            AgentStyle::CodeReview
896        );
897        assert_eq!(
898            AgentStyle::detect_from_message("Analyze this code for best practices"),
899            AgentStyle::CodeReview
900        );
901        assert_eq!(
902            AgentStyle::detect_from_message("Assess code quality"),
903            AgentStyle::CodeReview
904        );
905    }
906
907    #[test]
908    fn test_agent_style_detect_default_is_general_purpose() {
909        // "Implement" was removed from Plan keywords as too generic
910        assert_eq!(
911            AgentStyle::detect_from_message("Implement the new feature"),
912            AgentStyle::GeneralPurpose
913        );
914        // "Write tests" contains "test" so it's detected as Verification
915        assert_eq!(
916            AgentStyle::detect_from_message("Write code for the API"),
917            AgentStyle::GeneralPurpose
918        );
919    }
920
921    #[test]
922    fn test_build_with_message_auto_detects_style() {
923        let slots = SystemPromptSlots::default();
924        let built = slots.build_with_message("Help me plan a new feature");
925        // Should use Plan style
926        assert!(built.contains("planning agent") || built.contains("READ-ONLY"));
927    }
928
929    #[test]
930    fn test_build_with_message_explicit_style_overrides() {
931        let slots = SystemPromptSlots {
932            style: Some(AgentStyle::Verification),
933            ..Default::default()
934        };
935        let built = slots.build_with_message("Help me plan a new feature");
936        // Should use Verification style, not Plan
937        assert!(built.contains("adversarial verification specialist"));
938    }
939
940    #[test]
941    fn test_build_with_message_plan_style() {
942        let slots = SystemPromptSlots::default();
943        let built = slots.build_with_message("Design the system architecture");
944        assert!(built.contains("planning agent") || built.contains("READ-ONLY"));
945    }
946
947    #[test]
948    fn test_build_with_message_explore_style() {
949        let slots = SystemPromptSlots::default();
950        let built = slots.build_with_message("Find all authentication files");
951        assert!(built.contains("exploration agent") || built.contains("explore"));
952    }
953
954    #[test]
955    fn test_build_with_message_code_review_style() {
956        let slots = SystemPromptSlots::default();
957        let built = slots.build_with_message("Review this code");
958        assert!(built.contains("code review agent"));
959        assert!(built.contains("regressions"));
960    }
961
962    #[test]
963    fn test_builder_methods() {
964        let slots = SystemPromptSlots::default()
965            .with_style(AgentStyle::Plan)
966            .with_role("You are a Python expert")
967            .with_guidelines("Use type hints")
968            .with_response_style("Be brief")
969            .with_extra("Additional instructions");
970
971        assert_eq!(slots.style, Some(AgentStyle::Plan));
972        assert_eq!(slots.role, Some("You are a Python expert".to_string()));
973        assert_eq!(slots.guidelines, Some("Use type hints".to_string()));
974        assert_eq!(slots.response_style, Some("Be brief".to_string()));
975        assert_eq!(slots.extra, Some("Additional instructions".to_string()));
976
977        let built = slots.build();
978        assert!(built.contains("Python expert"));
979        assert!(built.contains("Use type hints"));
980        assert!(built.contains("Be brief"));
981        assert!(built.contains("Additional instructions"));
982    }
983
984    #[test]
985    fn test_code_review_guidelines_appended() {
986        let slots = SystemPromptSlots {
987            style: Some(AgentStyle::CodeReview),
988            ..Default::default()
989        };
990        let built = slots.build();
991        assert!(built.contains("code review agent"));
992        assert!(built.contains("correctness"));
993        assert!(built.contains("regressions"));
994        assert!(built.contains("security"));
995    }
996
997    #[test]
998    fn test_prompts_do_not_reference_removed_surfaces() {
999        let prompts = [
1000            SYSTEM_DEFAULT,
1001            AGENT_VERIFICATION,
1002            PRE_ANALYSIS_SYSTEM,
1003            AGENT_EXPLORE,
1004            AGENT_PLAN,
1005            AGENT_CODE_REVIEW,
1006            SKILLS_CATALOG_HEADER,
1007            CONTINUATION,
1008        ]
1009        .join("\n")
1010        .to_lowercase();
1011
1012        for removed in [
1013            "orchestrator",
1014            "plugin",
1015            "agentic_search",
1016            "agentic_parse",
1017            "agentic-search",
1018            "agentic-parse",
1019            "manage_skill",
1020            "claude.md",
1021        ] {
1022            assert!(
1023                !prompts.contains(removed),
1024                "prompt still references removed surface: {removed}"
1025            );
1026        }
1027
1028        assert!(SYSTEM_DEFAULT.contains("program"));
1029        assert!(SYSTEM_DEFAULT.contains("task"));
1030        assert!(SYSTEM_DEFAULT.contains("parallel_task"));
1031        assert!(SYSTEM_DEFAULT.contains("AHP"));
1032    }
1033}