mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
Add auto_compact agent settings (#58883)
Adds two new agent settings nested under `auto_compact`, `enabled` and `threshold`, which control automatic compaction of the agent's context window as part of the handoff feature. Both surface in the settings UI under Agent Configuration, gated behind the `handoff` feature flag so they only appear for users on that flag. The `threshold` accepts three forms: a percentage string ending in `%` (e.g. `"80%"` or `"95.5%"`) measured against the model's context window, a positive integer to compact after that many tokens have been used, or a negative integer to compact once that many tokens remain in the window. `0` and bare non-integer numbers are rejected. It deserializes from either a JSON string or integer and resolves to a typed enum, falling back to the `"80%"` default (with a logged warning) when the configured value is invalid. In the settings UI the threshold is a free-form text field so all three forms can be entered. Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
This commit is contained in:
parent
e5052961af
commit
9baefe701e
8 changed files with 446 additions and 33 deletions
|
|
@ -1098,6 +1098,22 @@
|
|||
},
|
||||
// When enabled, agent edits will be displayed in single-file editors for review
|
||||
"single_file_review": false,
|
||||
// Settings for automatic agent context compaction, which summarizes earlier
|
||||
// messages to free up room in the model's context window once it grows too
|
||||
// large.
|
||||
"auto_compact": {
|
||||
// Whether to automatically compact the agent's context near the limit.
|
||||
"enabled": true,
|
||||
// The threshold at which auto-compaction runs. One of:
|
||||
// - A percentage string ending in "%" (e.g. "90%"), measured against
|
||||
// the model's context window. Decimals are allowed (e.g. "95.5%").
|
||||
// - A positive integer: compact after that many tokens have been used
|
||||
// (e.g. 100000 compacts after 100,000 tokens are used).
|
||||
// - A negative integer: compact once that many tokens remain in the
|
||||
// context window (e.g. -20000 compacts once fewer than 20,000 remain).
|
||||
// 0 is not a valid threshold.
|
||||
"threshold": "90%",
|
||||
},
|
||||
// When enabled, show voting thumbs for feedback on agent edits.
|
||||
"enable_feedback": true,
|
||||
"default_profile": "write",
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ use acp_thread::{MentionUri, UserMessageId};
|
|||
use action_log::ActionLog;
|
||||
use agent_settings::UserAgentsMd;
|
||||
use feature_flags::{FeatureFlagAppExt as _, HandoffFeatureFlag};
|
||||
use zed_env_vars::{EnvVar, env_var};
|
||||
|
||||
use crate::sandboxing::{SandboxRequest, ThreadSandboxGrants, sandboxing_enabled};
|
||||
use agent_client_protocol::schema as acp;
|
||||
use agent_settings::{
|
||||
AgentProfileId, AgentSettings, COMPACTION_PROMPT, SUMMARIZE_THREAD_DETAILED_PROMPT,
|
||||
SUMMARIZE_THREAD_PROMPT,
|
||||
AgentProfileId, AgentSettings, AutoCompactThreshold, COMPACTION_PROMPT,
|
||||
SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
|
|
@ -70,17 +69,12 @@ const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
|
|||
pub const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
pub const MAX_SUBAGENT_DEPTH: u8 = 1;
|
||||
|
||||
const AGENT_COMPACTION_REMAINING_TOKEN_BUDGET: u64 = 40_000;
|
||||
|
||||
/// Auto-compaction is only available for models whose context window is at least
|
||||
/// this large. For smaller models there isn't enough headroom for a compaction
|
||||
/// pass to be worthwhile, so we leave the thread uncompacted and let the UI warn
|
||||
/// the user instead.
|
||||
pub const MIN_COMPACTION_CONTEXT_WINDOW: u64 = 80_000;
|
||||
|
||||
static AGENT_COMPACTION_REMAINING_TOKEN_BUDGET_ENV_VAR: std::sync::LazyLock<EnvVar> =
|
||||
env_var!("AGENT_COMPACTION_REMAINING_TOKEN_BUDGET");
|
||||
|
||||
// Using the heuristic that 1 token is about 4 bytes, keep the last 80K bytes of user-message content (~20k tokens).
|
||||
const COMPACTION_RETAINED_USER_MESSAGES_BYTE_BUDGET: usize = 80_000;
|
||||
|
||||
|
|
@ -2658,7 +2652,7 @@ impl Thread {
|
|||
cx: &mut AsyncApp,
|
||||
) -> Result<ControlFlow<()>> {
|
||||
let Some((model, request, insertion_ix)) = this.update(cx, |this, cx| {
|
||||
let insertion_ix = this.compaction_message_target_ix()?;
|
||||
let insertion_ix = this.compaction_message_target_ix(cx)?;
|
||||
let model = this.model.clone()?;
|
||||
let request = this.build_compaction_request(insertion_ix, &model, cx);
|
||||
this.current_request_token_usage = TokenUsage::default();
|
||||
|
|
@ -3765,11 +3759,17 @@ impl Thread {
|
|||
.rposition(|message| matches!(&**message, Message::Compaction(_)))
|
||||
}
|
||||
|
||||
fn compaction_message_target_ix(&self) -> Option<usize> {
|
||||
fn compaction_message_target_ix(&self, cx: &App) -> Option<usize> {
|
||||
let auto_compact = AgentSettings::get_global(cx).auto_compact;
|
||||
if !auto_compact.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let model = self.model.as_ref()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
// Models with a small context window don't leave enough headroom for a
|
||||
// compaction pass; the UI warns the user about the token limit instead.
|
||||
if model.max_token_count() < MIN_COMPACTION_CONTEXT_WINDOW {
|
||||
if max_token_count < MIN_COMPACTION_CONTEXT_WINDOW {
|
||||
return None;
|
||||
}
|
||||
let (usage_ix, usage) = {
|
||||
|
|
@ -3796,17 +3796,8 @@ impl Thread {
|
|||
}
|
||||
|
||||
let active_tokens = total_input_tokens(usage).saturating_add(usage.output_tokens);
|
||||
|
||||
let remaining_budget = AGENT_COMPACTION_REMAINING_TOKEN_BUDGET_ENV_VAR
|
||||
.value
|
||||
.as_ref()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(AGENT_COMPACTION_REMAINING_TOKEN_BUDGET);
|
||||
|
||||
let compaction_threshold = model
|
||||
.max_token_count()
|
||||
.saturating_sub(model.max_output_tokens().unwrap_or_default())
|
||||
.saturating_sub(remaining_budget);
|
||||
let compaction_threshold =
|
||||
auto_compact_threshold_token_count(auto_compact.threshold, max_token_count);
|
||||
if active_tokens < compaction_threshold {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -4024,6 +4015,21 @@ fn total_input_tokens(usage: language_model::TokenUsage) -> u64 {
|
|||
.saturating_add(usage.cache_read_input_tokens)
|
||||
}
|
||||
|
||||
fn auto_compact_threshold_token_count(
|
||||
threshold: AutoCompactThreshold,
|
||||
max_token_count: u64,
|
||||
) -> u64 {
|
||||
match threshold {
|
||||
AutoCompactThreshold::Percentage(percent) => {
|
||||
((max_token_count as f64) * percent).ceil() as u64
|
||||
}
|
||||
AutoCompactThreshold::TokensUsed(tokens) => tokens,
|
||||
AutoCompactThreshold::TokensRemaining(tokens) => {
|
||||
max_token_count.saturating_sub(tokens).saturating_add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn user_message_byte_len(message: &LanguageModelRequestMessage) -> usize {
|
||||
message
|
||||
.content
|
||||
|
|
@ -5599,6 +5605,12 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
fn set_auto_compact_settings(cx: &mut App, auto_compact: agent_settings::AutoCompactSettings) {
|
||||
let mut settings = AgentSettings::get_global(cx).clone();
|
||||
settings.auto_compact = auto_compact;
|
||||
AgentSettings::override_global(settings, cx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary_compaction_renders_for_request_and_markdown() {
|
||||
let message = Message::Compaction(CompactionInfo::Summary("Older context".into()));
|
||||
|
|
@ -5654,12 +5666,54 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_compaction_threshold_uses_latest_reported_usage(cx: &mut TestAppContext) {
|
||||
async fn test_compaction_threshold_uses_percentage_setting(cx: &mut TestAppContext) {
|
||||
let (thread, _event_stream) = setup_thread_for_test(cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let user_message_id = UserMessageId::new();
|
||||
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model, cx);
|
||||
thread
|
||||
.messages
|
||||
.push(user_text_message(user_message_id.clone(), "below limit"));
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 899_999,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 900_000,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_compaction_threshold_respects_enabled_setting(cx: &mut TestAppContext) {
|
||||
let (thread, _event_stream) = setup_thread_for_test(cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let user_message_id = UserMessageId::new();
|
||||
|
||||
cx.update(|cx| {
|
||||
set_auto_compact_settings(
|
||||
cx,
|
||||
agent_settings::AutoCompactSettings {
|
||||
enabled: false,
|
||||
threshold: AutoCompactThreshold::Percentage(0.9),
|
||||
},
|
||||
);
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model, cx);
|
||||
thread
|
||||
|
|
@ -5673,35 +5727,77 @@ mod tests {
|
|||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(), Some(1));
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_compaction_threshold_reserves_max_output_tokens(cx: &mut TestAppContext) {
|
||||
async fn test_compaction_threshold_respects_token_settings(cx: &mut TestAppContext) {
|
||||
let (thread, _event_stream) = setup_thread_for_test(cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
model.set_max_token_count(272_000);
|
||||
model.set_max_output_tokens(Some(128_000));
|
||||
let user_message_id = UserMessageId::new();
|
||||
|
||||
cx.update(|cx| {
|
||||
set_auto_compact_settings(
|
||||
cx,
|
||||
agent_settings::AutoCompactSettings {
|
||||
enabled: true,
|
||||
threshold: AutoCompactThreshold::TokensUsed(100_000),
|
||||
},
|
||||
);
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model, cx);
|
||||
thread.messages.push(user_text_message(
|
||||
user_message_id.clone(),
|
||||
"near output-reserved limit",
|
||||
"fixed token limit",
|
||||
));
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 105_000,
|
||||
input_tokens: 99_999,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(), Some(1));
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100_000,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
|
||||
|
||||
set_auto_compact_settings(
|
||||
cx,
|
||||
agent_settings::AutoCompactSettings {
|
||||
enabled: true,
|
||||
threshold: AutoCompactThreshold::TokensRemaining(20_000),
|
||||
},
|
||||
);
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 980_000,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
|
||||
thread.request_token_usage.insert(
|
||||
user_message_id.clone(),
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 980_001,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), Some(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -5728,7 +5824,7 @@ mod tests {
|
|||
},
|
||||
);
|
||||
|
||||
assert_eq!(thread.compaction_message_target_ix(), None);
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -5830,7 +5926,7 @@ mod tests {
|
|||
.push(user_text_message(user_message_id.clone(), "old user"));
|
||||
thread.messages.push(agent_text_message("old assistant"));
|
||||
// Auto-compaction would be a no-op here.
|
||||
assert_eq!(thread.compaction_message_target_ix(), None);
|
||||
assert_eq!(thread.compaction_message_target_ix(cx), None);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -590,6 +590,10 @@ mod tests {
|
|||
play_sound_when_agent_done: PlaySoundWhenAgentDone::default(),
|
||||
single_file_review: false,
|
||||
model_parameters: vec![],
|
||||
auto_compact: agent_settings::AutoCompactSettings {
|
||||
enabled: false,
|
||||
threshold: agent_settings::AutoCompactThreshold::DEFAULT,
|
||||
},
|
||||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
mod agent_profile;
|
||||
mod user_agents_md;
|
||||
|
||||
use std::cmp::Ordering::{Equal, Greater, Less};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::channel::oneshot;
|
||||
|
|
@ -18,6 +20,7 @@ use settings::{
|
|||
SettingsStore, SidebarDockPosition, SidebarSide, ThinkingBlockDisplay, ToolPermissionMode,
|
||||
update_settings_file, update_settings_file_with_completion,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md};
|
||||
|
|
@ -135,6 +138,58 @@ impl WindowLayout {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutoCompactThreshold {
|
||||
/// Compact once the context window is at least this full, as a fraction in
|
||||
/// the range `(0.0, 1.0]`.
|
||||
Percentage(f64),
|
||||
/// Compact once at least this many tokens have been used.
|
||||
TokensUsed(u64),
|
||||
/// Compact once fewer than this many tokens remain in the context window.
|
||||
TokensRemaining(u64),
|
||||
}
|
||||
|
||||
impl AutoCompactThreshold {
|
||||
/// The threshold used when none is configured, or when the configured value
|
||||
/// is invalid (90% of the context window).
|
||||
pub const DEFAULT: Self = Self::Percentage(0.9);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AutoCompactSettings {
|
||||
pub enabled: bool,
|
||||
pub threshold: AutoCompactThreshold,
|
||||
}
|
||||
|
||||
fn parse_auto_compact_threshold(raw: &str) -> anyhow::Result<AutoCompactThreshold> {
|
||||
let trimmed = raw.trim();
|
||||
if let Some(percent) = trimmed.strip_suffix('%') {
|
||||
let value: f64 = percent
|
||||
.trim_end()
|
||||
.parse()
|
||||
.with_context(|| format!("invalid auto_compact threshold percentage {raw:?}"))?;
|
||||
anyhow::ensure!(
|
||||
value > 0.0 && value <= 100.0,
|
||||
"auto_compact threshold percentage must be between 0% and 100%, got {raw:?}"
|
||||
);
|
||||
Ok(AutoCompactThreshold::Percentage(value / 100.0))
|
||||
} else {
|
||||
let tokens: i64 = trimmed.parse().with_context(|| {
|
||||
format!(
|
||||
"invalid auto_compact threshold {raw:?}; \
|
||||
expected a percentage like \"90%\" or an integer number of tokens"
|
||||
)
|
||||
})?;
|
||||
match tokens.cmp(&0) {
|
||||
Greater => Ok(AutoCompactThreshold::TokensUsed(tokens as u64)),
|
||||
Less => Ok(AutoCompactThreshold::TokensRemaining(tokens.unsigned_abs())),
|
||||
Equal => {
|
||||
anyhow::bail!("auto_compact threshold of 0 is not valid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, RegisterSetting)]
|
||||
pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
|
|
@ -161,6 +216,7 @@ pub struct AgentSettings {
|
|||
pub play_sound_when_agent_done: PlaySoundWhenAgentDone,
|
||||
pub single_file_review: bool,
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
pub auto_compact: AutoCompactSettings,
|
||||
pub enable_feedback: bool,
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
|
|
@ -682,6 +738,16 @@ impl Settings for AgentSettings {
|
|||
play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap_or_default(),
|
||||
single_file_review: agent.single_file_review.unwrap(),
|
||||
model_parameters: agent.model_parameters,
|
||||
auto_compact: {
|
||||
let auto_compact = agent.auto_compact.unwrap();
|
||||
let threshold = parse_auto_compact_threshold(&auto_compact.threshold.unwrap().0)
|
||||
.log_err()
|
||||
.unwrap_or(AutoCompactThreshold::DEFAULT);
|
||||
AutoCompactSettings {
|
||||
enabled: auto_compact.enabled.unwrap(),
|
||||
threshold,
|
||||
}
|
||||
},
|
||||
enable_feedback: agent.enable_feedback.unwrap(),
|
||||
expand_edit_card: agent.expand_edit_card.unwrap(),
|
||||
expand_terminal_card: agent.expand_terminal_card.unwrap(),
|
||||
|
|
@ -823,6 +889,47 @@ mod tests {
|
|||
use settings::ToolPermissionMode;
|
||||
use settings::ToolPermissionsContent;
|
||||
|
||||
#[test]
|
||||
fn test_parse_auto_compact_threshold() {
|
||||
use AutoCompactThreshold::*;
|
||||
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold("90%").unwrap(),
|
||||
Percentage(0.9)
|
||||
);
|
||||
assert_eq!(AutoCompactThreshold::DEFAULT, Percentage(0.9));
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold(" 92.5% ").unwrap(),
|
||||
Percentage(0.925)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold("95.5%").unwrap(),
|
||||
Percentage(0.955)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold("100%").unwrap(),
|
||||
Percentage(1.0)
|
||||
);
|
||||
// Token counts must be integers; a non-integer token value is invalid.
|
||||
assert!(parse_auto_compact_threshold("100.5").is_err());
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold("100000").unwrap(),
|
||||
TokensUsed(100_000)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_auto_compact_threshold("-20000").unwrap(),
|
||||
TokensRemaining(20_000)
|
||||
);
|
||||
|
||||
// 0 is invalid in every form.
|
||||
assert!(parse_auto_compact_threshold("0").is_err());
|
||||
assert!(parse_auto_compact_threshold("0%").is_err());
|
||||
// Out-of-range percentages and bare decimals are invalid.
|
||||
assert!(parse_auto_compact_threshold("150%").is_err());
|
||||
assert!(parse_auto_compact_threshold("0.8").is_err());
|
||||
assert!(parse_auto_compact_threshold("eighty percent").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compiled_regex_case_insensitive() {
|
||||
let regex = CompiledRegex::new("rm\\s+-rf", false).unwrap();
|
||||
|
|
|
|||
|
|
@ -957,6 +957,10 @@ mod tests {
|
|||
play_sound_when_agent_done: PlaySoundWhenAgentDone::Never,
|
||||
single_file_review: false,
|
||||
model_parameters: vec![],
|
||||
auto_compact: agent_settings::AutoCompactSettings {
|
||||
enabled: false,
|
||||
threshold: agent_settings::AutoCompactThreshold::DEFAULT,
|
||||
},
|
||||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,123 @@ pub enum ThinkingBlockDisplay {
|
|||
AlwaysCollapsed,
|
||||
}
|
||||
|
||||
/// Threshold at which agent auto-compaction runs. See
|
||||
/// [`AutoCompactSettingsContent::threshold`] for the accepted formats.
|
||||
///
|
||||
/// The canonical textual form is stored verbatim so it can round-trip through
|
||||
/// the settings UI; it is serialized back as a JSON string for percentages and
|
||||
/// as a JSON integer for token counts.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, MergeFrom)]
|
||||
pub struct AutoCompactThreshold(pub String);
|
||||
|
||||
impl From<String> for AutoCompactThreshold {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AutoCompactThreshold> for String {
|
||||
fn from(value: AutoCompactThreshold) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AutoCompactThreshold {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AutoCompactThreshold {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
if self.0.ends_with('%') {
|
||||
serializer.serialize_str(&self.0)
|
||||
} else if let Ok(tokens) = self.0.parse::<i64>() {
|
||||
serializer.serialize_i64(tokens)
|
||||
} else {
|
||||
serializer.serialize_str(&self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AutoCompactThreshold {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct ThresholdVisitor;
|
||||
|
||||
impl serde::de::Visitor<'_> for ThresholdVisitor {
|
||||
type Value = AutoCompactThreshold;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter
|
||||
.write_str("a percentage string like \"90%\" or an integer number of tokens")
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
Ok(AutoCompactThreshold(value.to_owned()))
|
||||
}
|
||||
|
||||
fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
|
||||
Ok(AutoCompactThreshold(value.to_string()))
|
||||
}
|
||||
|
||||
fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
|
||||
Ok(AutoCompactThreshold(value.to_string()))
|
||||
}
|
||||
|
||||
fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
|
||||
Ok(AutoCompactThreshold(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ThresholdVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AutoCompactThreshold {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AutoCompactThreshold".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^\\d+(\\.\\d+)?%$"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
|
||||
pub struct AutoCompactSettingsContent {
|
||||
/// Whether to automatically compact the agent's context when it grows too
|
||||
/// large, summarizing earlier messages to free up room in the model's
|
||||
/// context window.
|
||||
///
|
||||
/// Default: true
|
||||
pub enabled: Option<bool>,
|
||||
/// The threshold at which auto-compaction runs. This is one of:
|
||||
///
|
||||
/// - A percentage string ending in `%`, e.g. `"90%"`, measured against the
|
||||
/// model's context window. `"90%"` compacts once the context is 90% full.
|
||||
/// - A positive integer: compaction runs once that many tokens have been
|
||||
/// used. For example, `100000` compacts after 100,000 tokens are used.
|
||||
/// - A negative integer: compaction runs once that many tokens remain in
|
||||
/// the context window. For example, `-20000` compacts once there are
|
||||
/// fewer than 20,000 tokens of headroom left in the context window.
|
||||
///
|
||||
/// `0` is not a valid threshold.
|
||||
///
|
||||
/// Default: "90%"
|
||||
pub threshold: Option<AutoCompactThreshold>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
|
||||
pub struct AgentSettingsContent {
|
||||
|
|
@ -165,6 +282,10 @@ pub struct AgentSettingsContent {
|
|||
/// Default: []
|
||||
#[serde(default)]
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
/// Settings for automatic agent context compaction, which summarizes
|
||||
/// earlier messages to free up room in the model's context window once the
|
||||
/// context grows too large.
|
||||
pub auto_compact: Option<AutoCompactSettingsContent>,
|
||||
/// Whether to show thumb buttons for feedback in the agent panel.
|
||||
///
|
||||
/// Default: true
|
||||
|
|
|
|||
|
|
@ -7819,7 +7819,9 @@ fn ai_page(cx: &App) -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn agent_configuration_section(_cx: &App) -> Box<[SettingsPageItem]> {
|
||||
fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> {
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
|
||||
let mut items = vec![
|
||||
SettingsPageItem::SectionHeader("Agent Configuration"),
|
||||
SettingsPageItem::SubPageLink(SubPageLink {
|
||||
|
|
@ -8105,6 +8107,68 @@ fn ai_page(cx: &App) -> SettingsPage {
|
|||
}),
|
||||
]);
|
||||
|
||||
if cx.has_flag::<feature_flags::HandoffFeatureFlag>() {
|
||||
items.extend([
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Auto Compact",
|
||||
description: "Automatically compact the agent's context when it grows too large, summarizing earlier messages to free up room in the model's context window.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("agent.auto_compact.enabled"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.agent
|
||||
.as_ref()?
|
||||
.auto_compact
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.auto_compact
|
||||
.get_or_insert_default()
|
||||
.enabled = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Auto Compact Threshold",
|
||||
description: "When auto compaction runs. A percentage string like \"90%\" is measured against the context window. A positive integer is the number of used tokens to compact after. A negative integer is the number of tokens remaining in the context window before compacting.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("agent.auto_compact.threshold"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.agent
|
||||
.as_ref()?
|
||||
.auto_compact
|
||||
.as_ref()?
|
||||
.threshold
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.auto_compact
|
||||
.get_or_insert_default()
|
||||
.threshold = value;
|
||||
},
|
||||
}),
|
||||
metadata: Some(Box::new(SettingsFieldMetadata {
|
||||
placeholder: Some("90%"),
|
||||
..Default::default()
|
||||
})),
|
||||
files: USER,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
items.into_boxed_slice()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -565,6 +565,7 @@ fn init_renderers(cx: &mut App) {
|
|||
.add_basic_renderer::<settings::EditPredictionPromptFormatContent>(render_dropdown)
|
||||
.add_basic_renderer::<settings::EditPredictionDataCollectionChoice>(render_dropdown)
|
||||
.add_basic_renderer::<f32>(render_editable_number_field)
|
||||
.add_basic_renderer::<settings::AutoCompactThreshold>(render_text_field)
|
||||
.add_basic_renderer::<u32>(render_editable_number_field)
|
||||
.add_basic_renderer::<u64>(render_editable_number_field)
|
||||
.add_basic_renderer::<usize>(render_editable_number_field)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue