diff --git a/crates/hi-agent/src/steering/implementation.rs b/crates/hi-agent/src/steering/implementation.rs index cf3387b2..e06c0426 100644 --- a/crates/hi-agent/src/steering/implementation.rs +++ b/crates/hi-agent/src/steering/implementation.rs @@ -182,8 +182,8 @@ fn tool_result_is_failure(output: &str) -> bool { let trimmed = output.trim_start(); trimmed.starts_with("Error:") || trimmed.starts_with("⚠ refused:") - || trimmed.contains("[exit code ") - || trimmed.contains("[timed out") + || trimmed.contains(hi_tools::markers::EXIT_CODE_PREFIX) + || trimmed.contains(hi_tools::markers::TIMED_OUT_PREFIX) } fn filesystem_mutation_result_landed(name: &str, output: &str) -> bool { diff --git a/crates/hi-tools/src/lib.rs b/crates/hi-tools/src/lib.rs index 5225a724..59624cf6 100644 --- a/crates/hi-tools/src/lib.rs +++ b/crates/hi-tools/src/lib.rs @@ -140,6 +140,31 @@ pub enum TruncationState { }, } +/// Model-facing tool-result markers. Steering (hi-agent) string-matches on +/// these, so emitters and matchers must share one definition — a matcher +/// drifting from the emitted text is exactly the bug class that left the +/// timed-out matcher dead ("[timed out after " vs the emitted string). +/// Tests deliberately keep verbatim literals as canaries: changing a marker +/// must be a conscious, test-breaking decision. +pub mod markers { + /// Fallback for an empty result whose status carries no exit code. + pub const NO_OUTPUT: &str = "[no output]"; + /// Empty stdout+stderr on a successful command. + pub const NO_OUTPUT_SUCCESS: &str = "[no output — command succeeded (exit 0)]"; + /// Successful command whose only output went to stderr. + pub const STDERR_ONLY_SUCCESS: &str = "[command succeeded (exit 0) — output above is stderr]"; + /// Prefix of every failure exit-code annotation. Success annotations must + /// never contain it — steering classifies on this prefix. + pub const EXIT_CODE_PREFIX: &str = "[exit code "; + /// Prefix shared by all timeout annotations. + pub const TIMED_OUT_PREFIX: &str = "[timed out"; + /// The full timeout annotation. + pub const TIMED_OUT_KILLED: &str = "[timed out — process killed]"; + /// Just-in-time reminder appended to successful commands containing `cd`. + pub const CWD_RESET_NOTE: &str = + "[note: the working directory resets to the workspace root before the next command]"; +} + /// The result of a tool call, split into `content` shown to the model and an /// optional richer `display` for the UI (e.g. a colored diff). This keeps /// edit/write feedback terse for the model while showing the user what changed. diff --git a/crates/hi-tools/src/process.rs b/crates/hi-tools/src/process.rs index 65fdbb03..1aa6759e 100644 --- a/crates/hi-tools/src/process.rs +++ b/crates/hi-tools/src/process.rs @@ -62,10 +62,11 @@ impl ProcessExecution { } if stdout_empty && stderr_empty { out.push_str(&format!( - "[exit code {code} — no output on stdout or stderr]" + "{}{code} — no output on stdout or stderr]", + crate::markers::EXIT_CODE_PREFIX )); } else { - out.push_str(&format!("[exit code {code}]")); + out.push_str(&format!("{}{code}]", crate::markers::EXIT_CODE_PREFIX)); } } } @@ -73,25 +74,25 @@ impl ProcessExecution { if !out.is_empty() && !out.ends_with('\n') { out.push('\n'); } - out.push_str("[timed out — process killed]"); + out.push_str(crate::markers::TIMED_OUT_KILLED); } // Empty and stderr-only results are where models form false // premises ("did it work?" / "stderr means it failed") — state // the verdict explicitly instead of leaving it implied. ToolStatus::Succeeded => { if stdout_empty && stderr_empty { - out.push_str("[no output — command succeeded (exit 0)]"); + out.push_str(crate::markers::NO_OUTPUT_SUCCESS); } else if stdout_empty { if !out.ends_with('\n') { out.push('\n'); } - out.push_str("[command succeeded (exit 0) — output above is stderr]"); + out.push_str(crate::markers::STDERR_ONLY_SUCCESS); } } _ => {} } if out.is_empty() { - out.push_str("[no output]"); + out.push_str(crate::markers::NO_OUTPUT); } out } diff --git a/crates/hi-tools/src/tools.rs b/crates/hi-tools/src/tools.rs index ff76629c..91ad3d86 100644 --- a/crates/hi-tools/src/tools.rs +++ b/crates/hi-tools/src/tools.rs @@ -1982,10 +1982,7 @@ async fn run_bash_tool( if !content.ends_with('\n') { content.push('\n'); } - content.push_str( - "[note: the working directory resets to the workspace root before the \ - next command]", - ); + content.push_str(crate::markers::CWD_RESET_NOTE); } let mut outcome = ToolOutcome::plain(content); outcome.status = execution.status;