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