1pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/system_default.md");
28
29pub const CONTINUATION: &str = include_str!("../prompts/continuation.md");
32
33pub const SUBAGENT_EXPLORE: &str = include_str!("../prompts/subagent_explore.md");
39
40pub const SUBAGENT_PLAN: &str = include_str!("../prompts/subagent_plan.md");
42
43pub const SUBAGENT_TITLE: &str = include_str!("../prompts/subagent_title.md");
45
46pub const SUBAGENT_SUMMARY: &str = include_str!("../prompts/subagent_summary.md");
48
49pub const CONTEXT_COMPACT: &str = include_str!("../prompts/context_compact.md");
55
56pub const CONTEXT_SUMMARY_PREFIX: &str =
58 "[Context Summary: The following is a summary of earlier conversation]\n\n";
59
60#[allow(dead_code)]
66pub const TITLE_GENERATE: &str = include_str!("../prompts/title_generate.md");
67
68pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/llm_plan_system.md");
74
75pub const LLM_GOAL_EXTRACT_SYSTEM: &str = include_str!("../prompts/llm_goal_extract_system.md");
77
78pub const LLM_GOAL_CHECK_SYSTEM: &str = include_str!("../prompts/llm_goal_check_system.md");
80
81pub const PLAN_EXECUTE_GOAL: &str =
87 "Goal: {goal}\n\nExecute the following plan step by step:\n{steps}";
88
89pub const PLAN_EXECUTE_STEP: &str = "Execute step {step_num}: {description}";
91
92pub const PLAN_FALLBACK_STEP: &str = "Execute step {step_num} of the plan";
94
95pub const PLAN_PARALLEL_RESULTS: &str =
97 "The following steps were executed in parallel:\n{results}\n\nContinue with the next steps.";
98
99#[derive(Debug, Clone, Default)]
119pub struct SystemPromptSlots {
120 pub role: Option<String>,
125
126 pub guidelines: Option<String>,
130
131 pub response_style: Option<String>,
135
136 pub extra: Option<String>,
141}
142
143const 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
147const 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 pub fn build(&self) -> String {
160 let mut parts: Vec<String> = Vec::new();
161
162 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 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 if let Some(ref style) = self.response_style {
186 parts.push(format!("## Response Format\n\n{}", style));
187 }
188
189 if let Some(ref guidelines) = self.guidelines {
191 parts.push(format!("## Guidelines\n\n{}", guidelines));
192 }
193
194 if let Some(ref extra) = self.extra {
196 parts.push(extra.clone());
197 }
198
199 parts.join("\n\n")
200 }
201
202 pub fn from_legacy(prompt: String) -> Self {
207 Self {
208 extra: Some(prompt),
209 ..Default::default()
210 }
211 }
212
213 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
222pub 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 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 #[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 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 assert!(!built.contains("emit tool calls, no prose"));
343 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 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 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}