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
222 changes: 211 additions & 11 deletions crates/agent/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2516,8 +2516,7 @@ impl Thread {
cx: &mut Context<Self>,
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
let model = self
.model()
.cloned()
.compaction_model(cx)
.ok_or_else(|| anyhow!(NoModelConfiguredError))?;

// Flush any pending message and cancel an in-flight turn before we
Expand All @@ -2530,11 +2529,12 @@ impl Thread {
self.advance_prompt_id();
let request = self.build_compaction_request(request_end_ix, &model, cx);
self.current_request_token_usage = TokenUsage::default();
(model, request)
(model.clone(), request)
});

if compaction.is_some() {
self.pending_compaction_telemetry = self.build_compaction_telemetry("manual", cx);
self.pending_compaction_telemetry =
self.build_compaction_telemetry("manual", &model, cx);
}

self.clear_summary();
Expand Down Expand Up @@ -3092,13 +3092,14 @@ impl Thread {
) -> Result<ControlFlow<()>> {
let Some((model, request, insertion_ix)) = this.update(cx, |this, cx| {
let insertion_ix = this.compaction_message_target_ix(cx)?;
let model = this.model().cloned()?;
let model = this.compaction_model(cx)?;
let request = this.build_compaction_request(insertion_ix, &model, cx);
this.current_request_token_usage = TokenUsage::default();
// Preserve telemetry across retries so the retry count keeps
// accumulating rather than resetting on each attempt.
if this.pending_compaction_telemetry.is_none() {
this.pending_compaction_telemetry = this.build_compaction_telemetry("auto", cx);
this.pending_compaction_telemetry =
this.build_compaction_telemetry("auto", &model, cx);
}
Some((model, request, insertion_ix))
})?
Expand Down Expand Up @@ -4318,11 +4319,10 @@ impl Thread {
extend_request_history_until(&self.messages, request_messages, end_ix);
}

/// Captures the data for an `"Agent Compaction Completed"` telemetry event
/// at the moment a compaction starts. Returns `None` if there's no model.
fn build_compaction_telemetry(
&self,
trigger: &'static str,
compaction_model: &Arc<dyn LanguageModel>,
cx: &App,
) -> Option<CompactionTelemetry> {
let model = self.model()?;
Expand All @@ -4338,7 +4338,7 @@ impl Thread {
parent_thread_id: self.parent_thread_id().map(|id| id.to_string()),
prompt_id: self.prompt_id.to_string(),
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
compaction_model: compaction_model.telemetry_id(),
thinking_effort: self.thinking_effort.clone(),
max_tokens,
tokens_before,
Expand Down Expand Up @@ -4431,6 +4431,13 @@ impl Thread {
Some(self.messages.len())
}

fn compaction_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
LanguageModelRegistry::read_global(cx)
.compaction_model()
.map(|m| m.model)
.or_else(|| self.model().cloned())
}

fn build_compaction_request(
&self,
insertion_ix: usize,
Expand Down Expand Up @@ -4608,7 +4615,7 @@ struct CompactionTelemetry {
parent_thread_id: Option<String>,
prompt_id: String,
model: String,
model_provider: String,
compaction_model: String,
thinking_effort: Option<String>,
max_tokens: u64,
/// Tokens in the context window immediately before compaction.
Expand All @@ -4632,7 +4639,7 @@ impl CompactionTelemetry {
parent_thread_id = self.parent_thread_id,
prompt_id = self.prompt_id,
model = self.model,
model_provider = self.model_provider,
compaction_model = self.compaction_model,
thinking_effort = self.thinking_effort,
max_tokens = self.max_tokens,
tokens_before = self.tokens_before,
Expand Down Expand Up @@ -6670,12 +6677,15 @@ mod tests {
use language_model::LanguageModelToolUseId;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::LanguageModelProviderSetting;
use std::sync::Arc;

async fn setup_thread_for_test(cx: &mut TestAppContext) -> (Entity<Thread>, ThreadEventStream) {
cx.update(|cx| {
let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store);

LanguageModelRegistry::test(cx);
});

let fs = fs::FakeFs::new(cx.background_executor.clone());
Expand Down Expand Up @@ -6712,6 +6722,19 @@ mod tests {
AgentSettings::override_global(settings, cx);
}

fn set_registry_compaction_model(cx: &mut App, model: Option<Arc<dyn LanguageModel>>) {
use language_model::fake_provider::FakeLanguageModelProvider;
use language_model::{ConfiguredModel, LanguageModelProvider};
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
let configured = model.map(|m| ConfiguredModel {
provider: Arc::new(FakeLanguageModelProvider::default())
as Arc<dyn LanguageModelProvider>,
model: m,
});
registry.set_compaction_model(configured, cx);
});
}

#[test]
fn test_summary_compaction_renders_for_request_and_markdown() {
let message = Message::Compaction(CompactionInfo::Summary("Older context".into()));
Expand Down Expand Up @@ -7397,6 +7420,183 @@ mod tests {
);
}

/// When `agent.compaction_model` is configured, manual `/compact` streams
/// to the configured model rather than the thread's primary model.
#[gpui::test]
async fn test_compaction_uses_configured_compaction_model(cx: &mut TestAppContext) {
let (thread, _event_stream) = setup_thread_for_test(cx).await;
let thread_model = Arc::new(FakeLanguageModel::default());
let compaction_model = Arc::new(FakeLanguageModel::default());

cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.set_model(thread_model.clone(), cx);
thread
.messages
.push(user_text_message(ClientUserMessageId::new(), "old user"));
thread.messages.push(agent_text_message("old assistant"));
});
set_registry_compaction_model(cx, Some(compaction_model.clone()));
});

let _events = cx
.update(|cx| {
thread.update(cx, |thread, cx| {
thread.compact(ClientUserMessageId::new(), cx)
})
})
.unwrap();
cx.run_until_parked();

assert_eq!(
thread_model.pending_completions().len(),
0,
"thread's primary model should not have been used for compaction"
);
let request = compaction_model
.pending_completions()
.pop()
.expect("compaction model should have received the request");
assert_eq!(
request.intent,
Some(CompletionIntent::ThreadContextSummarization)
);

thread.read_with(cx, |thread, _cx| {
let telemetry = thread
.pending_compaction_telemetry
.as_ref()
.expect("pending telemetry");
assert_eq!(telemetry.model, compaction_model.telemetry_id());
});

compaction_model.send_completion_stream_text_chunk(&request, "summary");
compaction_model.end_completion_stream(&request);
cx.run_until_parked();
}

/// When `agent.compaction_model` is configured but doesn't resolve (e.g.
/// the provider isn't registered), manual `/compact` falls back to the
/// thread's primary model and the telemetry reflects the actual stream
/// model — not the one the user tried to configure.
#[gpui::test]
async fn test_compaction_falls_back_when_compaction_model_unavailable(cx: &mut TestAppContext) {
let (thread, _event_stream) = setup_thread_for_test(cx).await;
let thread_model = Arc::new(FakeLanguageModel::default());

cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.set_model(thread_model.clone(), cx);
thread
.messages
.push(user_text_message(ClientUserMessageId::new(), "old user"));
thread.messages.push(agent_text_message("old assistant"));
});
// Settings say "configured"; registry says "couldn't resolve".
let mut settings = AgentSettings::get_global(cx).clone();
settings.compaction_model = Some(LanguageModelSelection {
provider: LanguageModelProviderSetting("missing".into()),
model: "missing-model".into(),
enable_thinking: false,
effort: None,
speed: None,
});
AgentSettings::override_global(settings, cx);
set_registry_compaction_model(cx, None);
});

let _events = cx
.update(|cx| {
thread.update(cx, |thread, cx| {
thread.compact(ClientUserMessageId::new(), cx)
})
})
.unwrap();
cx.run_until_parked();

let request = thread_model
.pending_completions()
.pop()
.expect("thread model should have received the fallback request");
assert_eq!(
request.intent,
Some(CompletionIntent::ThreadContextSummarization)
);

thread.read_with(cx, |thread, _cx| {
let telemetry = thread
.pending_compaction_telemetry
.as_ref()
.expect("pending telemetry");
assert_eq!(telemetry.model, thread_model.telemetry_id());
});

thread_model.send_completion_stream_text_chunk(&request, "summary");
thread_model.end_completion_stream(&request);
cx.run_until_parked();
}

/// Auto-compaction triggered by the threshold also honors
/// `agent.compaction_model`.
#[gpui::test]
async fn test_auto_compaction_uses_compaction_model(cx: &mut TestAppContext) {
let (thread, _event_stream) = setup_thread_for_test(cx).await;
let thread_model = Arc::new(FakeLanguageModel::default());
let compaction_model = Arc::new(FakeLanguageModel::default());
let old_user_message_id = ClientUserMessageId::new();

cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.set_model(thread_model.clone(), cx);
thread
.messages
.push(user_text_message(old_user_message_id.clone(), "old user"));
thread.messages.push(agent_text_message("old assistant"));
thread.request_token_usage.insert(
old_user_message_id.clone(),
TokenUsage {
input_tokens: u64::MAX,
..Default::default()
},
);
});
set_auto_compact_settings(
cx,
agent_settings::AutoCompactSettings {
enabled: true,
threshold: agent_settings::AutoCompactThreshold::Percentage(0.5),
},
);
set_registry_compaction_model(cx, Some(compaction_model.clone()));
});

// The auto-compact gate fires inside `run_turn` when we kick off a
// new user message. Drive a `send` so `perform_compaction_if_needed`
// runs.
let _events = cx
.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(ClientUserMessageId::new(), vec!["new prompt"], cx)
})
})
.unwrap();
cx.run_until_parked();

assert_eq!(thread_model.pending_completions().len(), 0);
let request = compaction_model
.pending_completions()
.pop()
.expect("compaction model should have received the auto-compaction request");
assert_eq!(
request.intent,
Some(CompletionIntent::ThreadContextSummarization)
);

compaction_model.send_completion_stream_text_chunk(&request, "summary");
compaction_model.end_completion_stream(&request);
cx.run_until_parked();
}

#[gpui::test]
async fn test_compaction_usage_counts_toward_cumulative_usage(cx: &mut TestAppContext) {
let (thread, _event_stream) = setup_thread_for_test(cx).await;
Expand Down
1 change: 1 addition & 0 deletions crates/agent/src/tool_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ mod tests {
commit_message_include_project_rules: true,
commit_message_instructions: None,
thread_summary_model: None,
compaction_model: None,
inline_alternatives: vec![],
favorite_models: vec![],
default_profile: AgentProfileId::default(),
Expand Down
2 changes: 2 additions & 0 deletions crates/agent_settings/src/agent_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ pub struct AgentSettings {
pub commit_message_include_project_rules: bool,
pub commit_message_instructions: Option<String>,
pub thread_summary_model: Option<LanguageModelSelection>,
pub compaction_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub favorite_models: Vec<LanguageModelSelection>,
pub default_profile: AgentProfileId,
Expand Down Expand Up @@ -770,6 +771,7 @@ impl Settings for AgentSettings {
commit_message_model: agent.commit_message_model,
commit_message_instructions: agent.commit_message_instructions,
thread_summary_model: agent.thread_summary_model,
compaction_model: agent.compaction_model,
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
favorite_models: agent.favorite_models,
default_profile: AgentProfileId(agent.default_profile.unwrap()),
Expand Down
3 changes: 3 additions & 0 deletions crates/agent_ui/src/agent_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
.thread_summary_model
.as_ref()
.map(to_selected_model);
let compaction = settings.compaction_model.as_ref().map(to_selected_model);
let inline_alternatives = settings
.inline_alternatives
.iter()
Expand All @@ -940,6 +941,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
registry.select_compaction_model(compaction.as_ref(), cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
registry.set_should_use_fallback(should_use_fallback);
});
Expand Down Expand Up @@ -986,6 +988,7 @@ mod tests {
commit_message_include_project_rules: true,
commit_message_instructions: None,
thread_summary_model: None,
compaction_model: None,
inline_alternatives: vec![],
favorite_models: vec![],
default_profile: AgentProfileId::default(),
Expand Down
Loading
Loading