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