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        // 1. Role: replace default role line or use default
163        let core = if let Some(ref role) = self.role {
164            let custom_role = format!(
165                "{}. You operate in an agentic loop: you\nthink, use tools, observe results, and keep working until the task is fully complete.",
166                role.trim_end_matches('.')
167            );
168            SYSTEM_DEFAULT.replace(DEFAULT_ROLE_LINE, &custom_role)
169        } else {
170            SYSTEM_DEFAULT.to_string()
171        };
172
173        // 2. Core: strip the default response format section if custom one is provided
174        let core = if self.response_style.is_some() {
175            core.replace(DEFAULT_RESPONSE_FORMAT, "")
176                .trim_end()
177                .to_string()
178        } else {
179            core.trim_end().to_string()
180        };
181
182        parts.push(core);
183
184        // 3. Custom response style (replaces default Response Format)
185        if let Some(ref style) = self.response_style {
186            parts.push(format!("## Response Format\n\n{}", style));
187        }
188
189        // 4. Guidelines
190        if let Some(ref guidelines) = self.guidelines {
191            parts.push(format!("## Guidelines\n\n{}", guidelines));
192        }
193
194        // 5. Extra (freeform, backward-compatible with old system_prompt)
195        if let Some(ref extra) = self.extra {
196            parts.push(extra.clone());
197        }
198
199        parts.join("\n\n")
200    }
201
202    /// Create slots from a legacy full system prompt string.
203    ///
204    /// For backward compatibility: the entire string is placed in the `extra` slot,
205    /// and the default core prompt is still prepended.
206    pub fn from_legacy(prompt: String) -> Self {
207        Self {
208            extra: Some(prompt),
209            ..Default::default()
210        }
211    }
212
213    /// Returns true if all slots are empty (use pure default prompt).
214    pub fn is_empty(&self) -> bool {
215        self.role.is_none()
216            && self.guidelines.is_none()
217            && self.response_style.is_none()
218            && self.extra.is_none()
219    }
220}
221
222// ============================================================================
223// Helper Functions
224// ============================================================================
225
226/// Render a template by replacing `{key}` placeholders with values
227pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
228    let mut result = template.to_string();
229    for (key, value) in vars {
230        result = result.replace(&format!("{{{}}}", key), value);
231    }
232    result
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_all_prompts_loaded() {
241        // Verify all prompts are non-empty at compile time
242        assert!(!SYSTEM_DEFAULT.is_empty());
243        assert!(!CONTINUATION.is_empty());
244        assert!(!SUBAGENT_EXPLORE.is_empty());
245        assert!(!SUBAGENT_PLAN.is_empty());
246        assert!(!SUBAGENT_TITLE.is_empty());
247        assert!(!SUBAGENT_SUMMARY.is_empty());
248        assert!(!CONTEXT_COMPACT.is_empty());
249        assert!(!TITLE_GENERATE.is_empty());
250        assert!(!LLM_PLAN_SYSTEM.is_empty());
251        assert!(!LLM_GOAL_EXTRACT_SYSTEM.is_empty());
252        assert!(!LLM_GOAL_CHECK_SYSTEM.is_empty());
253    }
254
255    #[test]
256    fn test_render_template() {
257        let result = render(
258            PLAN_EXECUTE_GOAL,
259            &[("goal", "Build app"), ("steps", "1. Init")],
260        );
261        assert!(result.contains("Build app"));
262        assert!(!result.contains("{goal}"));
263    }
264
265    #[test]
266    fn test_render_multiple_placeholders() {
267        let template = "Goal: {goal}\nCriteria: {criteria}\nState: {current_state}";
268        let result = render(
269            template,
270            &[
271                ("goal", "Build a REST API"),
272                ("criteria", "- Endpoint works\n- Tests pass"),
273                ("current_state", "API is deployed"),
274            ],
275        );
276        assert!(result.contains("Build a REST API"));
277        assert!(result.contains("Endpoint works"));
278        assert!(result.contains("API is deployed"));
279    }
280
281    #[test]
282    fn test_subagent_prompts_contain_guidelines() {
283        assert!(SUBAGENT_EXPLORE.contains("Guidelines"));
284        assert!(SUBAGENT_EXPLORE.contains("read-only"));
285        assert!(SUBAGENT_PLAN.contains("Guidelines"));
286        assert!(SUBAGENT_PLAN.contains("read-only"));
287    }
288
289    #[test]
290    fn test_context_summary_prefix() {
291        assert!(CONTEXT_SUMMARY_PREFIX.contains("Context Summary"));
292    }
293
294    // ── SystemPromptSlots tests ──
295
296    #[test]
297    fn test_slots_default_builds_system_default() {
298        let slots = SystemPromptSlots::default();
299        let built = slots.build();
300        assert!(built.contains("Core Behaviour"));
301        assert!(built.contains("Tool Usage Strategy"));
302        assert!(built.contains("Completion Criteria"));
303        assert!(built.contains("Response Format"));
304        assert!(built.contains("A3S Code"));
305    }
306
307    #[test]
308    fn test_slots_custom_role_replaces_default() {
309        let slots = SystemPromptSlots {
310            role: Some("You are a senior Python developer".to_string()),
311            ..Default::default()
312        };
313        let built = slots.build();
314        assert!(built.contains("You are a senior Python developer"));
315        assert!(!built.contains("You are A3S Code"));
316        // Core sections still present
317        assert!(built.contains("Core Behaviour"));
318        assert!(built.contains("Tool Usage Strategy"));
319    }
320
321    #[test]
322    fn test_slots_custom_guidelines_appended() {
323        let slots = SystemPromptSlots {
324            guidelines: Some("Always use type hints. Follow PEP 8.".to_string()),
325            ..Default::default()
326        };
327        let built = slots.build();
328        assert!(built.contains("## Guidelines"));
329        assert!(built.contains("Always use type hints"));
330        assert!(built.contains("Core Behaviour"));
331    }
332
333    #[test]
334    fn test_slots_custom_response_style_replaces_default() {
335        let slots = SystemPromptSlots {
336            response_style: Some("Be concise. Use bullet points.".to_string()),
337            ..Default::default()
338        };
339        let built = slots.build();
340        assert!(built.contains("Be concise. Use bullet points."));
341        // Default response format content should be gone
342        assert!(!built.contains("emit tool calls, no prose"));
343        // But core is still there
344        assert!(built.contains("Core Behaviour"));
345    }
346
347    #[test]
348    fn test_slots_extra_appended() {
349        let slots = SystemPromptSlots {
350            extra: Some("Remember: always write tests first.".to_string()),
351            ..Default::default()
352        };
353        let built = slots.build();
354        assert!(built.contains("Remember: always write tests first."));
355        assert!(built.contains("Core Behaviour"));
356    }
357
358    #[test]
359    fn test_slots_from_legacy() {
360        let slots = SystemPromptSlots::from_legacy("You are a helpful assistant.".to_string());
361        let built = slots.build();
362        // Legacy prompt goes into extra, core is still present
363        assert!(built.contains("You are a helpful assistant."));
364        assert!(built.contains("Core Behaviour"));
365        assert!(built.contains("Tool Usage Strategy"));
366    }
367
368    #[test]
369    fn test_slots_all_slots_combined() {
370        let slots = SystemPromptSlots {
371            role: Some("You are a Rust expert".to_string()),
372            guidelines: Some("Use clippy. No unwrap.".to_string()),
373            response_style: Some("Short answers only.".to_string()),
374            extra: Some("Project uses tokio.".to_string()),
375        };
376        let built = slots.build();
377        assert!(built.contains("You are a Rust expert"));
378        assert!(built.contains("Core Behaviour"));
379        assert!(built.contains("## Guidelines"));
380        assert!(built.contains("Use clippy"));
381        assert!(built.contains("Short answers only"));
382        assert!(built.contains("Project uses tokio"));
383        // Default response format replaced
384        assert!(!built.contains("emit tool calls, no prose"));
385    }
386
387    #[test]
388    fn test_slots_is_empty() {
389        assert!(SystemPromptSlots::default().is_empty());
390        assert!(!SystemPromptSlots {
391            role: Some("test".to_string()),
392            ..Default::default()
393        }
394        .is_empty());
395    }
396}