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 system_default = SYSTEM_DEFAULT.replace('\r', "");
165
166 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 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 if let Some(ref style) = self.response_style {
190 parts.push(format!("## Response Format\n\n{}", style));
191 }
192
193 if let Some(ref guidelines) = self.guidelines {
195 parts.push(format!("## Guidelines\n\n{}", guidelines));
196 }
197
198 if let Some(ref extra) = self.extra {
200 parts.push(extra.clone());
201 }
202
203 parts.join("\n\n")
204 }
205
206 pub fn from_legacy(prompt: String) -> Self {
211 Self {
212 extra: Some(prompt),
213 ..Default::default()
214 }
215 }
216
217 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
226pub 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 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 #[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 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 assert!(!built.contains("emit tool calls, no prose"));
347 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 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 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}