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