diff --git a/server/src/app.rs b/server/src/app.rs index 5d4749f..f53fdc3 100644 --- a/server/src/app.rs +++ b/server/src/app.rs @@ -35,7 +35,10 @@ impl App { .set_port(store.port_settings().await?.service_port); } let assets = PromptAssets::embedded()?; - let compiler = PromptCompiler::new(assets); + let compiler = PromptCompiler::with_compaction_prompt_path( + assets, + crate::config::compaction_prompt_path()?, + ); let provider = std::sync::Arc::new(ProviderRouter::new( store.clone(), config.provider_request_timeout, diff --git a/server/src/config.rs b/server/src/config.rs index ba82881..c3afeae 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -9,6 +9,7 @@ const DATA_DIR_NAME: &str = ".cursor-byok-v3"; const DATABASE_FILE_NAME: &str = "cursor-byok.db"; const V0049_DATA_DIR_NAME: &str = ".cursor-local-assistant-v2"; const V0049_CONFIG_FILE_NAME: &str = "config.yaml"; +const COMPACTION_PROMPT_PATH: &str = "prompts/compaction.md"; pub fn managed_data_dir() -> Result { let home_dir = dirs::home_dir() @@ -28,6 +29,26 @@ pub fn v0049_config_path() -> Result { .join(V0049_CONFIG_FILE_NAME)) } +pub fn compaction_prompt_path() -> Result { + Ok(managed_data_dir()?.join(COMPACTION_PROMPT_PATH)) +} + +pub fn compaction_prompt_override() -> Result> { + compaction_prompt_override_at(&compaction_prompt_path()?) +} + +pub(crate) fn compaction_prompt_override_at(path: &std::path::Path) -> Result> { + match fs::read_to_string(path) { + Ok(prompt) if prompt.trim().is_empty() => Ok(None), + Ok(prompt) => Ok(Some(prompt)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(Error::Config(format!( + "cannot read compaction prompt at {}: {error}", + path.display() + ))), + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum ProviderKind { OpenAiChat, @@ -156,6 +177,21 @@ fn database_url_for_dir(data_dir: &std::path::Path) -> Result { mod tests { use super::*; + #[test] + fn compaction_prompt_override_is_optional_and_reloaded() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("compaction.md"); + + assert_eq!(compaction_prompt_override_at(&path).unwrap(), None); + fs::write(&path, " \n").unwrap(); + assert_eq!(compaction_prompt_override_at(&path).unwrap(), None); + fs::write(&path, "custom prompt").unwrap(); + assert_eq!( + compaction_prompt_override_at(&path).unwrap().as_deref(), + Some("custom prompt") + ); + } + #[tokio::test] async fn managed_database_supports_home_paths_with_spaces() { let directory = tempfile::tempdir().unwrap(); diff --git a/server/src/cursor/prompting/compiler.rs b/server/src/cursor/prompting/compiler.rs index ec4e7b9..96145a1 100644 --- a/server/src/cursor/prompting/compiler.rs +++ b/server/src/cursor/prompting/compiler.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::PathBuf}; use crate::{ model::{ModelSpec, PromptSpec, ToolDefinition}, @@ -10,11 +10,22 @@ use super::{assets::runtime_expression, Mode, PromptAssets}; #[derive(Clone)] pub struct PromptCompiler { assets: PromptAssets, + compaction_prompt_path: Option, } impl PromptCompiler { pub fn new(assets: PromptAssets) -> Self { - Self { assets } + Self { + assets, + compaction_prompt_path: None, + } + } + + pub fn with_compaction_prompt_path(assets: PromptAssets, path: PathBuf) -> Self { + Self { + assets, + compaction_prompt_path: Some(path), + } } pub fn runtime_message(&self, mode: Mode, values: &BTreeMap<&str, String>) -> Result { @@ -39,12 +50,13 @@ impl PromptCompiler { .display_name .as_deref() .unwrap_or(model.model_id.as_str()); + let prompt = match (mode, &self.compaction_prompt_path) { + (Mode::Compaction, Some(path)) => crate::config::compaction_prompt_override_at(path)? + .unwrap_or_else(|| self.assets.mode(mode).prompt.clone()), + _ => self.assets.mode(mode).prompt.clone(), + }; Ok(PromptSpec { - instructions: self - .assets - .mode(mode) - .prompt - .replace("{{FAKE_MODEL_NAME}}", fake_model_name), + instructions: prompt.replace("{{FAKE_MODEL_NAME}}", fake_model_name), tools, }) } diff --git a/server/src/run/engine.rs b/server/src/run/engine.rs index 296309f..73deef1 100644 --- a/server/src/run/engine.rs +++ b/server/src/run/engine.rs @@ -541,6 +541,9 @@ impl RunEngine { model.max_output_tokens = Some(COMPACTION_OUTPUT_TOKENS); model.reasoning.enabled = false; model.reasoning.effort = None; + let instructions = crate::config::compaction_prompt_override() + .map_err(|error| RunOutcome::Failed(error.into()))? + .unwrap_or_else(|| COMPACTION_INSTRUCTIONS.into()); let invocation = crate::model::ModelInvocation { call_id: format!("{}:{provider_call_index}", prepared.run_id), run_id: prepared.run_id.to_string(), @@ -548,7 +551,7 @@ impl RunEngine { provider_call_index, request: crate::model::ModelRequest { prompt: crate::model::PromptSpec { - instructions: COMPACTION_INSTRUCTIONS.into(), + instructions, tools: Vec::new(), }, model,