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