diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index 692c7ef04..9dc0e982e 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -577,6 +577,8 @@ pub struct PermissionConfigPartial { #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CompactionConfigPartial { + #[serde(default)] + pub prune_enabled: Option, #[serde(default)] pub prune_protected_tokens: Option, #[serde(default)] @@ -912,6 +914,7 @@ impl PermissionConfigPartial { impl CompactionConfigPartial { fn merge(self, upper: Self) -> Self { Self { + prune_enabled: upper.prune_enabled.or(self.prune_enabled), prune_protected_tokens: upper.prune_protected_tokens.or(self.prune_protected_tokens), prune_min_savings: upper.prune_min_savings.or(self.prune_min_savings), threshold: upper.threshold.or(self.threshold), @@ -1223,6 +1226,7 @@ impl TryFrom for WorkerManifest { validate_model_paths(cm, "compaction.model.auth.file")?; } Ok(CompactionConfig { + prune_enabled: c.prune_enabled.unwrap_or(defaults::PRUNE_ENABLED), prune_protected_tokens: c .prune_protected_tokens .unwrap_or(defaults::PRUNE_PROTECTED_TOKENS), @@ -1831,6 +1835,7 @@ mod tests { let lower = WorkerManifestConfig { compaction: Some(CompactionConfigPartial { threshold: Some(50_000), + prune_enabled: Some(false), prune_protected_tokens: Some(5_000), ..Default::default() }), @@ -1861,7 +1866,8 @@ mod tests { let merged = lower.merge(upper); let c = merged.compaction.unwrap(); assert_eq!(c.threshold, Some(80_000)); - // field from lower retained when upper has None + // fields from lower retained when upper has None + assert_eq!(c.prune_enabled, Some(false)); assert_eq!(c.prune_protected_tokens, Some(5_000)); let search = merged.web.unwrap().search.unwrap(); assert_eq!(search.timeout_secs, Some(3)); diff --git a/crates/manifest/src/defaults.rs b/crates/manifest/src/defaults.rs index 88aeafab4..d208d244a 100644 --- a/crates/manifest/src/defaults.rs +++ b/crates/manifest/src/defaults.rs @@ -14,6 +14,10 @@ pub const TOOL_OUTPUT_MAX_BYTES: usize = 64 * 1024; /// See [`crate::FileUploadLimits`]. pub const FILE_UPLOAD_MAX_BYTES: usize = 256 * 1024; +/// Whether request-time tool-result pruning is enabled when `[compaction]` +/// exists. See [`crate::CompactionConfig::prune_enabled`]. +pub const PRUNE_ENABLED: bool = false; + /// Token budget at the history tail protected from pruning. See /// [`crate::CompactionConfig::prune_protected_tokens`]. pub const PRUNE_PROTECTED_TOKENS: u64 = 8000; diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 63af43df6..aac87acf6 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -768,6 +768,11 @@ pub enum ToolPermissionAction { /// (full history summarisation). Omitting `[compaction]` disables both. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompactionConfig { + /// Whether request-time projection removes detail from old tool results. + /// Independent from the compaction thresholds below. + #[serde(default = "default_prune_enabled")] + pub prune_enabled: bool, + /// Token budget at the history tail protected from pruning. #[serde(default = "default_prune_protected_tokens")] pub prune_protected_tokens: u64, @@ -868,6 +873,9 @@ pub struct CompactionConfig { pub model: Option, } +fn default_prune_enabled() -> bool { + defaults::PRUNE_ENABLED +} fn default_prune_protected_tokens() -> u64 { defaults::PRUNE_PROTECTED_TOKENS } @@ -914,6 +922,7 @@ fn default_result_context_max_tokens() -> u64 { impl Default for CompactionConfig { fn default() -> Self { Self { + prune_enabled: default_prune_enabled(), prune_protected_tokens: default_prune_protected_tokens(), prune_min_savings: default_prune_min_savings(), threshold: None, @@ -1460,9 +1469,11 @@ model_id = "claude-sonnet-4-20250514" #[test] fn parse_compaction_config() { - let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\nthreshold = 80000\n"); + let toml = + format!("{MINIMAL_REQUIRED}\n[compaction]\nprune_enabled = false\nthreshold = 80000\n"); let manifest = WorkerManifest::from_toml(&toml).unwrap(); let c = manifest.compaction.unwrap(); + assert!(!c.prune_enabled); assert_eq!(c.prune_protected_tokens, 8000); assert_eq!(c.prune_min_savings, 4096); assert_eq!(c.threshold, Some(80000)); @@ -1471,6 +1482,11 @@ model_id = "claude-sonnet-4-20250514" assert_eq!(c.worker_max_turns, Some(20)); } + #[test] + fn compaction_defaults_to_pruning_disabled() { + assert!(!CompactionConfig::default().prune_enabled); + } + #[test] fn reject_removed_prune_protected_turns_field() { let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\nprune_protected_turns = 3\n"); diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index e5572b0bb..7f42ed499 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -1245,6 +1245,26 @@ mod tests { .unwrap() }; + for name in [ + "default", + "intake", + "orchestrator", + "companion", + "coder", + "reviewer", + "memory-consolidation", + ] { + let role = resolve(name); + assert_eq!( + role.manifest + .compaction + .as_ref() + .map(|compaction| compaction.prune_enabled), + Some(false), + "{name} must keep pruning disabled for the experiment" + ); + } + let companion = resolve("companion"); assert_eq!( companion.manifest.model.ref_.as_deref(), diff --git a/crates/worker/src/compact/prune.rs b/crates/worker/src/compact/prune.rs index 8288bbf05..6be34fa10 100644 --- a/crates/worker/src/compact/prune.rs +++ b/crates/worker/src/compact/prune.rs @@ -22,6 +22,13 @@ use crate::compact::token_counter::{ EstimateSource, savings_for_prune_impl, token_estimates_for_prune_impl, }; +fn prune_config_from_compaction(compaction: &manifest::CompactionConfig) -> Option { + compaction.prune_enabled.then_some(PruneConfig { + protected_tokens: compaction.prune_protected_tokens, + min_savings: compaction.prune_min_savings, + }) +} + impl Worker { /// Enable prune projection on the underlying Engine. /// @@ -105,18 +112,46 @@ impl Worker { worker.set_prune_observer(Some(observer)); } - /// If the manifest has a `[compaction]` section, build a `PruneConfig` - /// from its `prune_*` fields and call [`attach_prune`](Self::attach_prune). - /// Otherwise no-op. Called from all Worker constructors so prune is - /// active whenever the manifest asks for it. + /// If the manifest enables prune under `[compaction]`, build a + /// `PruneConfig` from its `prune_*` fields and call + /// [`attach_prune`](Self::attach_prune). Otherwise no-op. Called from all + /// Worker constructors. pub(crate) fn apply_prune_from_manifest(&mut self) { let Some(compaction) = self.manifest().compaction.as_ref() else { return; }; - let config = PruneConfig { - protected_tokens: compaction.prune_protected_tokens, - min_savings: compaction.prune_min_savings, + let Some(config) = prune_config_from_compaction(compaction) else { + return; }; self.attach_prune(config); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn disabled_pruning_does_not_build_prune_config() { + let compaction = manifest::CompactionConfig { + prune_enabled: false, + ..Default::default() + }; + + assert!(prune_config_from_compaction(&compaction).is_none()); + } + + #[test] + fn enabled_compaction_preserves_prune_thresholds() { + let compaction = manifest::CompactionConfig { + prune_enabled: true, + prune_protected_tokens: 1234, + prune_min_savings: 5678, + ..Default::default() + }; + + let config = prune_config_from_compaction(&compaction).unwrap(); + assert_eq!(config.protected_tokens, 1234); + assert_eq!(config.min_savings, 5678); + } +} diff --git a/crates/worker/src/spawn/tool.rs b/crates/worker/src/spawn/tool.rs index 6c0aa2824..5622dfbcf 100644 --- a/crates/worker/src/spawn/tool.rs +++ b/crates/worker/src/spawn/tool.rs @@ -956,6 +956,7 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi .compaction .as_ref() .map(|c| CompactionConfigPartial { + prune_enabled: Some(c.prune_enabled), prune_protected_tokens: Some(c.prune_protected_tokens), prune_min_savings: Some(c.prune_min_savings), threshold: c.threshold, diff --git a/crates/worker/tests/session_metrics_test.rs b/crates/worker/tests/session_metrics_test.rs index 17e34aa86..51149e832 100644 --- a/crates/worker/tests/session_metrics_test.rs +++ b/crates/worker/tests/session_metrics_test.rs @@ -159,6 +159,7 @@ model_id = "test-model" max_tokens = 100 [compaction] +prune_enabled = true prune_protected_tokens = {prune_protected_tokens} prune_min_savings = {prune_min_savings} diff --git a/docs/manifest.toml b/docs/manifest.toml index 5ec595511..92b671e6e 100644 --- a/docs/manifest.toml +++ b/docs/manifest.toml @@ -179,9 +179,12 @@ permission = "write" # ===== [compaction] ========================================================= # コンテキスト圧縮 (Prune / Compact)。セクション省略で両方無効。 -# セクションを書いた時点で Prune は有効化、Compact は閾値が None なら無効。 +# `prune_enabled`でPruneを制御する。Compactは閾値がNoneなら無効。 # [compaction] # +# # 任意。デフォルト: false。Compactの設定を保ったままPruneだけ切替できる。 +# prune_enabled = true +# # # 任意。デフォルト: 8000 (`defaults::PRUNE_PROTECTED_TOKENS`)。 # # pruning から保護する末尾 token budget。turn 数ではなく usage estimate で境界を引く。 # prune_protected_tokens = 8000 diff --git a/resources/profiles/base.dcdl b/resources/profiles/base.dcdl index 80a6121bd..04e42783f 100644 --- a/resources/profiles/base.dcdl +++ b/resources/profiles/base.dcdl @@ -16,6 +16,7 @@ engine = { compaction = { kind = "tokens"; + prune_enabled = false; threshold = 240000; request_threshold = 270000; worker_context_max_tokens = 100000;