diff --git a/crates/libsy/src/algorithms/util/stage.rs b/crates/libsy/src/algorithms/util/stage.rs index 2338f7b01..fe7dee0c3 100644 --- a/crates/libsy/src/algorithms/util/stage.rs +++ b/crates/libsy/src/algorithms/util/stage.rs @@ -103,7 +103,7 @@ impl Tier { #[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PickerMode { - /// Default to capable unless the scorer confidently picks efficient. + /// Default to capable until the first edit or write, then default to efficient. CapableFirst, /// Default to efficient unless the scorer confidently picks capable. EfficientFirst, @@ -168,6 +168,8 @@ pub enum DecisionSource { Override, /// A recent escalation is being held on the capable tier. CapableHold, + /// Capable-first handed execution off after the first edit or write. + Mutation, /// Scorer crossed `confidence_threshold`. Dimensions, /// Scorer was not confident, so the signals did not decide this turn. @@ -184,6 +186,7 @@ impl DecisionSource { match self { Self::Override => "override", Self::CapableHold => "capable_hold", + Self::Mutation => "mutation", Self::Dimensions => "dimensions", Self::Ambiguous => "ambiguous", Self::LlmClassifier => "llm-classifier", @@ -385,7 +388,7 @@ fn override_reason(signal: &ToolSignals) -> Option { /// /// 1. **Escalate** — repeated failure, critical error, or compaction. /// 2. **Scorer** — no hard reason, so weigh the two axes; if confident, follow it. -/// 3. **Fall open** — not confident: hand to the classifier, else the default. +/// 3. **Fall open** — capable-first mutations go efficient; otherwise use the fallback. /// /// Deterministic and pure: the async classifier lives in the caller, so rule 3 /// returns [`PickOutcome::ConsultClassifier`] instead of calling it here. The @@ -415,6 +418,12 @@ pub fn pick_tier(signal: &ToolSignals, mode: PickerMode, confidence_threshold: f ); } + // A mutation changes capable-first's undecided default to efficient. + if matches!(mode, PickerMode::CapableFirst) && (signal.edit_count > 0 || signal.write_count > 0) + { + return resolved(Tier::Efficient, DecisionSource::Mutation, 0.0, Some(1.0)); + } + // 3. Fall open — the signals didn't corroborate enough to be sure. Hand off // to the caller's classifier; with none, land on the picker's default. PickOutcome::ConsultClassifier { @@ -825,6 +834,29 @@ mod tests { assert_eq!(PickerMode::EfficientFirst.default_tier(), Tier::Efficient); } + #[test] + fn capable_first_hands_off_after_mutation() { + for signal in [ + ToolSignals { + edit_count: 1, + ..Default::default() + }, + ToolSignals { + write_count: 1, + ..Default::default() + }, + ] { + assert!(matches!( + pick_tier(&signal, PickerMode::CapableFirst, 0.5), + PickOutcome::Resolved { + tier: Tier::Efficient, + source: DecisionSource::Mutation, + .. + } + )); + } + } + #[test] fn quiet_signal_falls_open_to_default() { let signal = signal_from(json!([{"role": "user", "content": "hi"}])); diff --git a/crates/libsy/src/algorithms/util/tool_signals.rs b/crates/libsy/src/algorithms/util/tool_signals.rs index ba469cfdf..f1efc59a5 100644 --- a/crates/libsy/src/algorithms/util/tool_signals.rs +++ b/crates/libsy/src/algorithms/util/tool_signals.rs @@ -13,6 +13,7 @@ #![allow(dead_code)] use std::path::Path; +use std::sync::LazyLock; use async_trait::async_trait; use serde::Deserialize; @@ -201,6 +202,17 @@ static BASH_TOOL_NAMES: &[&str] = &[ "exec_command", // codex ]; +// Codex unified-exec wraps its ordinary tools in a JavaScript program carried +// by one custom `exec` call. Its inner tool calls retain their normal semantics. +static CODE_MODE_TOOL_NAMES: &[&str] = &["exec"]; + +static CODE_MODE_EXEC_COMMAND: LazyLock> = LazyLock::new(|| { + regex::Regex::new( + r#"tools\.exec_command\s*\(\s*\{\s*(?:"cmd"|cmd)\s*:\s*(?:"(?P(?:\\.|[^"\\])*)"|'(?P(?:\\.|[^'\\])*)'|`(?P