feat: bound compact worker context

This commit is contained in:
2026-05-28 11:59:41 +09:00
parent 7034d02455
commit c274e4a891
10 changed files with 791 additions and 211 deletions
+90 -42
View File
@@ -125,18 +125,34 @@ pub struct CompactionConfigPartial {
pub prune_protected_tokens: Option<u64>,
#[serde(default)]
pub prune_min_savings: Option<u64>,
#[serde(default, alias = "compact_threshold")]
pub threshold: Option<u64>,
#[serde(default, alias = "compact_request_threshold")]
pub request_threshold: Option<u64>,
#[serde(default, alias = "compact_retained_tokens")]
pub retained_tokens: Option<u64>,
#[serde(default)]
pub compact_threshold: Option<u64>,
pub overview_target_tokens: Option<u64>,
#[serde(default)]
pub compact_request_threshold: Option<u64>,
pub overview_warning_tokens: Option<u64>,
#[serde(default)]
pub compact_retained_tokens: Option<u64>,
pub overview_deadline_tokens: Option<u64>,
#[serde(default, alias = "compact_worker_max_input_tokens")]
pub worker_context_max_tokens: Option<u64>,
#[serde(default)]
pub compact_auto_read_budget: Option<u64>,
pub finish_warning_remaining_tokens: Option<u64>,
#[serde(default)]
pub compact_worker_max_input_tokens: Option<u64>,
pub final_reserve_tokens: Option<u64>,
#[serde(default, alias = "compact_worker_max_turns")]
pub worker_max_turns: Option<u32>,
#[serde(default)]
pub compact_worker_max_turns: Option<u32>,
pub summary_target_tokens: Option<u64>,
#[serde(default)]
pub summary_max_tokens: Option<u64>,
#[serde(default, alias = "compact_auto_read_budget")]
pub auto_read_budget_tokens: Option<u64>,
#[serde(default)]
pub result_context_max_tokens: Option<u64>,
#[serde(default)]
pub model: Option<ModelManifest>,
}
@@ -386,22 +402,32 @@ impl CompactionConfigPartial {
Self {
prune_protected_tokens: upper.prune_protected_tokens.or(self.prune_protected_tokens),
prune_min_savings: upper.prune_min_savings.or(self.prune_min_savings),
compact_threshold: upper.compact_threshold.or(self.compact_threshold),
compact_request_threshold: upper
.compact_request_threshold
.or(self.compact_request_threshold),
compact_retained_tokens: upper
.compact_retained_tokens
.or(self.compact_retained_tokens),
compact_auto_read_budget: upper
.compact_auto_read_budget
.or(self.compact_auto_read_budget),
compact_worker_max_input_tokens: upper
.compact_worker_max_input_tokens
.or(self.compact_worker_max_input_tokens),
compact_worker_max_turns: upper
.compact_worker_max_turns
.or(self.compact_worker_max_turns),
threshold: upper.threshold.or(self.threshold),
request_threshold: upper.request_threshold.or(self.request_threshold),
retained_tokens: upper.retained_tokens.or(self.retained_tokens),
overview_target_tokens: upper.overview_target_tokens.or(self.overview_target_tokens),
overview_warning_tokens: upper
.overview_warning_tokens
.or(self.overview_warning_tokens),
overview_deadline_tokens: upper
.overview_deadline_tokens
.or(self.overview_deadline_tokens),
worker_context_max_tokens: upper
.worker_context_max_tokens
.or(self.worker_context_max_tokens),
finish_warning_remaining_tokens: upper
.finish_warning_remaining_tokens
.or(self.finish_warning_remaining_tokens),
final_reserve_tokens: upper.final_reserve_tokens.or(self.final_reserve_tokens),
worker_max_turns: upper.worker_max_turns.or(self.worker_max_turns),
summary_target_tokens: upper.summary_target_tokens.or(self.summary_target_tokens),
summary_max_tokens: upper.summary_max_tokens.or(self.summary_max_tokens),
auto_read_budget_tokens: upper
.auto_read_budget_tokens
.or(self.auto_read_budget_tokens),
result_context_max_tokens: upper
.result_context_max_tokens
.or(self.result_context_max_tokens),
model: merge_option(self.model, upper.model, ModelManifest::merge),
}
}
@@ -544,20 +570,42 @@ impl TryFrom<PodManifestConfig> for PodManifest {
.prune_protected_tokens
.unwrap_or(defaults::PRUNE_PROTECTED_TOKENS),
prune_min_savings: c.prune_min_savings.unwrap_or(defaults::PRUNE_MIN_SAVINGS),
compact_threshold: c.compact_threshold,
compact_request_threshold: c.compact_request_threshold,
compact_retained_tokens: c
.compact_retained_tokens
threshold: c.threshold,
request_threshold: c.request_threshold,
retained_tokens: c
.retained_tokens
.unwrap_or(defaults::COMPACT_RETAINED_TOKENS),
compact_auto_read_budget: c
.compact_auto_read_budget
.unwrap_or(defaults::COMPACT_AUTO_READ_BUDGET),
compact_worker_max_input_tokens: c
.compact_worker_max_input_tokens
overview_target_tokens: c
.overview_target_tokens
.unwrap_or(defaults::COMPACT_OVERVIEW_TARGET_TOKENS),
overview_warning_tokens: c
.overview_warning_tokens
.unwrap_or(defaults::COMPACT_OVERVIEW_WARNING_TOKENS),
overview_deadline_tokens: c
.overview_deadline_tokens
.unwrap_or(defaults::COMPACT_OVERVIEW_DEADLINE_TOKENS),
worker_context_max_tokens: c
.worker_context_max_tokens
.unwrap_or(defaults::COMPACT_WORKER_MAX_INPUT_TOKENS),
compact_worker_max_turns: c
.compact_worker_max_turns
.or(defaults::COMPACT_WORKER_MAX_TURNS),
finish_warning_remaining_tokens: c
.finish_warning_remaining_tokens
.unwrap_or(defaults::COMPACT_FINISH_WARNING_REMAINING_TOKENS),
final_reserve_tokens: c
.final_reserve_tokens
.unwrap_or(defaults::COMPACT_FINAL_RESERVE_TOKENS),
worker_max_turns: c.worker_max_turns.or(defaults::COMPACT_WORKER_MAX_TURNS),
summary_target_tokens: c
.summary_target_tokens
.unwrap_or(defaults::COMPACT_SUMMARY_TARGET_TOKENS),
summary_max_tokens: c
.summary_max_tokens
.unwrap_or(defaults::COMPACT_SUMMARY_MAX_TOKENS),
auto_read_budget_tokens: c
.auto_read_budget_tokens
.unwrap_or(defaults::COMPACT_AUTO_READ_BUDGET),
result_context_max_tokens: c
.result_context_max_tokens
.unwrap_or(defaults::COMPACT_RESULT_CONTEXT_MAX_TOKENS),
model: c.model,
})
})
@@ -984,7 +1032,7 @@ mod tests {
fn merge_option_struct_field_wise() {
let lower = PodManifestConfig {
compaction: Some(CompactionConfigPartial {
compact_threshold: Some(50_000),
threshold: Some(50_000),
prune_protected_tokens: Some(5_000),
..Default::default()
}),
@@ -992,14 +1040,14 @@ mod tests {
};
let upper = PodManifestConfig {
compaction: Some(CompactionConfigPartial {
compact_threshold: Some(80_000),
threshold: Some(80_000),
..Default::default()
}),
..Default::default()
};
let merged = lower.merge(upper);
let c = merged.compaction.unwrap();
assert_eq!(c.compact_threshold, Some(80_000));
assert_eq!(c.threshold, Some(80_000));
// field from lower retained when upper has None
assert_eq!(c.prune_protected_tokens, Some(5_000));
}
@@ -1122,27 +1170,27 @@ stop_sequences = ["\n\n", "</stop>"]
}
#[test]
fn from_toml_accepts_compact_worker_max_turns() {
fn from_toml_accepts_worker_max_turns() {
let cfg = PodManifestConfig::from_toml(
r#"
[compaction]
compact_worker_max_turns = 7
worker_max_turns = 7
"#,
)
.unwrap();
assert_eq!(cfg.compaction.unwrap().compact_worker_max_turns, Some(7));
assert_eq!(cfg.compaction.unwrap().worker_max_turns, Some(7));
}
#[test]
fn try_from_compaction_defaults_compact_worker_max_turns() {
fn try_from_compaction_defaults_worker_max_turns() {
let mut cfg = minimal_valid();
cfg.compaction = Some(CompactionConfigPartial::default());
let manifest = PodManifest::try_from(cfg).unwrap();
assert_eq!(
manifest.compaction.unwrap().compact_worker_max_turns,
manifest.compaction.unwrap().worker_max_turns,
defaults::COMPACT_WORKER_MAX_TURNS
);
}
+39 -5
View File
@@ -25,9 +25,23 @@ pub const PRUNE_MIN_SAVINGS: u64 = 4096;
/// Token budget retained (unchanged) at the tail of the history across
/// a compact. Items whose cumulative token count fits within this budget
/// starting from the end are kept verbatim; the rest are summarised.
/// See [`crate::CompactionConfig::compact_retained_tokens`].
/// See [`crate::CompactionConfig::retained_tokens`].
pub const COMPACT_RETAINED_TOKENS: u64 = 8000;
/// Target size for the deterministic compact overview/index fed to the
/// compact worker. Exceeding this target is tolerated.
/// See [`crate::CompactionConfig::overview_target_tokens`].
pub const COMPACT_OVERVIEW_TARGET_TOKENS: u64 = 8_000;
/// Warning threshold for compact overview/index size. Compaction continues.
/// See [`crate::CompactionConfig::overview_warning_tokens`].
pub const COMPACT_OVERVIEW_WARNING_TOKENS: u64 = 16_000;
/// Hard deterministic-overview deadline. When exceeded, overview generation
/// falls back to a coarser index before the compact worker is started.
/// See [`crate::CompactionConfig::overview_deadline_tokens`].
pub const COMPACT_OVERVIEW_DEADLINE_TOKENS: u64 = 40_000;
/// Default instruction asset reference used when `worker.instruction`
/// is omitted. See the `PromptLoader` prefix addressing scheme for the
/// `$insomnia/` / `$user/` / `$workspace/` namespaces.
@@ -42,19 +56,39 @@ pub const WORKER_LANGUAGE: &str =
/// session after compaction. Limits how much raw file text the
/// compact worker can pull into the compacted context via
/// `mark_read_required`. See
/// [`crate::CompactionConfig::compact_auto_read_budget`].
/// [`crate::CompactionConfig::auto_read_budget_tokens`].
pub const COMPACT_AUTO_READ_BUDGET: u64 = 8000;
/// Current prompt-occupancy cap for the compact worker's own LLM
/// calls. Exceeding this aborts the compact run (circuit-breaker
/// path). See
/// [`crate::CompactionConfig::compact_worker_max_input_tokens`].
/// path). See [`crate::CompactionConfig::worker_context_max_tokens`].
pub const COMPACT_WORKER_MAX_INPUT_TOKENS: u64 = 50_000;
/// Remaining compact-worker context threshold that triggers an instruction
/// to stop exploring and call `write_summary`.
/// See [`crate::CompactionConfig::finish_warning_remaining_tokens`].
pub const COMPACT_FINISH_WARNING_REMAINING_TOKENS: u64 = 8_000;
/// Context reserve preserved for final summary/tool closing turns.
/// See [`crate::CompactionConfig::final_reserve_tokens`].
pub const COMPACT_FINAL_RESERVE_TOKENS: u64 = 4_000;
/// Optional maximum compact-worker tool-loop depth. `None` means unlimited.
/// See [`crate::CompactionConfig::compact_worker_max_turns`].
/// See [`crate::CompactionConfig::worker_max_turns`].
pub const COMPACT_WORKER_MAX_TURNS: Option<u32> = Some(20);
/// Target size for the `write_summary` text. Used in prompt/nudge text.
/// See [`crate::CompactionConfig::summary_target_tokens`].
pub const COMPACT_SUMMARY_TARGET_TOKENS: u64 = 2_000;
/// Hard validation cap for the final `write_summary` text.
/// See [`crate::CompactionConfig::summary_max_tokens`].
pub const COMPACT_SUMMARY_MAX_TOKENS: u64 = 4_000;
/// Dry-run cap for the compacted session's initial request context.
/// See [`crate::CompactionConfig::result_context_max_tokens`].
pub const COMPACT_RESULT_CONTEXT_MAX_TOKENS: u64 = 60_000;
/// Number of recently-touched files fed to the compact worker as
/// default references.
pub const COMPACT_DEFAULT_REFERENCE_COUNT: usize = 5;
+119 -43
View File
@@ -363,8 +363,8 @@ pub struct CompactionConfig {
/// Checked by the Controller after each run. When current occupancy
/// exceeds this value, compact runs before the next turn. `None`
/// disables the between-turns check.
#[serde(default)]
pub compact_threshold: Option<u64>,
#[serde(default, alias = "compact_threshold")]
pub threshold: Option<u64>,
/// Safety-net (between-requests) compaction threshold.
///
@@ -373,32 +373,76 @@ pub struct CompactionConfig {
/// Controller can compact before the next LLM request. `None`
/// disables the between-requests check.
///
/// Expected relation: `compact_threshold < compact_request_threshold`
/// (proactive triggers before safety net). A reversed configuration
/// is accepted but logged as a warning.
#[serde(default)]
pub compact_request_threshold: Option<u64>,
/// Expected relation: `threshold < request_threshold` (proactive triggers
/// before safety net). A reversed configuration is accepted but logged as
/// a warning.
#[serde(default, alias = "compact_request_threshold")]
pub request_threshold: Option<u64>,
/// Token budget retained verbatim at the tail of the history after
/// compaction. Measured against the occupancy estimate from
/// `UsageRecord` history; turn boundaries are ignored.
#[serde(default = "default_compact_retained_tokens")]
pub compact_retained_tokens: u64,
#[serde(default = "default_retained_tokens", alias = "compact_retained_tokens")]
pub retained_tokens: u64,
/// Aggregate token budget for auto-read file contents injected into
/// the compacted session by the compact worker.
#[serde(default = "default_compact_auto_read_budget")]
pub compact_auto_read_budget: u64,
/// Target size for the deterministic overview/index fed to the compact
/// worker. Overshooting this target is not an error.
#[serde(default = "default_overview_target_tokens")]
pub overview_target_tokens: u64,
/// Warning threshold for deterministic overview/index size.
#[serde(default = "default_overview_warning_tokens")]
pub overview_warning_tokens: u64,
/// Deadline threshold for deterministic overview/index generation.
/// Oversized overviews fall back to a coarser deterministic index.
#[serde(default = "default_overview_deadline_tokens")]
pub overview_deadline_tokens: u64,
/// Current prompt-occupancy cap for the compact worker's own LLM
/// requests. Exceeding this aborts the compact run.
#[serde(default = "default_compact_worker_max_input_tokens")]
pub compact_worker_max_input_tokens: u64,
#[serde(
default = "default_worker_context_max_tokens",
alias = "compact_worker_max_input_tokens"
)]
pub worker_context_max_tokens: u64,
/// Remaining compact-worker context threshold that triggers a warning and
/// an instruction to stop exploring and call `write_summary`.
#[serde(default = "default_finish_warning_remaining_tokens")]
pub finish_warning_remaining_tokens: u64,
/// Context reserve preserved for final summary/tool closing turns.
#[serde(default = "default_final_reserve_tokens")]
pub final_reserve_tokens: u64,
/// Optional maximum compact-worker tool-loop depth. `None` leaves the
/// worker unlimited; the default bounds runaway short-context loops.
#[serde(default = "default_compact_worker_max_turns")]
pub compact_worker_max_turns: Option<u32>,
#[serde(
default = "default_worker_max_turns",
alias = "compact_worker_max_turns"
)]
pub worker_max_turns: Option<u32>,
/// Target size for the `write_summary` text. Used in prompt/nudge text.
#[serde(default = "default_summary_target_tokens")]
pub summary_target_tokens: u64,
/// Hard validation cap for the final `write_summary` text.
#[serde(default = "default_summary_max_tokens")]
pub summary_max_tokens: u64,
/// Aggregate token budget for auto-read file contents injected into
/// the compacted session by the compact worker.
#[serde(
default = "default_auto_read_budget_tokens",
alias = "compact_auto_read_budget"
)]
pub auto_read_budget_tokens: u64,
/// Dry-run cap for the compacted session's initial request context.
#[serde(default = "default_result_context_max_tokens")]
pub result_context_max_tokens: u64,
/// Optional model for the compactor (summary) LLM.
/// If omitted, the main model is cloned via `clone_boxed()`.
@@ -412,30 +456,62 @@ fn default_prune_protected_tokens() -> u64 {
fn default_prune_min_savings() -> u64 {
defaults::PRUNE_MIN_SAVINGS
}
fn default_compact_retained_tokens() -> u64 {
fn default_retained_tokens() -> u64 {
defaults::COMPACT_RETAINED_TOKENS
}
fn default_compact_auto_read_budget() -> u64 {
defaults::COMPACT_AUTO_READ_BUDGET
fn default_overview_target_tokens() -> u64 {
defaults::COMPACT_OVERVIEW_TARGET_TOKENS
}
fn default_compact_worker_max_input_tokens() -> u64 {
fn default_overview_warning_tokens() -> u64 {
defaults::COMPACT_OVERVIEW_WARNING_TOKENS
}
fn default_overview_deadline_tokens() -> u64 {
defaults::COMPACT_OVERVIEW_DEADLINE_TOKENS
}
fn default_worker_context_max_tokens() -> u64 {
defaults::COMPACT_WORKER_MAX_INPUT_TOKENS
}
fn default_compact_worker_max_turns() -> Option<u32> {
fn default_finish_warning_remaining_tokens() -> u64 {
defaults::COMPACT_FINISH_WARNING_REMAINING_TOKENS
}
fn default_final_reserve_tokens() -> u64 {
defaults::COMPACT_FINAL_RESERVE_TOKENS
}
fn default_worker_max_turns() -> Option<u32> {
defaults::COMPACT_WORKER_MAX_TURNS
}
fn default_summary_target_tokens() -> u64 {
defaults::COMPACT_SUMMARY_TARGET_TOKENS
}
fn default_summary_max_tokens() -> u64 {
defaults::COMPACT_SUMMARY_MAX_TOKENS
}
fn default_auto_read_budget_tokens() -> u64 {
defaults::COMPACT_AUTO_READ_BUDGET
}
fn default_result_context_max_tokens() -> u64 {
defaults::COMPACT_RESULT_CONTEXT_MAX_TOKENS
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
prune_protected_tokens: default_prune_protected_tokens(),
prune_min_savings: default_prune_min_savings(),
compact_threshold: None,
compact_request_threshold: None,
compact_retained_tokens: default_compact_retained_tokens(),
compact_auto_read_budget: default_compact_auto_read_budget(),
compact_worker_max_input_tokens: default_compact_worker_max_input_tokens(),
compact_worker_max_turns: default_compact_worker_max_turns(),
threshold: None,
request_threshold: None,
retained_tokens: default_retained_tokens(),
overview_target_tokens: default_overview_target_tokens(),
overview_warning_tokens: default_overview_warning_tokens(),
overview_deadline_tokens: default_overview_deadline_tokens(),
worker_context_max_tokens: default_worker_context_max_tokens(),
finish_warning_remaining_tokens: default_finish_warning_remaining_tokens(),
final_reserve_tokens: default_final_reserve_tokens(),
worker_max_turns: default_worker_max_turns(),
summary_target_tokens: default_summary_target_tokens(),
summary_max_tokens: default_summary_max_tokens(),
auto_read_budget_tokens: default_auto_read_budget_tokens(),
result_context_max_tokens: default_result_context_max_tokens(),
model: None,
}
}
@@ -592,15 +668,15 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn parse_compaction_config() {
let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\ncompact_threshold = 80000\n");
let toml = format!("{MINIMAL_REQUIRED}\n[compaction]\nthreshold = 80000\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.prune_protected_tokens, 8000);
assert_eq!(c.prune_min_savings, 4096);
assert_eq!(c.compact_threshold, Some(80000));
assert_eq!(c.compact_request_threshold, None);
assert_eq!(c.compact_retained_tokens, 8000);
assert_eq!(c.compact_worker_max_turns, Some(20));
assert_eq!(c.threshold, Some(80000));
assert_eq!(c.request_threshold, None);
assert_eq!(c.retained_tokens, 8000);
assert_eq!(c.worker_max_turns, Some(20));
}
#[test]
@@ -618,11 +694,11 @@ model_id = "claude-sonnet-4-20250514"
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[compaction]\n\
compact_worker_max_turns = 7\n"
worker_max_turns = 7\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.compact_worker_max_turns, Some(7));
assert_eq!(c.worker_max_turns, Some(7));
}
#[test]
@@ -630,13 +706,13 @@ model_id = "claude-sonnet-4-20250514"
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[compaction]\n\
compact_threshold = 80000\n\
compact_request_threshold = 90000\n"
threshold = 80000\n\
request_threshold = 90000\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.compact_threshold, Some(80000));
assert_eq!(c.compact_request_threshold, Some(90000));
assert_eq!(c.threshold, Some(80000));
assert_eq!(c.request_threshold, Some(90000));
}
#[test]
@@ -644,12 +720,12 @@ model_id = "claude-sonnet-4-20250514"
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[compaction]\n\
compact_request_threshold = 90000\n"
request_threshold = 90000\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
assert_eq!(c.compact_threshold, None);
assert_eq!(c.compact_request_threshold, Some(90000));
assert_eq!(c.threshold, None);
assert_eq!(c.request_threshold, Some(90000));
}
#[test]
@@ -657,7 +733,7 @@ model_id = "claude-sonnet-4-20250514"
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[compaction]\n\
compact_threshold = 80000\n\n\
threshold = 80000\n\n\
[compaction.model]\n\
scheme = \"gemini\"\n\
model_id = \"gemini-2.0-flash\"\n"
+1 -4
View File
@@ -281,10 +281,7 @@ mod tests {
("HOME", Some("/h")),
("XDG_RUNTIME_DIR", Some("/run/user/1000")),
]);
assert_eq!(
runtime_dir().unwrap(),
PathBuf::from("<runtime-dir>")
);
assert_eq!(runtime_dir().unwrap(), PathBuf::from("<runtime-dir>"));
}
#[test]