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
116 changes: 20 additions & 96 deletions crates/agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ mod tools;

use context_server::ContextServerId;
pub use db::*;
use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag};
use itertools::Itertools;
pub use native_agent_server::NativeAgentServer;
pub use pattern_extraction::*;
Expand Down Expand Up @@ -1494,24 +1493,21 @@ impl NativeAgent {
let Some(state) = project_state else {
return Vec::new();
};
let compact_command = cx.has_flag::<HandoffFeatureFlag>().then(|| {
acp::AvailableCommand::new(
COMPACT_COMMAND_NAME,
"Summarize the conversation so far to free up context",
)
.meta(acp_thread::meta_with_command_category(
acp_thread::CommandCategory::Native,
))
});
let compact_command = acp::AvailableCommand::new(
COMPACT_COMMAND_NAME,
"Summarize the conversation so far to free up context",
)
.meta(acp_thread::meta_with_command_category(
acp_thread::CommandCategory::Native,
));

let registry = state.context_server_registry.read(cx);

// Reserve the built-in command name (when active) so a same-named MCP
// prompt is force-prefixed (`/<server>.compact`) and stays reachable:
// an unqualified `/compact` always routes to the native command.
let reserved = compact_command.as_ref().map(|_| COMPACT_COMMAND_NAME);
// Reserve the built-in command name so a same-named MCP prompt is
// force-prefixed (`/<server>.compact`) and stays reachable: an
// unqualified `/compact` always routes to the native command.
let ambiguous_prompt_names = ambiguous_mcp_prompt_names(
reserved,
[COMPACT_COMMAND_NAME],
registry.prompts().map(|p| p.prompt.name.as_str()),
);

Expand Down Expand Up @@ -1550,7 +1546,9 @@ impl NativeAgent {
Some(command)
});

compact_command.into_iter().chain(mcp_commands).collect()
std::iter::once(compact_command)
.chain(mcp_commands)
.collect()
}

pub fn load_thread(
Expand Down Expand Up @@ -2583,9 +2581,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
};

if let Some(parsed_command) = Command::parse(&params.prompt) {
if cx.has_flag::<HandoffFeatureFlag>()
&& parsed_command.is_unqualified(COMPACT_COMMAND_NAME)
{
if parsed_command.is_unqualified(COMPACT_COMMAND_NAME) {
return self.0.update(cx, |agent, cx| {
agent.send_compact_command(id, session_id, cx)
});
Expand Down Expand Up @@ -3594,7 +3590,7 @@ mod internal_tests {
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
use agent_settings::COMPACTION_PROMPT;
use fs::FakeFs;
use gpui::{TestAppContext, UpdateGlobal};
use gpui::TestAppContext;
use indoc::formatdoc;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::{
Expand Down Expand Up @@ -3666,27 +3662,9 @@ mod internal_tests {
.collect()
}

fn set_handoff_flag_override(value: &str, cx: &mut TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |store, _| {
store.register_setting::<feature_flags::FeatureFlagsSettings>();
});
cx.update_flags(false, vec![]);
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |content| {
content
.feature_flags
.get_or_insert_default()
.insert("handoff".to_string(), value.to_string());
});
});
});
}

#[gpui::test]
async fn test_compact_command_requires_handoff_feature_flag(cx: &mut TestAppContext) {
async fn test_compact_command_is_available(cx: &mut TestAppContext) {
init_test(cx);
set_handoff_flag_override("off", cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
let thread_store = cx.new(|cx| ThreadStore::new(cx));
Expand All @@ -3706,30 +3684,11 @@ mod internal_tests {
.unwrap();
cx.run_until_parked();

cx.update(|cx| {
let commands = acp_thread.read(cx).available_commands();
assert!(commands.is_empty());
});

set_handoff_flag_override("on", cx);

let acp_thread = cx
.update(|cx| {
Rc::new(connection.clone()).new_session(
project.clone(),
PathList::new(&[Path::new("/")]),
cx,
)
})
.await
.unwrap();
cx.run_until_parked();

cx.update(|cx| {
let commands = acp_thread.read(cx).available_commands();

let compact = commands.iter().find(|command| command.name == "compact");
let compact = compact.expect("compact command should be available behind the flag");
let compact = compact.expect("compact command should be available");
assert_eq!(
acp_thread::command_category_from_meta(&compact.meta),
Some(acp_thread::CommandCategory::Native),
Expand All @@ -3738,43 +3697,8 @@ mod internal_tests {
}

#[gpui::test]
async fn test_compact_prompt_is_regular_prompt_without_handoff(cx: &mut TestAppContext) {
init_test(cx);
set_handoff_flag_override("off", cx);

let (connection, agent, _project, acp_thread) = setup_native_agent_session(cx).await;
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx));
let model = Arc::new(FakeLanguageModel::default());
cx.update(|cx| thread.update(cx, |thread, cx| thread.set_model(model.clone(), cx)));

let message_id = UserMessageId::new();
let prompt_task = cx.update(|cx| {
connection.prompt(
message_id.clone(),
acp::PromptRequest::new(session_id.clone(), vec!["/compact".into()]),
cx,
)
});
cx.run_until_parked();

let request = model.pending_completions().pop().unwrap();
assert_eq!(request.intent, Some(CompletionIntent::UserPrompt));
assert_eq!(
request_texts_after_system(&request.messages),
vec!["/compact".to_string()]
);

model.send_completion_stream_text_chunk(&request, "regular response");
model.end_completion_stream(&request);
cx.run_until_parked();
prompt_task.await.unwrap();
}

#[gpui::test]
async fn test_compact_prompt_routes_to_manual_compaction_with_handoff(cx: &mut TestAppContext) {
async fn test_compact_prompt_routes_to_manual_compaction(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| cx.update_flags(true, vec!["handoff".to_string()]));
let (connection, agent, project, acp_thread) = setup_native_agent_session(cx).await;
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
let thread = cx.update(|cx| native_thread_for_session(&agent, &session_id, cx));
Expand Down Expand Up @@ -3833,7 +3757,7 @@ mod internal_tests {
assert!(ambiguous.contains("compact"));
assert!(!ambiguous.contains("deploy"));

// Without the reservation (handoff off), a unique MCP prompt is left bare.
// Without the reservation, a unique MCP prompt is left bare.
let ambiguous = ambiguous_mcp_prompt_names([], ["compact", "deploy"]);
assert!(ambiguous.is_empty());

Expand Down
141 changes: 68 additions & 73 deletions crates/agent/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::{
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
use agent_settings::UserAgentsMd;
use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag};

use crate::sandboxing::{SandboxRequest, ThreadSandboxGrants, sandboxing_enabled};
use agent_client_protocol::schema as acp;
Expand Down Expand Up @@ -2428,78 +2427,76 @@ impl Thread {
// Set when a refusal fallback occurs so subsequent iterations use the fallback model.
let mut refusal_fallback_model: Option<Arc<dyn LanguageModel>> = None;
loop {
if cx.update(|cx| cx.has_flag::<HandoffFeatureFlag>()) {
match Self::perform_compaction_if_needed(
this,
event_stream,
cancellation_rx.clone(),
cx,
)
.await
{
// On success the telemetry event is deferred until the
// completion below reports usage, so we can record an
// accurate post-compaction context size (see
// `handle_completion_event`).
Ok(ControlFlow::Continue(())) => {}
Ok(ControlFlow::Break(())) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome("canceled", None)
})?;
return Ok(());
}
Err(error) => {
log::error!("Compaction failed: {}", error);
let error_message = error.to_string();
match error.downcast::<LanguageModelCompletionError>() {
Ok(error) => {
attempt += 1;
match Self::retry_completion_error(
this,
event_stream,
&mut cancellation_rx,
error,
attempt,
cx,
)
.await
{
Ok(ControlFlow::Break(())) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome("canceled", None)
})?;
return Ok(());
}
Ok(ControlFlow::Continue(())) => {
this.update(cx, |this, _| {
if let Some(telemetry) =
this.pending_compaction_telemetry.as_mut()
{
telemetry.retries += 1;
}
})?;
continue;
}
Err(retry_error) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome(
"failed",
Some(error_message),
)
})?;
return Err(retry_error);
}
match Self::perform_compaction_if_needed(
this,
event_stream,
cancellation_rx.clone(),
cx,
)
.await
{
// On success the telemetry event is deferred until the
// completion below reports usage, so we can record an
// accurate post-compaction context size (see
// `handle_completion_event`).
Ok(ControlFlow::Continue(())) => {}
Ok(ControlFlow::Break(())) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome("canceled", None)
})?;
return Ok(());
}
Err(error) => {
log::error!("Compaction failed: {}", error);
let error_message = error.to_string();
match error.downcast::<LanguageModelCompletionError>() {
Ok(error) => {
attempt += 1;
match Self::retry_completion_error(
this,
event_stream,
&mut cancellation_rx,
error,
attempt,
cx,
)
.await
{
Ok(ControlFlow::Break(())) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome("canceled", None)
})?;
return Ok(());
}
Ok(ControlFlow::Continue(())) => {
this.update(cx, |this, _| {
if let Some(telemetry) =
this.pending_compaction_telemetry.as_mut()
{
telemetry.retries += 1;
}
})?;
continue;
}
Err(retry_error) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome(
"failed",
Some(error_message),
)
})?;
return Err(retry_error);
}
}
Err(error) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome(
"failed",
Some(error_message),
)
})?;
return Err(error);
}
}
Err(error) => {
this.update(cx, |this, _| {
this.emit_compaction_telemetry_outcome(
"failed",
Some(error_message),
)
})?;
return Err(error);
}
}
}
Expand Down Expand Up @@ -6127,7 +6124,6 @@ mod tests {
let new_user_message_id = UserMessageId::new();

cx.update(|cx| {
cx.update_flags(true, vec!["handoff".to_string()]);
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread
Expand Down Expand Up @@ -6443,7 +6439,6 @@ mod tests {
};

cx.update(|cx| {
cx.update_flags(true, vec!["handoff".to_string()]);
thread.update(cx, |thread, cx| {
thread.set_model(model.clone(), cx);
thread
Expand Down
2 changes: 1 addition & 1 deletion crates/agent_ui/src/conversation_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use editor::scroll::Autoscroll;
use editor::{
Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
};
use feature_flags::{AgentSharingFeatureFlag, FeatureFlagAppExt as _, HandoffFeatureFlag};
use feature_flags::{AgentSharingFeatureFlag, FeatureFlagAppExt as _};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
Expand Down
14 changes: 6 additions & 8 deletions crates/agent_ui/src/conversation_view/thread_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10150,14 +10150,12 @@ impl ThreadView {

let token_usage = self.thread.read(cx).token_usage()?;

// When auto-compaction is available (the handoff feature flag is enabled
// and the model's context window is large enough), the thread is
// compacted automatically before it reaches the limit, so there's no
// need to warn the user. Models with a context window that's too small
// can't be auto-compacted, so we fall back to the normal warning.
if cx.has_flag::<HandoffFeatureFlag>()
&& token_usage.max_tokens >= agent::MIN_COMPACTION_CONTEXT_WINDOW
{
// When auto-compaction is available (the model's context window is large
// enough), the thread is compacted automatically before it reaches the
// limit, so there's no need to warn the user. Models with a context
// window that's too small can't be auto-compacted, so we fall back to
// the normal warning.
if token_usage.max_tokens >= agent::MIN_COMPACTION_CONTEXT_WINDOW {
return None;
}

Expand Down
Loading
Loading