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