Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/crates/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ pub use policy_engine::{
pub use prompt::{
load_system_prompt, load_system_prompt_with_context, prepend_bullets, ContextFile,
ModelFamilyIdentity, ProjectContext, PromptBuildError, SystemPromptBuilder,
FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
};
pub use recovery_recipes::{
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState,
Expand Down
67 changes: 55 additions & 12 deletions rust/crates/runtime/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ impl From<ConfigError> for PromptBuildError {

/// Marker separating static prompt scaffolding from dynamic runtime context.
pub const SYSTEM_PROMPT_DYNAMIC_BOUNDARY: &str = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";
/// Human-readable default frontier model name embedded into generated prompts.
pub const FRONTIER_MODEL_NAME: &str = "Claude Opus 4.6";
const MAX_INSTRUCTION_FILE_CHARS: usize = 4_000;
const MAX_TOTAL_INSTRUCTION_CHARS: usize = 12_000;
const MAX_GIT_DIFF_CHARS: usize = 50_000;
Expand All @@ -56,7 +54,7 @@ impl ModelFamilyIdentity {
#[must_use]
pub const fn family_label(self) -> &'static str {
match self {
Self::Claude => FRONTIER_MODEL_NAME,
Self::Claude => "Claude",
Self::Generic => "an AI assistant",
}
}
Expand Down Expand Up @@ -158,6 +156,7 @@ pub struct SystemPromptBuilder {
os_name: Option<String>,
os_version: Option<String>,
model_family: Option<ModelFamilyIdentity>,
model: Option<String>,
append_sections: Vec<String>,
project_context: Option<ProjectContext>,
config: Option<RuntimeConfig>,
Expand Down Expand Up @@ -191,6 +190,12 @@ impl SystemPromptBuilder {
self
}

#[must_use]
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}

#[must_use]
pub fn with_project_context(mut self, project_context: ProjectContext) -> Self {
self.project_context = Some(project_context);
Expand Down Expand Up @@ -294,17 +299,21 @@ impl SystemPromptBuilder {
|context| context.current_date.clone(),
);
let identity = self.model_family.unwrap_or_default();
let mut lines = vec!["# Environment context".to_string()];
lines.extend(prepend_bullets(vec![
format!("Model family: {}", identity.family_label()),
let mut items = vec![format!("Model family: {}", identity.family_label())];
if let Some(model) = &self.model {
items.push(format!("Model: {model}"));
}
items.extend([
format!("Working directory: {cwd}"),
format!("Date: {date}"),
format!(
"Platform: {} {}",
self.os_name.as_deref().unwrap_or("unknown"),
self.os_version.as_deref().unwrap_or("unknown")
),
]));
]);
let mut lines = vec!["# Environment context".to_string()];
lines.extend(prepend_bullets(items));
lines.join("\n")
}
}
Expand Down Expand Up @@ -670,11 +679,18 @@ pub fn load_system_prompt(
current_date: impl Into<String>,
os_name: impl Into<String>,
os_version: impl Into<String>,
model: impl Into<String>,
model_family: ModelFamilyIdentity,
) -> Result<Vec<String>, PromptBuildError> {
let cwd = cwd.into();
let (sections, _) =
load_system_prompt_with_context(cwd, current_date, os_name, os_version, model_family)?;
let (sections, _) = load_system_prompt_with_context(
cwd,
current_date,
os_name,
os_version,
model,
model_family,
)?;
Ok(sections)
}

Expand All @@ -684,6 +700,7 @@ pub fn load_system_prompt_with_context(
current_date: impl Into<String>,
os_name: impl Into<String>,
os_version: impl Into<String>,
model: impl Into<String>,
model_family: ModelFamilyIdentity,
) -> Result<(Vec<String>, ProjectContext), PromptBuildError> {
let cwd = cwd.into();
Expand All @@ -693,6 +710,7 @@ pub fn load_system_prompt_with_context(
let memory_enabled = config.feature_config().auto_memory_enabled();
let mut builder = SystemPromptBuilder::new()
.with_os(os_name, os_version)
.with_model(model)
.with_model_family(model_family)
.with_project_context(project_context.clone())
.with_runtime_config(config);
Expand Down Expand Up @@ -1255,6 +1273,7 @@ mod tests {
"2026-03-31",
"linux",
"6.8",
"claude-opus-4-6",
ModelFamilyIdentity::Claude,
)
.expect("system prompt should load")
Expand Down Expand Up @@ -1304,6 +1323,7 @@ mod tests {
"2026-03-31",
"linux",
"6.8",
"claude-opus-4-6",
ModelFamilyIdentity::Claude,
)
.expect("system prompt should load")
Expand Down Expand Up @@ -1341,7 +1361,30 @@ mod tests {
.render();

// then: the Claude model family label is preserved by default
assert!(prompt.contains("Model family: Claude Opus 4.6"));
assert!(prompt.contains(" - Model family: Claude\n"));
assert!(!prompt.contains("Model:"));
}

#[test]
fn renders_configured_model_instead_of_a_fixed_claude_version() {
// given: a prompt builder for the model the session actually uses
let project_context = ProjectContext {
cwd: PathBuf::from("/tmp/project"),
current_date: "2026-03-31".to_string(),
..ProjectContext::default()
};

// when: rendering the system prompt environment section
let prompt = SystemPromptBuilder::new()
.with_os("linux", "6.8")
.with_model("claude-sonnet-4-6")
.with_model_family(ModelFamilyIdentity::Claude)
.with_project_context(project_context)
.render();

// then: the prompt names that model rather than a hardcoded Opus release
assert!(prompt.contains(" - Model family: Claude\n - Model: claude-sonnet-4-6\n"));
assert!(!prompt.contains("Opus"));
}

#[test]
Expand All @@ -1364,9 +1407,9 @@ mod tests {
.find(|line| line.contains("Model family:"))
.expect("model family line should render");

// then: the model family line is neutral and excludes Claude Opus 4.6
// then: the model family line is neutral and excludes Claude
assert_eq!(model_family_line, " - Model family: an AI assistant");
assert!(!model_family_line.contains("Claude Opus 4.6"));
assert!(!model_family_line.contains("Claude"));
}

#[test]
Expand Down
6 changes: 5 additions & 1 deletion rust/crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5012,6 +5012,7 @@ fn print_system_prompt(
date,
env::consts::OS,
"unknown",
model,
model_family_identity_for(model),
)?;
let (project_root, _) =
Expand Down Expand Up @@ -8749,11 +8750,12 @@ impl LiveCli {
let previous = self.model.clone();
let session = self.runtime.session().clone();
let message_count = session.messages.len();
let system_prompt = build_system_prompt(&model)?;
let runtime = build_runtime(
session,
&self.session.id,
model.clone(),
self.system_prompt.clone(),
system_prompt.clone(),
true,
true,
TerminalTail::new(),
Expand All @@ -8763,6 +8765,7 @@ impl LiveCli {
)?;
self.replace_runtime(runtime)?;
self.model.clone_from(&model);
self.system_prompt = system_prompt;
println!(
"{}",
format_model_switch_report(&previous, &model, message_count)
Expand Down Expand Up @@ -12360,6 +12363,7 @@ fn build_system_prompt(model: &str) -> Result<Vec<String>, Box<dyn std::error::E
DEFAULT_DATE,
env::consts::OS,
"unknown",
model,
model_family_identity_for(model),
)?)
}
Expand Down
4 changes: 3 additions & 1 deletion rust/crates/tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4374,6 +4374,7 @@ fn build_agent_system_prompt(subagent_type: &str, model: &str) -> Result<Vec<Str
DEFAULT_AGENT_SYSTEM_DATE.to_string(),
std::env::consts::OS,
"unknown",
model,
model_family_identity_for(model),
)
.map_err(|error| error.to_string())?;
Expand Down Expand Up @@ -9507,7 +9508,8 @@ mod tests {

// then: the prompt renders a generic model family identity
assert!(prompt.contains("Model family: an AI assistant"));
assert!(!prompt.contains("Model family: Claude Opus 4.6"));
assert!(!prompt.contains("Model family: Claude"));
assert!(prompt.contains("Model: openai/gpt-4.1-mini"));

fs::remove_dir_all(root).expect("cleanup temp workspace");
}
Expand Down
Loading