Skip to content
Draft
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
7 changes: 6 additions & 1 deletion crates/libsy/src/algorithms/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use super::util::stage::{
DecisionSource, HandoffNoteConfig, PickerMode, StageClassifier, StageTargets, Tier,
fall_open_tier, record_decision_source, record_routing_decision,
};
use super::util::tool_signals::{DEFAULT_RECENT_WINDOW, ToolSignalProcessor};
use super::util::tool_signals::{DEFAULT_RECENT_WINDOW, ToolSemantics, ToolSignalProcessor};
use crate::core::algorithm::{Algorithm, Driver};
use crate::core::classifier::{Classification, Classifier, Score};
use crate::core::state::State;
Expand Down Expand Up @@ -108,6 +108,8 @@ pub struct StageRouterConfig {
/// Trailing tool results the signals are computed over. `None` uses
/// [`DEFAULT_RECENT_WINDOW`].
pub recent_window: Option<usize>,
/// Exact tool-name semantics added to the built-in coding vocabulary.
pub tool_semantics: ToolSemantics,
/// Note handed to the model on a signal-driven escalation, and on a
/// hand-back to the efficient tier when a de-escalation note is configured.
pub handoff_notes: Option<HandoffNoteConfig>,
Expand All @@ -128,6 +130,7 @@ impl StageRouterConfig {
mode,
confidence_threshold,
recent_window: None,
tool_semantics: ToolSemantics::default(),
handoff_notes: None,
tier_prompts: TargetPrompts::default(),
llm_fallback: None,
Expand Down Expand Up @@ -185,6 +188,7 @@ pub(crate) fn build_stage_route(
),
});
}
config.tool_semantics.validate()?;
// The tiers are a fixed pair; their targets are whatever the deployment calls
// them, and the classifier scores onto those names.
let targets = StageTargets::new(capable.clone(), efficient.clone());
Expand All @@ -200,6 +204,7 @@ pub(crate) fn build_stage_route(
}
let signals = ToolSignalProcessor {
recent_window: config.recent_window.unwrap_or(DEFAULT_RECENT_WINDOW),
tool_semantics: config.tool_semantics,
};
let target_set = vec![capable.clone(), efficient.clone()];
let mut router = FallThrough::<State>::new_with_state(target_set)
Expand Down
24 changes: 20 additions & 4 deletions crates/libsy/src/algorithms/util/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,9 @@ impl ScoreResult {
pub struct CodingAgentDimensions {
/// Windowed max error severity in `[0, 1]`.
pub severity: f64,
/// `1.0` when a deep turn has no reads, plans, writes, or edits (pure churn).
/// `1.0` when a deep turn has no recognized observation, mutation, plan, or new activity.
pub spinning: f64,
/// `1.0` when a deep turn reads/plans but does not write or edit.
/// `1.0` when a deep turn observes/plans but does not mutate or show new activity.
pub exploring: f64,
/// Fraction of recent tool ops that produced code (writes + edits).
pub production_intensity: f64,
Expand Down Expand Up @@ -302,10 +302,11 @@ pub fn dimensions_from_signal(signal: &ToolSignals) -> CodingAgentDimensions {
let deep_enough = signal.turn_depth >= STALL_MIN_TURN_DEPTH;
let no_production = signal.recent_write_count == 0 && signal.recent_edit_count == 0;
let investigating = signal.recent_read_count >= 1 || signal.recent_todowrite_count >= 1;
let new_activity = signal.recent_new_count >= 1;
// spinning vs exploring partition the "not producing" case by investigative
// activity, so at most one fires — no double-counting on the production axis.
let spinning = deep_enough && no_production && !investigating;
let exploring = deep_enough && no_production && investigating;
let spinning = deep_enough && no_production && !investigating && !new_activity;
let exploring = deep_enough && no_production && investigating && !new_activity;

CodingAgentDimensions {
severity: f64::from(signal.severity),
Expand Down Expand Up @@ -735,6 +736,21 @@ mod tests {
}
}

#[test]
fn new_activity_suppresses_stall_dimensions_without_biasing_the_score() {
let signal = ToolSignals {
turn_depth: STALL_MIN_TURN_DEPTH,
recent_new_count: 1,
..Default::default()
};

let dimensions = dimensions_from_signal(&signal);

assert_eq!(dimensions.spinning, 0.0);
assert_eq!(dimensions.exploring, 0.0);
assert_eq!(score_signal(&signal).score, 0.0);
}

// ─── StageClassifier ─────────────────────────────────────────────────

/// Tiers named the way a deployment would name them.
Expand Down
Loading