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