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