Skip to main content

a3s_code_core/
prompts.rs

1// Prompt Registry
2//
3// Central registry for all system prompts and prompt templates used in A3S Code.
4// Every LLM-facing prompt is externalized here as a compile-time `include_str!`
5// so the full agentic design is visible in one place.
6//
7// Directory layout:
8//   prompts/
9//   ├── subagent_explore.md         — Explore subagent system prompt
10//   ├── subagent_plan.md            — Plan subagent system prompt
11//   ├── subagent_title.md           — Title generation subagent prompt
12//   ├── subagent_summary.md         — Summary generation subagent prompt
13//   ├── context_compact.md          — Context compaction / summarization
14//   ├── title_generate.md           — Session title generation
15//   ├── llm_plan_system.md          — LLM planner: plan creation (JSON)
16//   ├── llm_goal_extract_system.md  — LLM planner: goal extraction (JSON)
17//   └── llm_goal_check_system.md    — LLM planner: goal achievement (JSON)
18
19// ============================================================================
20// Default System Prompt
21// ============================================================================
22
23/// Default agentic system prompt — injected when no system prompt is configured.
24///
25/// Instructs the LLM to behave as an autonomous coding agent: use tools to act,
26/// verify results, and keep working until the task is fully complete.
27pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/system_default.md");
28
29/// Continuation message — injected as a user turn when the LLM stops without
30/// completing the task (i.e. stops calling tools mid-task).
31pub const CONTINUATION: &str = include_str!("../prompts/continuation.md");
32
33// ============================================================================
34// Subagent Prompts
35// ============================================================================
36
37/// Explore subagent — read-only codebase exploration
38pub const SUBAGENT_EXPLORE: &str = include_str!("../prompts/subagent_explore.md");
39
40/// Plan subagent — read-only planning and analysis
41pub const SUBAGENT_PLAN: &str = include_str!("../prompts/subagent_plan.md");
42
43/// Title subagent — generate concise conversation title
44pub const SUBAGENT_TITLE: &str = include_str!("../prompts/subagent_title.md");
45
46/// Summary subagent — summarize conversation key points
47pub const SUBAGENT_SUMMARY: &str = include_str!("../prompts/subagent_summary.md");
48
49// ============================================================================
50// Session — Context Compaction
51// ============================================================================
52
53/// User template for context compaction. Placeholder: `{conversation}`
54pub const CONTEXT_COMPACT: &str = include_str!("../prompts/context_compact.md");
55
56/// Prefix for compacted summary messages
57pub const CONTEXT_SUMMARY_PREFIX: &str =
58    "[Context Summary: The following is a summary of earlier conversation]\n\n";
59
60// ============================================================================
61// Session — Title Generation
62// ============================================================================
63
64/// User template for session title generation. Placeholder: `{conversation}`
65#[allow(dead_code)]
66pub const TITLE_GENERATE: &str = include_str!("../prompts/title_generate.md");
67
68// ============================================================================
69// LLM Planner — JSON-structured prompts
70// ============================================================================
71
72/// System prompt for LLM planner: plan creation (JSON output)
73pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/llm_plan_system.md");
74
75/// System prompt for LLM planner: goal extraction (JSON output)
76pub const LLM_GOAL_EXTRACT_SYSTEM: &str = include_str!("../prompts/llm_goal_extract_system.md");
77
78/// System prompt for LLM planner: goal achievement check (JSON output)
79pub const LLM_GOAL_CHECK_SYSTEM: &str = include_str!("../prompts/llm_goal_check_system.md");
80
81// ============================================================================
82// Plan Execution (inline templates — no file needed)
83// ============================================================================
84
85/// Template for initial plan execution message
86pub const PLAN_EXECUTE_GOAL: &str =
87    "Goal: {goal}\n\nExecute the following plan step by step:\n{steps}";
88
89/// Template for per-step execution prompt
90pub const PLAN_EXECUTE_STEP: &str = "Execute step {step_num}: {description}";
91
92/// Template for fallback plan step description
93pub const PLAN_FALLBACK_STEP: &str = "Execute step {step_num} of the plan";
94
95/// Template for merging results from parallel step execution
96pub const PLAN_PARALLEL_RESULTS: &str =
97    "The following steps were executed in parallel:\n{results}\n\nContinue with the next steps.";
98
99// ============================================================================
100// System Prompt Slots
101// ============================================================================
102
103/// Slot-based system prompt customization.
104///
105/// Users can customize specific parts of the system prompt without overriding
106/// the core agentic capabilities (tool usage, autonomous behavior, completion
107/// criteria). The default agentic core is ALWAYS included.
108///
109/// ## Assembly Order
110///
111/// ```text
112/// [role]            ← Custom identity/role (e.g. "You are a Python expert")
113/// [CORE]            ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
114/// [guidelines]      ← Custom coding rules / constraints
115/// [response_style]  ← Custom response format (replaces default Response Format section)
116/// [extra]           ← Freeform additional instructions
117/// ```
118#[derive(Debug, Clone, Default)]
119pub struct SystemPromptSlots {
120    /// Custom role/identity prepended before the core prompt.
121    ///
122    /// Example: "You are a senior Python developer specializing in FastAPI."
123    /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
124    pub role: Option<String>,
125
126    /// Custom coding guidelines appended after the core prompt sections.
127    ///
128    /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
129    pub guidelines: Option<String>,
130
131    /// Custom response style that replaces the default "Response Format" section.
132    ///
133    /// When `None`, the default response format is used.
134    pub response_style: Option<String>,
135
136    /// Freeform extra instructions appended at the very end.
137    ///
138    /// This is the backward-compatible slot: setting `system_prompt` in the old API
139    /// maps to this field.
140    pub extra: Option<String>,
141}
142
143/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
144const DEFAULT_ROLE_LINE: &str =
145    "You are A3S Code, an expert AI coding agent. You operate in an agentic loop: you\nthink, use tools, observe results, and keep working until the task is fully complete.";
146
147/// The default response format section.
148const DEFAULT_RESPONSE_FORMAT: &str = "## Response Format
149
150- During work: emit tool calls, no prose.
151- On completion: one short paragraph summarising what changed and why.
152- On genuine blockers: ask a single, specific question.";
153
154impl SystemPromptSlots {
155    /// Build the final system prompt by assembling slots around the core prompt.
156    ///
157    /// The core agentic behavior (Core Behaviour, Tool Usage Strategy, Completion
158    /// Criteria) is always preserved. Users can only customize the edges.
159    pub fn build(&self) -> String {
160        let mut parts: Vec<String> = Vec::new();
161
162        // Normalize line endings: strip \r so string matching works on Windows
163        // where include_str! may produce \r\n if the file has CRLF endings.
164        let system_default = SYSTEM_DEFAULT.replace('\r', "");
165
166        // 1. Role: replace default role line or use default
167        let core = if let Some(ref role) = self.role {
168            let custom_role = format!(
169                "{}. You operate in an agentic loop: you\nthink, use tools, observe results, and keep working until the task is fully complete.",
170                role.trim_end_matches('.')
171            );
172            system_default.replace(DEFAULT_ROLE_LINE, &custom_role)
173        } else {
174            system_default
175        };
176
177        // 2. Core: strip the default response format section if custom one is provided
178        let core = if self.response_style.is_some() {
179            core.replace(DEFAULT_RESPONSE_FORMAT, "")
180                .trim_end()
181                .to_string()
182        } else {
183            core.trim_end().to_string()
184        };
185
186        parts.push(core);
187
188        // 3. Custom response style (replaces default Response Format)
189        if let Some(ref style) = self.response_style {
190            parts.push(format!("## Response Format\n\n{}", style));
191        }
192
193        // 4. Guidelines
194        if let Some(ref guidelines) = self.guidelines {
195            parts.push(format!("## Guidelines\n\n{}", guidelines));
196        }
197
198        // 5. Extra (freeform, backward-compatible with old system_prompt)
199        if let Some(ref extra) = self.extra {
200            parts.push(extra.clone());
201        }
202
203        parts.join("\n\n")
204    }
205
206    /// Create slots from a legacy full system prompt string.
207    ///
208    /// For backward compatibility: the entire string is placed in the `extra` slot,
209    /// and the default core prompt is still prepended.
210    pub fn from_legacy(prompt: String) -> Self {
211        Self {
212            extra: Some(prompt),
213            ..Default::default()
214        }
215    }
216
217    /// Returns true if all slots are empty (use pure default prompt).
218    pub fn is_empty(&self) -> bool {
219        self.role.is_none()
220            && self.guidelines.is_none()
221            && self.response_style.is_none()
222            && self.extra.is_none()
223    }
224}
225
226// ============================================================================
227// Helper Functions
228// ============================================================================
229
230/// Render a template by replacing `{key}` placeholders with values
231pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
232    let mut result = template.to_string();
233    for (key, value) in vars {
234        result = result.replace(&format!("{{{}}}", key), value);
235    }
236    result
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_all_prompts_loaded() {
245        // Verify all prompts are non-empty at compile time
246        assert!(!SYSTEM_DEFAULT.is_empty());
247        assert!(!CONTINUATION.is_empty());
248        assert!(!SUBAGENT_EXPLORE.is_empty());
249        assert!(!SUBAGENT_PLAN.is_empty());
250        assert!(!SUBAGENT_TITLE.is_empty());
251        assert!(!SUBAGENT_SUMMARY.is_empty());
252        assert!(!CONTEXT_COMPACT.is_empty());
253        assert!(!TITLE_GENERATE.is_empty());
254        assert!(!LLM_PLAN_SYSTEM.is_empty());
255        assert!(!LLM_GOAL_EXTRACT_SYSTEM.is_empty());
256        assert!(!LLM_GOAL_CHECK_SYSTEM.is_empty());
257    }
258
259    #[test]
260    fn test_render_template() {
261        let result = render(
262            PLAN_EXECUTE_GOAL,
263            &[("goal", "Build app"), ("steps", "1. Init")],
264        );
265        assert!(result.contains("Build app"));
266        assert!(!result.contains("{goal}"));
267    }
268
269    #[test]
270    fn test_render_multiple_placeholders() {
271        let template = "Goal: {goal}\nCriteria: {criteria}\nState: {current_state}";
272        let result = render(
273            template,
274            &[
275                ("goal", "Build a REST API"),
276                ("criteria", "- Endpoint works\n- Tests pass"),
277                ("current_state", "API is deployed"),
278            ],
279        );
280        assert!(result.contains("Build a REST API"));
281        assert!(result.contains("Endpoint works"));
282        assert!(result.contains("API is deployed"));
283    }
284
285    #[test]
286    fn test_subagent_prompts_contain_guidelines() {
287        assert!(SUBAGENT_EXPLORE.contains("Guidelines"));
288        assert!(SUBAGENT_EXPLORE.contains("read-only"));
289        assert!(SUBAGENT_PLAN.contains("Guidelines"));
290        assert!(SUBAGENT_PLAN.contains("read-only"));
291    }
292
293    #[test]
294    fn test_context_summary_prefix() {
295        assert!(CONTEXT_SUMMARY_PREFIX.contains("Context Summary"));
296    }
297
298    // ── SystemPromptSlots tests ──
299
300    #[test]
301    fn test_slots_default_builds_system_default() {
302        let slots = SystemPromptSlots::default();
303        let built = slots.build();
304        assert!(built.contains("Core Behaviour"));
305        assert!(built.contains("Tool Usage Strategy"));
306        assert!(built.contains("Completion Criteria"));
307        assert!(built.contains("Response Format"));
308        assert!(built.contains("A3S Code"));
309    }
310
311    #[test]
312    fn test_slots_custom_role_replaces_default() {
313        let slots = SystemPromptSlots {
314            role: Some("You are a senior Python developer".to_string()),
315            ..Default::default()
316        };
317        let built = slots.build();
318        assert!(built.contains("You are a senior Python developer"));
319        assert!(!built.contains("You are A3S Code"));
320        // Core sections still present
321        assert!(built.contains("Core Behaviour"));
322        assert!(built.contains("Tool Usage Strategy"));
323    }
324
325    #[test]
326    fn test_slots_custom_guidelines_appended() {
327        let slots = SystemPromptSlots {
328            guidelines: Some("Always use type hints. Follow PEP 8.".to_string()),
329            ..Default::default()
330        };
331        let built = slots.build();
332        assert!(built.contains("## Guidelines"));
333        assert!(built.contains("Always use type hints"));
334        assert!(built.contains("Core Behaviour"));
335    }
336
337    #[test]
338    fn test_slots_custom_response_style_replaces_default() {
339        let slots = SystemPromptSlots {
340            response_style: Some("Be concise. Use bullet points.".to_string()),
341            ..Default::default()
342        };
343        let built = slots.build();
344        assert!(built.contains("Be concise. Use bullet points."));
345        // Default response format content should be gone
346        assert!(!built.contains("emit tool calls, no prose"));
347        // But core is still there
348        assert!(built.contains("Core Behaviour"));
349    }
350
351    #[test]
352    fn test_slots_extra_appended() {
353        let slots = SystemPromptSlots {
354            extra: Some("Remember: always write tests first.".to_string()),
355            ..Default::default()
356        };
357        let built = slots.build();
358        assert!(built.contains("Remember: always write tests first."));
359        assert!(built.contains("Core Behaviour"));
360    }
361
362    #[test]
363    fn test_slots_from_legacy() {
364        let slots = SystemPromptSlots::from_legacy("You are a helpful assistant.".to_string());
365        let built = slots.build();
366        // Legacy prompt goes into extra, core is still present
367        assert!(built.contains("You are a helpful assistant."));
368        assert!(built.contains("Core Behaviour"));
369        assert!(built.contains("Tool Usage Strategy"));
370    }
371
372    #[test]
373    fn test_slots_all_slots_combined() {
374        let slots = SystemPromptSlots {
375            role: Some("You are a Rust expert".to_string()),
376            guidelines: Some("Use clippy. No unwrap.".to_string()),
377            response_style: Some("Short answers only.".to_string()),
378            extra: Some("Project uses tokio.".to_string()),
379        };
380        let built = slots.build();
381        assert!(built.contains("You are a Rust expert"));
382        assert!(built.contains("Core Behaviour"));
383        assert!(built.contains("## Guidelines"));
384        assert!(built.contains("Use clippy"));
385        assert!(built.contains("Short answers only"));
386        assert!(built.contains("Project uses tokio"));
387        // Default response format replaced
388        assert!(!built.contains("emit tool calls, no prose"));
389    }
390
391    #[test]
392    fn test_slots_is_empty() {
393        assert!(SystemPromptSlots::default().is_empty());
394        assert!(!SystemPromptSlots {
395            role: Some("test".to_string()),
396            ..Default::default()
397        }
398        .is_empty());
399    }
400}