fix(flows): circuit-breaker counts validate/dry_run body-level ok:false failures - #4911
Conversation
…se failures RepeatedToolFailureMiddleware keyed its no-progress/repeated-failure detection only on result.error.is_some(), but validate_workflow and dry_run_workflow report an invalid graph or aborted sandbox run via ToolResult::success with a JSON body carrying top-level "ok": false — so the breaker never saw a failure and burned the full extended 50-iteration budget on a graph it could never fix, stopping only at the 600s wall clock with no useful root cause. Add is_body_level_failure(name, content), scoped to exactly these two tool names, and fold it into the failure signal fed to the crate's NoProgressTracker so a repeated body-level ok:false trips the same nudge/halt ladder as a real tool error.
📝 WalkthroughWalkthrough
ChangesWorkflow failure tracking
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/tinyagents/middleware.rs`:
- Around line 2441-2447: Add a tracing::debug! event in the body_level_failure
detection branch, logging a grep-friendly message and result.name while
excluding result.content. Place it before the existing nudge/halt handling so
the ok:false transition is recorded when is_body_level_failure returns true.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c63148ec-9ed7-4049-95f3-5280d491207c
📒 Files selected for processing (1)
src/openhuman/tinyagents/middleware.rs
| // Body-level failure signal: `validate_workflow` / `dry_run_workflow` | ||
| // report an invalid graph via a `success` result whose JSON body carries | ||
| // `"ok": false` — see `is_body_level_failure`. Only meaningful when | ||
| // `result.error` is `None`; when both are set, `result.error` already | ||
| // drives every check below, so this never double-counts one failure. | ||
| let body_level_failure = | ||
| result.error.is_none() && is_body_level_failure(&result.name, &result.content); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Log body-level failure classification.
Emit a tracing::debug! event when this branch detects ok: false, including the tool name but not the response body. This makes the new failure-state transition diagnosable before nudge/halt.
As per coding guidelines, “New or changed flows must include verbose, grep-friendly diagnostics for … branches … state transitions.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/openhuman/tinyagents/middleware.rs` around lines 2441 - 2447, Add a
tracing::debug! event in the body_level_failure detection branch, logging a
grep-friendly message and result.name while excluding result.content. Place it
before the existing nudge/halt handling so the ok:false transition is recorded
when is_body_level_failure returns true.
Source: Coding guidelines
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0e677ac3d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let body_level_failure = | ||
| result.error.is_none() && is_body_level_failure(&result.name, &result.content); |
There was a problem hiding this comment.
Detect ok:false before tool-output rewriting
In the session harness I checked, context_mw.install is pushed after the repeated-failure breaker and after_tool runs in reverse order, so ToolOutputMiddleware rewrites the result before this parse runs. For chat turns with the default 16 KiB tool-result budget/artifact preview, or when TokenJuice compacts these non-exempt tools, a large validate_workflow/dry_run_workflow {"ok": false} body is replaced with a preview/truncated non-JSON string; serde_json::from_str then returns false and the loop still is not counted as a failure. Capture the raw ok:false signal before output rewriting, or make these tool outputs exempt/parse from the persisted raw body, otherwise larger invalid graphs still burn the iteration budget.
Useful? React with 👍 / 👎.
Summary
RepeatedToolFailureMiddleware— the loop breaker that halts an agent run when a tool call keeps failing with no progress (issue #4249) — only treated a tool result as a "failure" whenresult.error.is_some().validate_workflowanddry_run_workflowreport an invalid graph or aborted sandbox run viaToolResult::success(result.error == None) with a JSON body carrying a top-level"ok": falseinstead. The breaker never saw those as failures, so whenworkflow_builderloops callingvalidate_workflow/dry_run_workflowon a graph it can't fix, it burns the full extended 50-iteration budget and only stops at the 600sFLOW_BUILD_TIMEOUT_SECSwall clock — with no useful root-cause summary for the user.Problem
src/openhuman/tinyagents/middleware.rs(RepeatedToolFailureMiddleware::after_tool, ~line 2315): the no-progress/repeated-failure ladder (NoProgressTracker) readsattempt.error: Option<&str>as its sole success/failure signal —Noneclears every counter as "progress was made."src/openhuman/flows/builder_tools.rs:validate_workflow(~768-777) returnsToolResult::successwith"ok": falsein the body for a structurally invalid graph or a failed builder gate.dry_run_workflow(~2846-2867, ~3059) returnsToolResult::successwith"ok": falsefor a sandbox abort/error or a dry run with null-resolved args / node errors.Since
result.errorstaysNonein both cases, the breaker'sToolAttempt.errorfield isNone, which the crate tracker (NoProgressTracker::record) treats as a success — it never trips the identical-repeat nudge/halt ladder, and the run only ends on the outer 600s timeout with a generic cap error instead of a root-cause halt summary.Solution
Added
fn is_body_level_failure(name: &str, content: &str) -> bool(src/openhuman/tinyagents/middleware.rs) that, only forvalidate_workflowanddry_run_workflow, parsescontentas JSON and returnstruewhen the top-level"ok"key isfalse. It's intentionally scoped to those two tool names — a generic"ok": falsefrom some other tool may be legitimate data, not a failure signal — and tolerant of non-JSON/missing-okcontent (returnsfalserather than guessing).In
after_tool:body_level_failureonce, gated onresult.error.is_none()so a result that already carries a realerroris never double-counted through the body check.failure_textto include the body content whenbody_level_failureis true (previously empty for aNone-error result), so it's available to the same halt-summary text the crate ladder already produces.ToolAttempt.errorfed toNoProgressTracker::record:Some(err)whenresult.erroris set (unchanged),Some(body content)when only the body-level signal fired,Noneotherwise (true success). This feeds the exact same nudge (at repeat 2) / halt (at repeat 3, or the varied-failure backstop at 6 consecutive) ladder as a real tool error, with no changes tohard_reject, the recoverable-failure headroom, or the terminal-inference fast-halt — those all remain gated onresult.error.is_some()as before, since a body-level validation failure is deterministic, not a transient/recoverable one.Testing
Added to the
RepeatedToolFailureMiddlewaretest module insrc/openhuman/tinyagents/middleware.rs:is_body_level_failure_detects_validate_and_dry_run_only— unit-tests the predicate directly:ok:falsedetected for both tool names,ok:truenever counts, an unrelated tool'sok:falseis ignored, and non-JSON/missing-okcontent is tolerated (returnsfalse).repeated_validate_workflow_ok_false_trips_the_breaker— repeatedvalidate_workflowok:falseresults (realToolResult::success,error: None) on the same graph trip the breaker (previously never would).single_or_unrelated_ok_false_does_not_falsely_trip_the_breaker— a singlevalidate_workflowok:falsedoesn't halt, and a repeated unrelated tool'sok:falseis never reinterpreted as a failure.existing_error_is_some_behavior_is_unchanged_by_body_level_check— the pre-existingresult.error.is_some()path (three identical errors → halt) is unchanged, and a result with botherrorset and body-levelok:falseis counted once per call, not double-counted.Acceptance
GGML_NATIVE=OFF cargo checkpasses cleancargo test --lib openhuman::tinyagents::middleware::passes (49/49, including the 4 new tests)cargo test --lib openhuman::flows::passes (383/383, unaffected)cargo fmtapplied,--checkcleanvalidate_workflow/dry_run_workflowbody-levelok:falseis reinterpreted as a failure; every other tool's behavior (including a genericok:falseelsewhere) is unchangedresult.error.is_some()behavior (hard-reject, recoverable-failure headroom, terminal-inference fast-halt, identical/varied nudge-halt ladder) is unchangederrorset and body-levelok:falseis not double-countedSummary by CodeRabbit