Skip to content
Open
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
4 changes: 2 additions & 2 deletions crates/hi-agent/src/steering/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions crates/hi-tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 7 additions & 6 deletions crates/hi-tools/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,36 +62,37 @@ 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));
}
}
}
ToolStatus::TimedOut => {
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
}
Expand Down
5 changes: 1 addition & 4 deletions crates/hi-tools/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading