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
36 changes: 34 additions & 2 deletions crates/libsy/src/algorithms/util/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl Tier {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See PR comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do! The comments are valid, the string patterns we match have possible leaks, do you know if Relay event streams have normalized tool responses ? Would it get easier if we used their schema to normalize the IR with ATOF format?

#[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,
Expand Down Expand Up @@ -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.
Expand All @@ -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",
Expand Down Expand Up @@ -385,7 +388,7 @@ fn override_reason(signal: &ToolSignals) -> Option<OverrideReason> {
///
/// 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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"}]));
Expand Down
175 changes: 157 additions & 18 deletions crates/libsy/src/algorithms/util/tool_signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#![allow(dead_code)]

use std::path::Path;
use std::sync::LazyLock;

use async_trait::async_trait;
use serde::Deserialize;
Expand Down Expand Up @@ -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<Option<regex::Regex>> = LazyLock::new(|| {
regex::Regex::new(
r#"tools\.exec_command\s*\(\s*\{\s*(?:"cmd"|cmd)\s*:\s*(?:"(?P<double>(?:\\.|[^"\\])*)"|'(?P<single>(?:\\.|[^'\\])*)'|`(?P<template>(?:\\.|[^`\\])*)`)"#,
)
.ok()
});

// Prefer false negatives: tests_passed clears a capable hold, so a false positive
// could hand an unfinished task back too early.
static TEST_PASS_PHRASES: &[&str] = &[
Expand Down Expand Up @@ -480,30 +492,31 @@ fn classify_tool_call_with_semantics(
if PLAN_TOOL_NAMES.contains(&lower.as_str()) {
return ToolSemantic::Plan;
}
if BASH_TOOL_NAMES.contains(&lower.as_str())
&& let Some(cmd) = command
if CODE_MODE_TOOL_NAMES.contains(&lower.as_str())
&& let Some(input) = command
{
// Write/edit redirection trumps read-like operands.
if BASH_WRITE_PATTERNS.iter().any(|p| cmd.contains(p)) || shell_command_is_write(cmd) {
return ToolSemantic::Mutate(MutationKind::Write);
}
if cmd.contains("python") && PYTHON_WRITE_PATTERNS.iter().any(|p| cmd.contains(p)) {
return ToolSemantic::Mutate(MutationKind::Write);
}
if shell_invokes_program(cmd, "node")
&& JAVASCRIPT_WRITE_PATTERNS
.iter()
.any(|pattern| cmd.contains(pattern))
{
return ToolSemantic::Mutate(MutationKind::Write);
}
if BASH_EDIT_PATTERNS.iter().any(|p| cmd.contains(p)) || shell_command_is_edit(cmd) {
if javascript_calls_tool(input, "tools.apply_patch") {
return ToolSemantic::Mutate(MutationKind::Edit);
}
if BASH_READ_PATTERNS.iter().any(|p| cmd.contains(p)) || shell_command_is_read(cmd) {

let mut observed = false;
for shell_command in javascript_exec_commands(input) {
match classify_shell_command(&shell_command) {
Some(mutation @ ToolSemantic::Mutate(_)) => return mutation,
Some(ToolSemantic::Observe) => observed = true,
_ => {}
}
}
if observed {
return ToolSemantic::Observe;
}
}
if BASH_TOOL_NAMES.contains(&lower.as_str())
&& let Some(cmd) = command
&& let Some(semantic) = classify_shell_command(cmd)
{
return semantic;
}
semantics.classify(name).unwrap_or(ToolSemantic::Unknown)
}

Expand All @@ -515,6 +528,78 @@ fn is_builtin_tool_name(lower: &str) -> bool {
|| BASH_TOOL_NAMES.contains(&lower)
}

fn classify_shell_command(command: &str) -> Option<ToolSemantic> {
// Write/edit redirection trumps read-like operands.
if BASH_WRITE_PATTERNS.iter().any(|p| command.contains(p)) || shell_command_is_write(command) {
return Some(ToolSemantic::Mutate(MutationKind::Write));
}
if command.contains("python") && PYTHON_WRITE_PATTERNS.iter().any(|p| command.contains(p)) {
return Some(ToolSemantic::Mutate(MutationKind::Write));
}
if shell_invokes_program(command, "node")
&& JAVASCRIPT_WRITE_PATTERNS
.iter()
.any(|pattern| command.contains(pattern))
{
return Some(ToolSemantic::Mutate(MutationKind::Write));
}
if BASH_EDIT_PATTERNS.iter().any(|p| command.contains(p)) || shell_command_is_edit(command) {
return Some(ToolSemantic::Mutate(MutationKind::Edit));
}
if BASH_READ_PATTERNS.iter().any(|p| command.contains(p)) || shell_command_is_read(command) {
return Some(ToolSemantic::Observe);
}
None
}

/// Returns whether JavaScript invokes `tool`, ignoring occurrences inside strings.
fn javascript_calls_tool(source: &str, tool: &str) -> bool {
let bytes = source.as_bytes();
let tool = tool.as_bytes();
let mut quote = None;
let mut escaped = false;
let mut index = 0usize;

while index < bytes.len() {
let byte = bytes[index];
if escaped {
escaped = false;
} else if quote.is_some() && byte == b'\\' {
escaped = true;
} else if quote == Some(byte) {
quote = None;
} else if quote.is_none() && matches!(byte, b'\'' | b'"' | b'`') {
quote = Some(byte);
} else if quote.is_none() && bytes[index..].starts_with(tool) {
let mut next = index + tool.len();
while bytes.get(next).is_some_and(u8::is_ascii_whitespace) {
next += 1;
}
if bytes.get(next) == Some(&b'(') {
return true;
}
}
index += 1;
}
false
}

/// Extracts literal `cmd` values from Codex's `tools.exec_command({...})` calls.
fn javascript_exec_commands(source: &str) -> Vec<String> {
let Some(pattern) = CODE_MODE_EXEC_COMMAND.as_ref() else {
return Vec::new();
};
pattern
.captures_iter(source)
.filter_map(|captures| {
["double", "single", "template"]
.into_iter()
.find_map(|name| captures.name(name))
.map(|value| value.as_str().to_lowercase())
})
.collect()
}

/// Split a shell line at unquoted command separators. This intentionally avoids
/// pretending to be a full shell parser; only the leading program and flags of
/// each segment are inspected below.
Expand Down Expand Up @@ -1587,6 +1672,17 @@ mod tests {
}
}

fn unified_exec(input: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentBlock::ToolCall(ToolCall {
id: String::new(),
name: "exec".to_string(),
arguments: json!({"input": input}),
})],
}
}

#[test]
fn codex_exec_command_is_classified() {
// arguments arrive as a JSON string, with the command under `cmd`
Expand All @@ -1598,6 +1694,49 @@ mod tests {
);
}

#[test]
fn codex_unified_exec_apply_patch_counts_as_an_edit() {
let request = with_messages(vec![
unified_exec(
"const patch = `*** Begin Patch`;
text(await tools.apply_patch(patch));",
),
tr("Done!"),
]);
let signal = ToolSignals::from_request(&request, None);
assert_eq!(signal.edit_count, 1);
assert_eq!(signal.recent_edit_count, 1);
}

#[test]
fn codex_unified_exec_inline_shell_mutation_is_classified() {
let request = with_messages(vec![
unified_exec(
r#"const r = await tools.exec_command({cmd:"gofmt -w src/main.go",workdir:"/app"});
text(r.output);"#,
),
tr("ok"),
]);
assert_eq!(
ToolSignals::from_request(&request, None).recent_edit_count,
1
);
}

#[test]
fn codex_unified_exec_search_for_apply_patch_is_observation() {
let request = with_messages(vec![
unified_exec(
r#"const r = await tools.exec_command({cmd:"rg -n 'tools.apply_patch(' src"});
text(r.output);"#,
),
tr("match"),
]);
let signal = ToolSignals::from_request(&request, None);
assert_eq!(signal.edit_count, 0);
assert_eq!(signal.read_count, 1);
}

#[test]
fn python_write_expressions_need_a_python_command() {
let write = with_messages(vec![
Expand Down
Loading