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