a3s_code_core/prompts.rs
1// Prompt Registry
2//
3// Central registry for all system prompts and prompt templates used in A3S Code.
4// Every LLM-facing prompt is externalized here as a compile-time `include_str!`
5// so the full agentic design is visible in one place.
6//
7// Directory layout:
8// prompts/
9// ├── common/ — shared runtime prompts used by multiple subsystems
10// ├── analysis/ — intent classification and pre-analysis prompts
11// ├── planning/ — planner and plan execution prompts
12// └── agents/ — built-in delegated-agent role prompts
13
14// ============================================================================
15// Default System Prompt
16// ============================================================================
17
18use crate::llm::LlmClient;
19use anyhow::Context;
20
21/// Default agentic system prompt — injected when no system prompt is configured.
22///
23/// Instructs the LLM to behave as an autonomous coding agent: use tools to act,
24/// verify results, and keep working until the task is fully complete.
25pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/common/system_default.md");
26
27/// Continuation message — injected as a user turn when the LLM stops without
28/// completing the task (i.e. stops calling tools mid-task).
29pub const CONTINUATION: &str = include_str!("../prompts/common/continuation.md");
30
31/// Safety boundaries (injection hygiene, secret handling, malicious-code refusal).
32///
33/// Single source of truth, appended to every assembled system prompt by
34/// [`SystemPromptSlots::build_with_style`] so it applies uniformly across all
35/// agent styles and delegated subagents (which build through the same path).
36pub const BOUNDARIES: &str = include_str!("../prompts/common/boundaries.md");
37
38// ============================================================================
39// Delegated Run Prompts
40// ============================================================================
41
42/// Explore delegated run — read-only codebase exploration
43pub const AGENT_EXPLORE: &str = include_str!("../prompts/agents/explore.md");
44
45/// Plan delegated run — read-only planning and analysis
46pub const AGENT_PLAN: &str = include_str!("../prompts/agents/plan.md");
47
48/// Code review delegated run — issue finding and review focus
49pub const AGENT_CODE_REVIEW: &str = include_str!("../prompts/agents/code_review.md");
50
51// ============================================================================
52// Session — Context Compaction
53// ============================================================================
54
55/// User template for context compaction. Placeholder: `{conversation}`
56pub const CONTEXT_COMPACT: &str = include_str!("../prompts/common/context_compact.md");
57
58/// Prefix for compacted summary messages
59pub const CONTEXT_SUMMARY_PREFIX: &str =
60 include_str!("../prompts/common/context_summary_prefix.md");
61
62// ============================================================================
63// LLM Planner — JSON-structured prompts
64// ============================================================================
65
66/// System prompt for LLM planner: plan creation (JSON output)
67pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/planning/llm_plan_system.md");
68
69/// System prompt for LLM planner: goal extraction (JSON output)
70pub const LLM_GOAL_EXTRACT_SYSTEM: &str =
71 include_str!("../prompts/planning/llm_goal_extract_system.md");
72
73/// System prompt for LLM planner: goal achievement check (JSON output)
74pub const LLM_GOAL_CHECK_SYSTEM: &str =
75 include_str!("../prompts/planning/llm_goal_check_system.md");
76
77/// System prompt for pre-analysis: combined intent + goal + plan + input optimization.
78pub const PRE_ANALYSIS_SYSTEM: &str = include_str!("../prompts/analysis/pre_analysis_system.md");
79
80// ============================================================================
81// Plan Execution Templates
82// ============================================================================
83
84/// Template for initial plan execution message
85pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execute_goal.md");
86
87/// Template for per-step execution prompt
88pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");
89
90/// Template for fallback plan step description
91pub const PLAN_FALLBACK_STEP: &str = include_str!("../prompts/planning/plan_fallback_step.md");
92
93/// Skill catalog header injected before listing available skill names/descriptions.
94pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
95
96// ============================================================================
97// Verification Agent
98// ============================================================================
99
100/// Verification agent — adversarial specialist that tries to break code
101pub const AGENT_VERIFICATION: &str = include_str!("../prompts/agents/verification.md");
102
103// ============================================================================
104// Intent Classification
105// ============================================================================
106
107/// System prompt for LLM-based intent classification
108pub const INTENT_CLASSIFY_SYSTEM: &str =
109 include_str!("../prompts/analysis/intent_classify_system.md");
110
111// ============================================================================
112// Planning Mode (Auto-Detection)
113// ============================================================================
114
115use serde::{Deserialize, Serialize};
116
117/// Planning mode — controls when planning phase is used.
118///
119/// When set to `Auto` (the default), the system detects from the user's
120/// message whether planning should be enabled. When explicitly `Enabled`,
121/// planning runs on every execution. When `Disabled`, planning is skipped.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
123pub enum PlanningMode {
124 /// Automatically detect from message content — enables planning when the
125 /// message benefits from structured pre-analysis. Local keyword detection is
126 /// only a fallback when pre-analysis is unavailable.
127 #[default]
128 Auto,
129 /// Explicitly disabled — never use planning phase.
130 Disabled,
131 /// Explicitly enabled — always use planning phase.
132 Enabled,
133}
134
135impl PlanningMode {
136 /// Returns true for the local no-LLM fallback path.
137 ///
138 /// Normal agent execution runs pre-analysis in `Auto` mode and uses its
139 /// structured `requires_planning` decision instead of this heuristic.
140 pub fn should_plan(&self, message: &str) -> bool {
141 match self {
142 PlanningMode::Auto => AgentStyle::detect_from_message(message).requires_planning(),
143 PlanningMode::Enabled => true,
144 PlanningMode::Disabled => false,
145 }
146 }
147}
148
149// ============================================================================
150// Agent Style (Intent-Based Prompt Selection)
151// ============================================================================
152
153/// Agent style — determines which system prompt template is used.
154///
155/// Each style has a different focus and behavior, selected based on the user's
156/// apparent intent from their message.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub enum AgentStyle {
159 /// Default — general purpose coding agent for research and multi-step tasks.
160 #[default]
161 GeneralPurpose,
162 /// Read-only planning and architecture analysis.
163 /// Prohibited from modifying files, focuses on design and planning.
164 Plan,
165 /// Adversarial verification specialist — tries to break code, not confirm it works.
166 Verification,
167 /// Fast file search and codebase exploration.
168 /// Read-only, optimized for finding files and patterns quickly.
169 Explore,
170 /// Code review focused — analyzes code quality, best practices, potential issues.
171 CodeReview,
172}
173
174/// Detection confidence level for style detection.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum DetectionConfidence {
177 /// High confidence — very specific keywords, skip LLM classification.
178 High,
179 /// Medium confidence — some indicators present, LLM classification helpful.
180 Medium,
181 /// Low confidence — no clear indicators, LLM classification recommended.
182 Low,
183}
184
185impl AgentStyle {
186 /// Returns the base system prompt for this style.
187 pub fn base_prompt(&self) -> &'static str {
188 match self {
189 AgentStyle::GeneralPurpose => SYSTEM_DEFAULT,
190 AgentStyle::Plan => AGENT_PLAN,
191 AgentStyle::Verification => AGENT_VERIFICATION,
192 AgentStyle::Explore => AGENT_EXPLORE,
193 AgentStyle::CodeReview => AGENT_CODE_REVIEW,
194 }
195 }
196
197 /// Returns style-specific guidelines if any.
198 pub fn guidelines(&self) -> Option<&'static str> {
199 match self {
200 AgentStyle::GeneralPurpose => None,
201 AgentStyle::Plan => None, // Already embedded in agents/plan.md
202 AgentStyle::Verification => None, // Already embedded in agents/verification.md
203 AgentStyle::Explore => None, // Already embedded in agents/explore.md
204 AgentStyle::CodeReview => None,
205 }
206 }
207
208 /// Returns a one-line description of this style.
209 pub fn description(&self) -> &'static str {
210 match self {
211 AgentStyle::GeneralPurpose => {
212 "General purpose coding agent for research and multi-step tasks"
213 }
214 AgentStyle::Plan => "Read-only planning and architecture analysis agent",
215 AgentStyle::Verification => "Adversarial verification specialist — tries to break code",
216 AgentStyle::Explore => "Fast read-only file search and codebase exploration agent",
217 AgentStyle::CodeReview => "Code review focused — analyzes quality and best practices",
218 }
219 }
220
221 /// Returns the canonical built-in delegated-agent name for this style.
222 pub fn builtin_agent_name(&self) -> &'static str {
223 match self {
224 AgentStyle::GeneralPurpose => "general",
225 AgentStyle::Plan => "plan",
226 AgentStyle::Verification => "verification",
227 AgentStyle::Explore => "explore",
228 AgentStyle::CodeReview => "review",
229 }
230 }
231
232 /// Returns the stable runtime mode label for UI/event consumers.
233 pub fn runtime_mode(&self) -> &'static str {
234 match self {
235 AgentStyle::GeneralPurpose => "general",
236 AgentStyle::Plan => "planning",
237 AgentStyle::Verification => "verification",
238 AgentStyle::Explore => "explore",
239 AgentStyle::CodeReview => "code_review",
240 }
241 }
242
243 /// Returns true if this style benefits from a planning phase.
244 ///
245 /// Planning is beneficial for styles that involve multi-step execution
246 /// or where a structured approach improves outcomes.
247 pub fn requires_planning(&self) -> bool {
248 matches!(self, AgentStyle::Plan)
249 }
250
251 /// Detects the most appropriate agent style based on user message content,
252 /// along with a confidence level.
253 ///
254 /// This is a local fallback for environments where LLM pre-analysis is
255 /// unavailable. Normal execution uses structured pre-analysis first.
256 pub fn detect_with_confidence(message: &str) -> (Self, DetectionConfidence) {
257 // Chinese text has high ambiguity in intent classification due to
258 // compound verb structures and context-dependent meaning.
259 // Bypass keyword matching entirely and route to LLM classification.
260 if message
261 .chars()
262 .any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
263 {
264 return (AgentStyle::GeneralPurpose, DetectionConfidence::Low);
265 }
266
267 let lower = message.to_lowercase();
268
269 // === HIGH CONFIDENCE: Very specific patterns ===
270
271 // Strong verification indicators
272 if lower.contains("try to break")
273 || lower.contains("find vulnerabilities")
274 || lower.contains("adversarial")
275 || lower.contains("security audit")
276 {
277 return (AgentStyle::Verification, DetectionConfidence::High);
278 }
279
280 // Strong plan indicators
281 if lower.contains("help me plan")
282 || lower.contains("help me design")
283 || lower.contains("create a plan")
284 || lower.contains("implementation plan")
285 || lower.contains("step-by-step plan")
286 {
287 return (AgentStyle::Plan, DetectionConfidence::High);
288 }
289
290 // Strong exploration indicators
291 if lower.contains("find all files")
292 || lower.contains("search for all")
293 || lower.contains("locate all")
294 {
295 return (AgentStyle::Explore, DetectionConfidence::High);
296 }
297
298 // === MEDIUM CONFIDENCE: Specific but less definitive ===
299
300 // Verification keywords
301 if lower.contains("verify")
302 || lower.contains("verification")
303 || lower.contains("break")
304 || lower.contains("debug")
305 || lower.contains("test")
306 || lower.contains("check if")
307 {
308 return (AgentStyle::Verification, DetectionConfidence::Medium);
309 }
310
311 // Plan keywords
312 if lower.contains("plan")
313 || lower.contains("design")
314 || lower.contains("architecture")
315 || lower.contains("approach")
316 {
317 return (AgentStyle::Plan, DetectionConfidence::Medium);
318 }
319
320 // Explore keywords
321 if lower.contains("find")
322 || lower.contains("search")
323 || lower.contains("where is")
324 || lower.contains("where's")
325 || lower.contains("locate")
326 || lower.contains("explore")
327 || lower.contains("look for")
328 {
329 return (AgentStyle::Explore, DetectionConfidence::Medium);
330 }
331
332 // Code review keywords
333 if lower.contains("review")
334 || lower.contains("code review")
335 || lower.contains("analyze")
336 || lower.contains("assess")
337 || lower.contains("quality")
338 || lower.contains("best practice")
339 {
340 return (AgentStyle::CodeReview, DetectionConfidence::Medium);
341 }
342
343 // No clear indicators
344 (AgentStyle::GeneralPurpose, DetectionConfidence::Low)
345 }
346
347 /// Detects the most appropriate agent style based on user message content.
348 ///
349 /// This is a local fallback heuristic. Normal execution uses structured
350 /// pre-analysis first; users can also explicitly set the style via
351 /// `SystemPromptSlots::with_style()`.
352 pub fn detect_from_message(message: &str) -> Self {
353 Self::detect_with_confidence(message).0
354 }
355
356 /// Classifies user intent using LLM when keyword confidence is low.
357 ///
358 /// This helper is available to callers that want explicit one-shot intent
359 /// classification outside the main pre-analysis path.
360 ///
361 /// Uses a lightweight classification prompt that returns a single word.
362 pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
363 use crate::llm::Message;
364
365 let system = INTENT_CLASSIFY_SYSTEM;
366 let messages = vec![Message::user(message)];
367
368 let response = llm
369 .complete(&messages, Some(system), &[])
370 .await
371 .context("LLM intent classification failed")?;
372
373 let text = response.text().trim().to_lowercase();
374
375 let style = match text.as_str() {
376 "plan" => AgentStyle::Plan,
377 "explore" => AgentStyle::Explore,
378 "verification" => AgentStyle::Verification,
379 "codereview" | "code review" => AgentStyle::CodeReview,
380 _ => AgentStyle::GeneralPurpose,
381 };
382
383 Ok(style)
384 }
385}
386
387// ============================================================================
388// System Prompt Slots
389// ============================================================================
390
391/// Slot-based system prompt customization with intent-based style selection.
392///
393/// Users can customize specific parts of the system prompt without overriding
394/// the core agentic capabilities (tool usage, autonomous behavior, completion
395/// criteria). The default agentic core is ALWAYS included.
396///
397/// ## Assembly Order
398///
399/// ```text
400/// [role] ← Custom identity/role (e.g. "You are a Python expert")
401/// [CORE] ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
402/// [guidelines] ← Custom coding rules / constraints
403/// [response_style] ← Custom response format (replaces default Response Format section)
404/// [extra] ← Freeform additional instructions
405/// ```
406///
407/// ## Intent-Based Selection
408///
409/// When `style` is left as `AgentStyle::GeneralPurpose` (the default), the
410/// system will attempt to detect the user's intent from their first message and
411/// automatically select an appropriate style. To override this behavior, explicitly
412/// set the `style` field.
413#[derive(Debug, Clone, Default)]
414pub struct SystemPromptSlots {
415 /// Agent style — determines which base prompt template is used.
416 ///
417 /// When `None` (default), the style is auto-detected from the user's message.
418 /// Explicitly set this to force a particular style regardless of message content.
419 pub style: Option<AgentStyle>,
420
421 /// Custom role/identity prepended before the core prompt.
422 ///
423 /// Example: "You are a senior Python developer specializing in FastAPI."
424 /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
425 pub role: Option<String>,
426
427 /// Custom coding guidelines appended after the core prompt sections.
428 ///
429 /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
430 pub guidelines: Option<String>,
431
432 /// Custom response style that replaces the default "Response Format" section.
433 ///
434 /// When `None`, the default response format is used.
435 pub response_style: Option<String>,
436
437 /// Freeform extra instructions appended at the very end.
438 pub extra: Option<String>,
439}
440
441/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
442const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
443
444/// The default response format section.
445const DEFAULT_RESPONSE_FORMAT: &str =
446 include_str!("../prompts/common/system_default_response_format.md");
447
448impl SystemPromptSlots {
449 /// Build the final system prompt by assembling slots around the core prompt.
450 ///
451 /// The core agentic behavior (Core Behaviour, Tool Usage Strategy, Completion
452 /// Criteria) is always preserved. Users can only customize the edges.
453 ///
454 /// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
455 /// `build_with_message()` to enable automatic intent-based style detection.
456 pub fn build(&self) -> String {
457 self.build_with_style(self.style.unwrap_or_default())
458 }
459
460 /// Build the final system prompt, auto-detecting style from the initial message.
461 ///
462 /// If `self.style` is explicitly set, that style is used regardless of message content.
463 /// Otherwise, the style is detected from `initial_message` using keyword analysis.
464 pub fn build_with_message(&self, initial_message: &str) -> String {
465 let style = self
466 .style
467 .unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
468 self.build_with_style(style)
469 }
470
471 /// Build the prompt with an explicitly specified style.
472 fn build_with_style(&self, style: AgentStyle) -> String {
473 let mut parts: Vec<String> = Vec::new();
474
475 // Normalize line endings: strip \r so string matching works on Windows
476 // where include_str! may produce \r\n if the file has CRLF endings.
477 let base_prompt = style.base_prompt().replace('\r', "");
478 let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
479 let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
480
481 // 1. Role: for GeneralPurpose, replace the default role line.
482 // For other styles (Plan, Explore, Verification), prepend custom role since
483 // those prompts have their own identity embedded.
484 let core = if let Some(ref role) = self.role {
485 if style == AgentStyle::GeneralPurpose {
486 let custom_role = format!(
487 "{}. You operate in an agentic loop: inspect, act with tools, observe results, and continue until the user's request is genuinely complete.",
488 role.trim_end_matches('.')
489 );
490 base_prompt.replace(&default_role_line, &custom_role)
491 } else {
492 // Prepend custom role for other styles
493 format!("{}\n\n{}", role, base_prompt)
494 }
495 } else {
496 base_prompt
497 };
498
499 // 2. Core: strip the default response format section if custom one is provided
500 let core = if self.response_style.is_some() {
501 core.replace(&default_response_format, "")
502 .trim_end()
503 .to_string()
504 } else {
505 core.trim_end().to_string()
506 };
507
508 parts.push(core);
509
510 // 2b. Safety boundaries — single source of truth, appended uniformly so
511 // every style and delegated subagent (which build through this path)
512 // carries injection-hygiene, secret-handling, and malware-refusal rules.
513 parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
514
515 // 3. Custom response style (replaces default Response Format)
516 if let Some(ref style) = self.response_style {
517 parts.push(format!("## Response Format\n\n{}", style));
518 }
519
520 // 4. Guidelines: style-specific + custom
521 let style_guidelines = style.guidelines();
522 if style_guidelines.is_some() || self.guidelines.is_some() {
523 let mut guidelines_parts = Vec::new();
524 if let Some(sg) = style_guidelines {
525 guidelines_parts.push(sg.to_string());
526 }
527 if let Some(ref g) = self.guidelines {
528 guidelines_parts.push(g.clone());
529 }
530 parts.push(format!(
531 "## Guidelines\n\n{}",
532 guidelines_parts.join("\n\n")
533 ));
534 }
535
536 // 5. Extra freeform instructions.
537 if let Some(ref extra) = self.extra {
538 parts.push(extra.clone());
539 }
540
541 parts.join("\n\n")
542 }
543
544 /// Returns true if all slots are empty (use pure default prompt).
545 pub fn is_empty(&self) -> bool {
546 self.style.is_none()
547 && self.role.is_none()
548 && self.guidelines.is_none()
549 && self.response_style.is_none()
550 && self.extra.is_none()
551 }
552
553 /// Set the agent style explicitly.
554 pub fn with_style(mut self, style: AgentStyle) -> Self {
555 self.style = Some(style);
556 self
557 }
558
559 /// Set the role/identity.
560 pub fn with_role(mut self, role: impl Into<String>) -> Self {
561 self.role = Some(role.into());
562 self
563 }
564
565 /// Set custom guidelines.
566 pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
567 self.guidelines = Some(guidelines.into());
568 self
569 }
570
571 /// Set custom response style.
572 pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
573 self.response_style = Some(style.into());
574 self
575 }
576
577 /// Set extra instructions.
578 pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
579 self.extra = Some(extra.into());
580 self
581 }
582}
583
584// ============================================================================
585// Helper Functions
586// ============================================================================
587
588/// Render a template by replacing `{key}` placeholders with values
589pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
590 let mut result = template.to_string();
591 for (key, value) in vars {
592 result = result.replace(&format!("{{{}}}", key), value);
593 }
594 result
595}
596
597#[cfg(test)]
598#[path = "prompts/tests.rs"]
599mod tests;