subagent: drop the turn count from a NeedHelp status

A NeedHelp status is waiting for an answer; `Turn 2 of 5` was
competing with the thing the reader actually needs, which is what it
asked for. Cut it at the describe_stopped call site (it already
formats a distinct sentence per stop reason) rather than in
describe_turns/describe_status, so TurnCap — where the count is the
whole point — and the running-turn case are untouched.

Refs #4415

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
atlas 2026-09-15 16:37:05 +02:00
commit 62a2122800

View file

@ -1738,8 +1738,8 @@ fn describe_stopped(name: &str, stop: &StopReason, turns: &str) -> String {
StopReason::NeedHelp(msg) => format!( StopReason::NeedHelp(msg) => format!(
"subagent `{name}` is BLOCKED and needs help: {msg}. It called `need_help`, which \ "subagent `{name}` is BLOCKED and needs help: {msg}. It called `need_help`, which \
stopped its goal continuation, and nothing is in flight it stays blocked until you \ stopped its goal continuation, and nothing is in flight it stays blocked until you \
answer it.{turns} `continue` is how you answer: give it what it asked for as the \ answer it. `continue` is how you answer: give it what it asked for as the next \
next turn's prompt." turn's prompt."
), ),
StopReason::TurnCap { turns: spent } => format!( StopReason::TurnCap { turns: spent } => format!(
"subagent `{name}` ran out of turns — the harness limit of {spent} was reached and it \ "subagent `{name}` ran out of turns — the harness limit of {spent} was reached and it \
@ -2884,8 +2884,9 @@ mod tests {
"the block and its reason must both be in the answer: {blocked}" "the block and its reason must both be in the answer: {blocked}"
); );
assert!( assert!(
blocked.contains("Turn 2 of 5"), !blocked.contains("Turn 2 of 5"),
"with the progress that says how far it got: {blocked}" "a subagent waiting on an answer needs the answer, not a turn count competing for \
attention: {blocked}"
); );
assert!( assert!(
!blocked.contains("idle"), !blocked.contains("idle"),
@ -2893,6 +2894,23 @@ mod tests {
); );
} }
#[test]
fn turn_cap_status_still_carries_the_turn_count() {
// The turn count is the whole point of a `TurnCap` stop — omitting
// it here (unlike `NeedHelp`) would be the regression a later
// refactor could silently reintroduce.
let capped = describe_status(&StatusFacts {
stop: Some(StopReason::TurnCap { turns: 5 }),
turns: Some((5, 5)),
..StatusFacts::new("n")
})
.expect("a capped session is a state, not an error");
assert!(
capped.contains("Turn 5 of 5"),
"the turn count is the point of a TurnCap status: {capped}"
);
}
#[test] #[test]
fn a_reported_goal_never_reads_as_a_verified_one() { fn a_reported_goal_never_reads_as_a_verified_one() {
// The failure this is built against: `goal_reached` is self-reported // The failure this is built against: `goal_reached` is self-reported