claude_agent/output_style/
generator.rs1use std::path::PathBuf;
7
8use super::{
9 ChainOutputStyleProvider, InMemoryOutputStyleProvider, OutputStyle, builtin_styles,
10 default_style, file_output_style_provider,
11};
12use crate::client::DEFAULT_MODEL;
13use crate::common::Provider;
14use crate::common::SourceType;
15use crate::prompts::{
16 base::{BASE_SYSTEM_PROMPT, TOOL_USAGE_POLICY},
17 coding,
18 environment::{current_platform, environment_block, is_git_repository, os_version},
19 identity::CLI_IDENTITY,
20};
21
22#[derive(Debug, Clone)]
48pub struct SystemPromptGenerator {
49 style: OutputStyle,
50 working_dir: Option<PathBuf>,
51 model_name: String,
52 model_id: String,
53 require_cli_identity: bool,
54}
55
56impl Default for SystemPromptGenerator {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl SystemPromptGenerator {
63 pub fn new() -> Self {
66 Self {
67 style: default_style(),
68 working_dir: None,
69 model_name: "Claude".to_string(),
70 model_id: DEFAULT_MODEL.to_string(),
71 require_cli_identity: false,
72 }
73 }
74
75 pub fn with_cli_identity() -> Self {
78 Self {
79 style: default_style(),
80 working_dir: None,
81 model_name: "Claude".to_string(),
82 model_id: DEFAULT_MODEL.to_string(),
83 require_cli_identity: true,
84 }
85 }
86
87 pub fn require_cli_identity(mut self, required: bool) -> Self {
90 self.require_cli_identity = required;
91 self
92 }
93
94 pub fn with_style(mut self, style: OutputStyle) -> Self {
96 self.style = style;
97 self
98 }
99
100 pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
102 self.working_dir = Some(dir.into());
103 self
104 }
105
106 pub fn with_model(mut self, model_id: impl Into<String>) -> Self {
108 let id = model_id.into();
109 self.model_name = derive_model_name(&id);
110 self.model_id = id;
111 self
112 }
113
114 pub fn with_model_name(mut self, name: impl Into<String>) -> Self {
116 self.model_name = name.into();
117 self
118 }
119
120 pub async fn with_style_name(mut self, name: &str) -> crate::Result<Self> {
127 let builtins = InMemoryOutputStyleProvider::new()
128 .with_items(builtin_styles())
129 .with_priority(0)
130 .with_source_type(SourceType::Builtin);
131
132 let mut chain = ChainOutputStyleProvider::new().with(builtins);
133
134 if let Some(ref working_dir) = self.working_dir {
135 let project = file_output_style_provider()
136 .with_project_path(working_dir)
137 .with_priority(20)
138 .with_source_type(SourceType::Project);
139 chain = chain.with(project);
140 }
141
142 let user = file_output_style_provider()
143 .with_user_path()
144 .with_priority(10)
145 .with_source_type(SourceType::User);
146 chain = chain.with(user);
147
148 if let Some(style) = chain.get(name).await? {
149 self.style = style;
150 Ok(self)
151 } else {
152 Err(crate::Error::Config(format!(
153 "Output style '{}' not found",
154 name
155 )))
156 }
157 }
158
159 pub fn generate(&self) -> String {
170 let mut parts = Vec::new();
171
172 if self.require_cli_identity {
174 parts.push(CLI_IDENTITY.to_string());
175 }
176
177 parts.push(BASE_SYSTEM_PROMPT.to_string());
179
180 parts.push(TOOL_USAGE_POLICY.to_string());
182
183 if self.style.keep_coding_instructions {
185 parts.push(coding::coding_instructions(&self.model_name));
186 }
187
188 if !self.style.prompt.is_empty() {
190 parts.push(self.style.prompt.clone());
191 }
192
193 let is_git = is_git_repository(self.working_dir.as_deref());
195 let platform = current_platform();
196 let os_ver = os_version();
197
198 parts.push(environment_block(
199 self.working_dir.as_deref(),
200 is_git,
201 platform,
202 &os_ver,
203 &self.model_name,
204 &self.model_id,
205 ));
206
207 parts.join("\n\n")
208 }
209
210 pub fn generate_with_context(&self, additional_context: &str) -> String {
214 let mut prompt = self.generate();
215 if !additional_context.is_empty() {
216 prompt.push_str("\n\n");
217 prompt.push_str(additional_context);
218 }
219 prompt
220 }
221
222 pub fn style(&self) -> &OutputStyle {
224 &self.style
225 }
226
227 pub fn has_coding_instructions(&self) -> bool {
229 self.style.keep_coding_instructions
230 }
231}
232
233fn derive_model_name(model_id: &str) -> String {
235 if model_id.contains("opus-4-5") || model_id.contains("opus-4.5") {
240 "Claude Opus 4.5".to_string()
241 } else if model_id.contains("opus-4") {
242 "Claude Opus 4".to_string()
243 } else if model_id.contains("sonnet-4-5") || model_id.contains("sonnet-4.5") {
244 "Claude Sonnet 4.5".to_string()
245 } else if model_id.contains("sonnet-4") {
246 "Claude Sonnet 4".to_string()
247 } else if model_id.contains("haiku-4-5") || model_id.contains("haiku-4.5") {
248 "Claude Haiku 4.5".to_string()
249 } else if model_id.contains("haiku-4") {
250 "Claude Haiku 4".to_string()
251 } else if model_id.contains("3.5") || model_id.contains("3-5") {
252 if model_id.contains("sonnet") {
253 "Claude 3.5 Sonnet".to_string()
254 } else if model_id.contains("haiku") {
255 "Claude 3.5 Haiku".to_string()
256 } else if model_id.contains("opus") {
257 "Claude 3.5 Opus".to_string()
258 } else {
259 "Claude 3.5".to_string()
260 }
261 } else {
262 "Claude".to_string()
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::output_style::SourceType;
270
271 #[test]
272 fn test_generator_default_no_cli_identity() {
273 let prompt = SystemPromptGenerator::new().generate();
274
275 assert!(!prompt.starts_with(CLI_IDENTITY));
277 assert!(prompt.contains("Doing tasks")); assert!(prompt.contains("<env>")); }
280
281 #[test]
282 fn test_generator_with_cli_identity() {
283 let prompt = SystemPromptGenerator::with_cli_identity().generate();
284
285 assert!(prompt.starts_with(CLI_IDENTITY));
287 assert!(prompt.contains("Doing tasks")); assert!(prompt.contains("<env>")); }
290
291 #[test]
292 fn test_generator_with_custom_style_keep_coding() {
293 let style = OutputStyle::new("test", "Test style", "Custom instructions here")
294 .with_source_type(SourceType::User)
295 .with_keep_coding_instructions(true);
296
297 let prompt = SystemPromptGenerator::with_cli_identity()
298 .with_style(style)
299 .generate();
300
301 assert!(prompt.starts_with(CLI_IDENTITY));
302 assert!(prompt.contains("Doing tasks")); assert!(prompt.contains("Custom instructions here")); assert!(prompt.contains("<env>")); }
306
307 #[test]
308 fn test_generator_with_custom_style_no_coding() {
309 let style = OutputStyle::new("concise", "Be concise", "Keep responses short.")
310 .with_source_type(SourceType::User)
311 .with_keep_coding_instructions(false);
312
313 let prompt = SystemPromptGenerator::with_cli_identity()
314 .with_style(style)
315 .generate();
316
317 assert!(prompt.starts_with(CLI_IDENTITY)); assert!(!prompt.contains("Doing tasks")); assert!(prompt.contains("Keep responses short.")); assert!(prompt.contains("<env>")); }
322
323 #[test]
324 fn test_generator_with_working_dir() {
325 let prompt = SystemPromptGenerator::new()
326 .with_working_dir("/test/project")
327 .generate();
328
329 assert!(prompt.contains("/test/project"));
330 }
331
332 #[test]
333 fn test_generator_with_model() {
334 let prompt = SystemPromptGenerator::new()
335 .with_model("claude-opus-4-5-20251101")
336 .generate();
337
338 assert!(prompt.contains("claude-opus-4-5-20251101"));
339 assert!(prompt.contains("Claude Opus 4.5"));
340 }
341
342 #[test]
343 fn test_derive_model_name() {
344 assert_eq!(
345 derive_model_name("claude-opus-4-5-20251101"),
346 "Claude Opus 4.5"
347 );
348 assert_eq!(
349 derive_model_name("claude-sonnet-4-20250514"),
350 "Claude Sonnet 4"
351 );
352 assert_eq!(
353 derive_model_name("claude-haiku-4-5-20251001"),
354 "Claude Haiku 4.5"
355 );
356 assert_eq!(
357 derive_model_name("claude-3-5-sonnet-20241022"),
358 "Claude 3.5 Sonnet"
359 );
360 }
361
362 #[test]
363 fn test_generator_with_context() {
364 let prompt = SystemPromptGenerator::new()
365 .generate_with_context("# Dynamic Rules\nSome dynamic content");
366
367 assert!(prompt.contains("# Dynamic Rules"));
368 assert!(prompt.contains("Some dynamic content"));
369 }
370
371 #[test]
372 fn test_has_coding_instructions() {
373 let generator = SystemPromptGenerator::new();
374 assert!(generator.has_coding_instructions());
375
376 let style = OutputStyle::new("no-coding", "", "").with_keep_coding_instructions(false);
377 let generator = SystemPromptGenerator::new().with_style(style);
378 assert!(!generator.has_coding_instructions());
379 }
380
381 #[test]
382 fn test_cli_identity_cannot_be_replaced_by_custom_prompt() {
383 let style = OutputStyle::new(
386 "custom",
387 "Custom identity",
388 "I am a different assistant.", )
390 .with_keep_coding_instructions(false);
391
392 let prompt = SystemPromptGenerator::with_cli_identity()
393 .with_style(style)
394 .generate();
395
396 assert!(prompt.starts_with(CLI_IDENTITY));
398 assert!(prompt.contains("I am a different assistant."));
399 }
400}