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
36 changes: 36 additions & 0 deletions crates/libsy/src/algorithms/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ pub struct StageRouterConfig {
pub recent_window: Option<usize>,
/// Exact tool-name semantics added to the built-in coding vocabulary.
pub tool_semantics: ToolSemantics,
/// Escalate on context compaction. Enabled by default.
pub compaction_override: bool,
/// Requests to keep on capable after an escalation. `0` disables the hold;
/// `-1` pins after switching to this tier.
pub capable_hold_turns: i64,
Expand Down Expand Up @@ -191,6 +193,7 @@ impl StageRouterConfig {
capable_first_threshold: 0.0,
recent_window: None,
tool_semantics: ToolSemantics::default(),
compaction_override: true,
capable_hold_turns: 2,
efficient_hold_turns: None,
handoff_notes: None,
Expand Down Expand Up @@ -285,6 +288,7 @@ pub(crate) fn build_stage_route(

let mut classifier = StageClassifier::new(config.mode, config.confidence_threshold)
.with_capable_first_threshold(config.capable_first_threshold)
.with_compaction_override(config.compaction_override)
.with_capable_hold_turns(config.capable_hold_turns);
classifier.reset_on_user_turn = reset_on_user_turn;
if let Some(turns) = config.efficient_hold_turns {
Expand Down Expand Up @@ -535,6 +539,38 @@ mod tests {
request
}

/// Disabling compaction escalation preserves normal signals in both pickers.
#[tokio::test]
async fn compaction_override_can_be_disabled() -> Result<()> {
for mode in [PickerMode::EfficientFirst, PickerMode::CapableFirst] {
for enabled in [true, false] {
let mut config = StageRouterConfig::new(mode, 0.5);
assert!(config.compaction_override);
config.compaction_override = enabled;
config.capable_hold_turns = 0;
let router = recording_router(config)?;
let recorder = Arc::new(Recorder::default());
for mut request in [production_request(), turn_request(true)] {
request.llm_request.has_compaction = true;
test_drive_with_models(
router.clone(),
request,
runtime_models(),
recorder.serve(),
)
.await?;
}
let actual: Vec<_> = recorder
.routed()
.into_iter()
.map(|call| call.target)
.collect();
assert_eq!(actual, [if enabled { "strong" } else { "weak" }, "strong"]);
}
}
Ok(())
}

#[tokio::test]
async fn standalone_pickers_default_to_no_efficient_hold() -> Result<()> {
for mode in [PickerMode::EfficientFirst, PickerMode::CapableFirst] {
Expand Down
11 changes: 10 additions & 1 deletion crates/libsy/src/algorithms/util/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ pub struct StageClassifier {
mode: PickerMode,
confidence_threshold: f64,
capable_first_threshold: f64,
compaction_override: bool,
capable_hold_turns: i64,
efficient_hold_turns: Option<i64>,
pub(crate) reset_on_user_turn: bool,
Expand All @@ -544,6 +545,7 @@ impl StageClassifier {
mode,
confidence_threshold,
capable_first_threshold: 0.0,
compaction_override: true,
capable_hold_turns: 2,
efficient_hold_turns: None,
reset_on_user_turn: false,
Expand All @@ -558,6 +560,12 @@ impl StageClassifier {
self
}

/// Whether context compaction forces the capable tier. Enabled by default.
pub fn with_compaction_override(mut self, enabled: bool) -> Self {
self.compaction_override = enabled;
self
}

/// Keep capable for this many requests after an escalation. Zero disables
/// it; `-1` pins after a switch. Validated by the enclosing stage router.
pub fn with_capable_hold_turns(mut self, turns: i64) -> Self {
Expand Down Expand Up @@ -750,11 +758,12 @@ impl Classifier for StageClassifier {
));
}
}
let Some(signal) = state.tool_signals.clone() else {
let Some(mut signal) = state.tool_signals.clone() else {
// No tool activity yet — nothing to score, so the signals have no
// opinion, same as a below-threshold turn.
return Ok((Self::abstain(), None));
};
signal.compacted &= self.compaction_override;
let (mode, threshold) = match fall_open_tier(state) {
Some(Tier::Capable) => (PickerMode::CapableFirst, self.capable_first_threshold),
Some(Tier::Efficient) => (PickerMode::EfficientFirst, self.confidence_threshold),
Expand Down
11 changes: 11 additions & 0 deletions crates/switchyard-runner/src/algorithm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ pub struct StageSettingsConfig {
pub confidence_threshold: Option<f64>,
pub recent_turn_window: Option<usize>,
pub tool_semantics: Option<ToolSemantics>,
pub compaction_override: Option<bool>,
pub capable_hold_turns: Option<i64>,
pub efficient_hold_turns: Option<i64>,
pub handoff_notes: Option<HandoffNoteConfig>,
Expand All @@ -678,6 +679,10 @@ impl StageSettingsConfig {
let mut config =
StageRouterConfig::new(mode, self.confidence_threshold.unwrap_or(threshold));
config.recent_window = self.recent_turn_window.or(shared.recent_turn_window);
config.compaction_override = self
.compaction_override
.or(shared.compaction_override)
.unwrap_or(true);
config.tool_semantics = self
.tool_semantics
.as_ref()
Expand Down Expand Up @@ -2281,6 +2286,7 @@ mod tests {
assert_eq!(config.mode, picker);
assert_eq!(config.confidence_threshold, threshold);
assert_eq!(config.recent_window, Some(3));
assert!(config.compaction_override);
assert_eq!(config.capable_hold_turns, capable_hold);
assert_eq!(config.efficient_hold_turns, Some(efficient_hold));
assert_eq!(config.capable_system_prompt.as_deref(), prompt);
Expand All @@ -2293,6 +2299,7 @@ mod tests {
0.5,
);
assert_eq!(standalone.capable_hold_turns, 2);
assert!(standalone.compaction_override);
assert!(standalone.capable_system_prompt.is_none());
Ok(())
}
Expand All @@ -2306,6 +2313,7 @@ efficient_target = "weak"
efficient_first_threshold = 0.7
capable_first_threshold = 0.2
recent_turn_window = 5
compaction_override = false
capable_hold_turns = 4
efficient_hold_turns = 1
capable_system_prompt = "Shared plan"
Expand All @@ -2315,6 +2323,7 @@ capable_system_prompt = "Shared plan"
let shared = stage.profile_config(&StageProfileConfig::default(), PickerMode::CapableFirst);
assert_eq!(shared.confidence_threshold, 0.2);
assert_eq!(shared.recent_window, Some(5));
assert!(!shared.compaction_override);
assert_eq!(shared.capable_hold_turns, 4);
assert_eq!(shared.efficient_hold_turns, Some(1));
assert_eq!(shared.capable_system_prompt.as_deref(), Some("Shared plan"));
Expand All @@ -2328,6 +2337,7 @@ capable_system_prompt = "Shared plan"
r#"
confidence_threshold = 0.0
recent_turn_window = 2
compaction_override = true
capable_hold_turns = 0
efficient_hold_turns = 0
capable_system_prompt = ""
Expand All @@ -2337,6 +2347,7 @@ capable_system_prompt = ""
let config = stage.profile_config(&profile, PickerMode::CapableFirst);
assert_eq!(config.confidence_threshold, 0.0);
assert_eq!(config.recent_window, Some(2));
assert!(config.compaction_override);
assert_eq!(config.capable_hold_turns, 0);
assert_eq!(config.efficient_hold_turns, Some(0));
assert_eq!(config.capable_system_prompt.as_deref(), Some(""));
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/toml_schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ the signals work and for tuning.
| `capable_system_prompt` | No | unset | System prompt prepended for capable requests. |
| `efficient_system_prompt` | No | unset | System prompt prepended for efficient requests. |
| `recent_turn_window` | No | `3` | Trailing tool results the signals are computed over. |
| `compaction_override` | No | `true` | Context compaction forces capable. Set `false` to use the other signals and picker normally. |
| `capable_hold_turns` | No | `2` | Requests kept on capable after a switch. Signal-driven capable decisions also start finite holds. Active holds skip signal processing and classification; `0` disables; `-1` pins after a switch. |
| `efficient_hold_turns` | No | `0` | Requests kept on efficient after a switch, even when errors appear. `0` disables; `-1` pins after a switch. |
| `tool_semantics.observe` | No | `[]` | Exact ASCII case-insensitive domain tool names that count as read-only investigation. |
Expand Down Expand Up @@ -591,6 +592,7 @@ confidence threshold. Tool signals still drive escalation and recovery. See
| `stage.efficient_first_threshold` | No | `0.4` | Signal confidence required after an Efficient classification. In `[0, 1]`. Also accepts `confidence_threshold`. |
| `stage.capable_first_threshold` | No | `0.0` | Signal confidence required after a Capable classification. In `[0, 1]`. |
| `stage.recent_turn_window` | No | `3` | Trailing tool results the signals are computed over. |
| `stage.compaction_override` | No | `true` | Same compaction override as `stage_router`. Can also be set per `efficient` or `capable` profile. |
| `stage.capable_hold_turns` | No | CF: `0`, EF: `2` | Same capable hold as `stage_router`: `0` disables; `-1` pins after a switch. |
| `stage.efficient_hold_turns` | No | CF: `-1`; EF: `0` | Defaults to pinning efficient after a switch when the classifier selected capable-first. Explicit `0` disables; positive values hold that many requests. |
| `stage.tool_semantics.observe` | No | `[]` | Additional exact ASCII case-insensitive tool names that count as observation. |
Expand Down
4 changes: 3 additions & 1 deletion docs/routing_algorithms/stage_router_routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ The axes are **corroborative**: the signed score is `tanh`-squashed to a
confidence in `[0, 1]`. One maxed scoring dimension produces about `0.46`;
corroborating evidence pushes confidence decisively past a `0.5` threshold.
Repeated failures, critical-error severity, and context compaction are hard
overrides to the capable tier. Active holds bypass signal extraction and scoring.
overrides to the capable tier. Set `compaction_override = false` to disable the
compaction override in either picker. Active holds bypass signal extraction and scoring.
Severity and test results come from tool output that ran something. The contents
returned by the built-in read and search tools do not count, unless the tool
reports a failure.
Expand Down Expand Up @@ -253,6 +254,7 @@ required columns.
| `capable_system_prompt` | unset | System prompt prepended for capable requests. |
| `efficient_system_prompt` | unset | System prompt prepended for efficient requests. |
| `recent_turn_window` | `3` | Trailing tool results the signals are computed over. |
| `compaction_override` | `true` | Context compaction forces capable. Set `false` to use the other signals and picker normally. |
| `capable_hold_turns` | `2` | Requests kept on capable after switching to it; signal-driven capable decisions also start finite holds. `0` disables; `-1` pins after a switch. |
| `efficient_hold_turns` | `0` | Requests kept on efficient after switching to it. `0` disables; `-1` pins after a switch. |
| `tool_semantics.observe` | `[]` | Domain tool names that count as investigation. |
Expand Down
Loading