Skip to content

fix(agent): gate headless run completion on honesty (no false-success on no-tool-call / self-reported-incomplete turns) - #325

Merged
gnanam1990 merged 5 commits into
mainfrom
fix/agent-completion-honesty
Jun 27, 2026
Merged

fix(agent): gate headless run completion on honesty (no false-success on no-tool-call / self-reported-incomplete turns)#325
gnanam1990 merged 5 commits into
mainfrom
fix/agent-completion-honesty

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two fixes to the headless agent loop so a run is reported success only when it actually finished (surfaced during headless-exec testing). Both are gated by a new Options.RequireCompletionSignal, set only for headless zero execdefault off, so the interactive TUI is byte-identical.

Bug #1 — premature loop termination (deterministic)

A turn that produced text but no tool call was always accepted as the final answer, so the loop reported success even mid-task. Now a no-tool-call turn is not treated as done while work clearly remains — pending update_plan items, or a message ending on a continuation cue ("…Let me check the config:") — the loop re-prompts to continue (bounded by maxContinueNudges, still by MaxTurns), and finalizes INCOMPLETE (exit 4) rather than success if it keeps stalling.

Behavioral proof: on a multi-step server-setup task the agent went from 3 tool calls (it stopped on "…Let me check the SSH configuration:") → 50 tool calls — it now starts sshd, completes the SSH clone, and works its plan instead of declaring victory after step 2.

Bug #2success without a task-grounded check

Deterministic, unit-tested:

  • self-report downgrade — if the final message admits the model guessed / could not meet the objective (first-person inability stems generalized over verb/tense, plus guess/fallback phrases, with a guard so success-y negations like "couldn't find any issues" / "cannot reproduce" are not misread) → INCOMPLETE, never success.
  • plan gate — pending / in_progress update_plan items at termination → INCOMPLETE.

Advisory (NOT a guarantee):

  • task-grounded acceptance — when --self-correct is on, one bounded acceptance pass re-derives the task's stated criterion and discourages three false-success patterns (well-formed==correct, existing-tests-pass==objective-met, result==baseline-it-was-told-to-beat). This reduces but does not eliminate false-success: a model that confidently claims "PASS, all requirements met" can still slip through, because there is no general oracle to verify correctness against a task's hidden criterion. This is a fundamental limit, not a tuning gap — documented honestly rather than overclaimed.

Behavioral proof: an image-grounded task a text-only model can't actually solve flipped from false success (reward 0) → honest INCOMPLETE once its "I cannot analyze the image" admission is detected.

Scope

Agent loop + headless exit wiring + tests only:

  • internal/agent/{loop,guardrails,types}.go
  • internal/cli/exec.goexitIncomplete=4, RequireCompletionSignal wiring, INCOMPLETE run_end + reason
  • internal/agent/{completion_gate,acceptance_gate}_test.go

Verification

make build, go vet ./..., make lint, go test ./... -race — all green (70 pkg, 0 fail, 0 races). Gate-off tests confirm interactive/default behavior is unchanged.

Summary by CodeRabbit

  • New Features
    • Headless runs now report distinct incomplete vs success outcomes, including an optional incomplete reason.
    • Added stricter headless completion gating, with optional task-grounded acceptance challenge for self-correction.
  • Bug Fixes
    • Prevents stalled or mid-step “continue” situations from being marked as success by injecting bounded re-prompts.
    • Treats self-reported inability (and certain “can’t/don’t know” phrasing) as incomplete, respects pending plan work, and marks max-turns cutoff as incomplete.
    • Preserves legacy behavior when the gate is disabled.
  • Tests
    • Added acceptance-gate and completion-gate regression coverage for these scenarios.

A turn that produced text but no tool call was always accepted as the final answer, so the loop reported success even when the model stopped mid-task (e.g. ended on "...Let me check the SSH configuration:" with plan steps still pending).

Add an opt-in completion gate (Options.RequireCompletionSignal): when a turn has no tool call and work clearly remains -- pending update_plan items, or the message ends on a continuation cue -- re-prompt the model to continue instead of finalizing. Bounded by maxContinueNudges and still by MaxTurns/the deadline; once the budget is spent the run finalizes as INCOMPLETE (Result.Incomplete) rather than success. Default off, so the interactive path is byte-identical.

Genuine single-turn completions (no pending plan, no cue) still finalize as success. Covered by internal/agent/completion_gate_test.go.
Enable the agent completion gate for headless exec (RequireCompletionSignal) and map Result.Incomplete to run_end status "incomplete" with a new exit code 4, so a run that stalled mid-task (model stopped without a tool call while work remained, continue budget exhausted) is no longer reported as success. Interactive callers are unaffected.
…unded acceptance

Reduce -- not eliminate -- false-success on headless runs. Two DETERMINISTIC, unit-tested gates (with the plan gate from the prior commit):

(a) self-report downgrade: if the final message admits the model guessed or could not meet the objective, finalize INCOMPLETE (exit 4), never success. Inability is matched by first-person STEMS generalized over verb/tense ("I cannot/can't/could not/am unable to/do not have/unable to ...") plus guess/fallback/uncertainty phrases, with a guard so success-y negations ("could not find any issues", "cannot reproduce") are not misread. (b7bc0b8's plan gate already forces INCOMPLETE on pending/in_progress update_plan items at termination.)

(b) task-grounded acceptance is ADVISORY, not a guarantee. When --self-correct is on it demands one bounded acceptance pass that re-derives the task's stated criterion and runs a concrete check, discouraging three false-success patterns (well-formed==correct, existing-tests-pass==objective-met, result==baseline-it-was-told-to-beat). But it is a prompt: a model that ignores it and confidently claims "PASS, all requirements met" still slips, because ZERO has no general oracle to verify correctness against a task's hidden criterion. Empirically (TB-2, qwen3-coder:480b) this reliably catches admissions and incomplete plans and REDUCES false-success, but a confident false PASS on a model-ceiling task is a residual, fundamental gap -- not a tuning miss.

Default off (RequireCompletionSignal); interactive callers are byte-identical. Covered by internal/agent/{acceptance_gate_test.go,completion_gate_test.go}.
When a headless run finalizes as INCOMPLETE, include Result.IncompleteReason in the session error event and a stderr warning so an honestly-incomplete run (e.g. "the final message admits the objective was not met") is debuggable rather than an opaque exit 4.
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 2f6473963eab
Changed files (6): internal/agent/acceptance_gate_test.go, internal/agent/completion_gate_test.go, internal/agent/guardrails.go, internal/agent/loop.go, internal/agent/types.go, internal/cli/exec.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e9dd7a90-30c1-4183-b92b-4534353b6119

📥 Commits

Reviewing files that changed from the base of the PR and between c111dc0 and 2f64739.

📒 Files selected for processing (5)
  • internal/agent/acceptance_gate_test.go
  • internal/agent/completion_gate_test.go
  • internal/agent/guardrails.go
  • internal/agent/loop.go
  • internal/cli/exec.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/cli/exec.go
  • internal/agent/completion_gate_test.go
  • internal/agent/guardrails.go

Walkthrough

Adds headless completion gating to agent runs, including pending-plan tracking, continuation-cue and self-reported incompletion handling, task-grounded acceptance re-checks, new Incomplete result fields, and a dedicated CLI exit code for incomplete runs.

Changes

Headless completion gating

Layer / File(s) Summary
Run result contract
internal/agent/types.go
Options gains RequireCompletionSignal, and Result gains Incomplete and IncompleteReason for headless completion reporting.
Continuation and plan guardrails
internal/agent/guardrails.go
Adds continuation-cue detection, pending update_plan item tracking, and acceptance-verification prompt helpers.
Completion gate in Run
internal/agent/loop.go, internal/agent/completion_gate_test.go, internal/agent/acceptance_gate_test.go
Run now re-prompts or returns Incomplete based on pending plan items, continuation cues, and self-reported incompletion, with tests covering continuation, legacy behavior, max-turns, and self-correct acceptance.
Headless CLI exit handling
internal/cli/exec.go
Headless execution enables the completion signal and returns exitIncomplete when the agent reports an incomplete run.

Sequence Diagram(s)

sequenceDiagram
  participant Exec as internal/cli/exec.go
  participant Run as agent.Run
  participant Guards as guardState
  participant SelfCorrector as SelfCorrector
  participant Writer as writer

  Exec->>Run: RequireCompletionSignal=true
  Run->>Guards: pendingPlanItems() / endsWithContinuationCue()
  alt completion gate requires more work
    Run->>SelfCorrector: acceptanceVerificationNudge()
    Run-->>Exec: Result.Incomplete
    Exec->>Writer: runEnd("incomplete", exitIncomplete)
  else final completion accepted
    Run-->>Exec: Result.Success
    Exec->>Writer: runEnd("success", exitSuccess)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#51: Shares the internal/agent/loop.go no-tool-call completion path that this PR extends with completion gating and incomplete reporting.

Suggested reviewers

  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: gating headless completion to avoid false-success on no-tool-call and self-reported-incomplete turns.
Docstring Coverage ✅ Passed Docstring coverage is 84.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@internal/agent/guardrails.go`:
- Around line 436-450: The update_plan parsing in guardrails.go is clearing
state too aggressively when the payload is valid JSON but missing the expected
plan data. Update the logic around the json.Unmarshal into the parsed Plan
struct and the state.planItemsPending assignment so that empty or malformed plan
payloads like {} or {"plans":[]} do not overwrite an existing pending count;
only replace state.planItemsPending when the parsed plan list is actually
present and usable.

In `@internal/agent/loop.go`:
- Around line 338-397: The completion gate in `loop.go` only downgrades to
INCOMPLETE inside the normal turn flow, so the max-turn fallback can still
return success after a final nudge or while `guards.pendingPlanItems()` remains
true. Update the max-turn exit path in
`runLoop`/`finalAnswerAfterMaxTurns`/`maxTurnsAnswer` to apply the same
`RequireCompletionSignal` checks before returning success, and if pending plan
items, a continuation cue, or a self-reported incompletion is still present, set
`result.Incomplete`, `IncompleteReason`, `FinalAnswer`, and `Messages` instead
of finalizing as success.

In `@internal/cli/exec.go`:
- Around line 637-647: The completion notification is emitted too early in
exec.go, so an incomplete headless run can still be reported as successful
before the result.Incomplete branch in the run-end handling runs. Update the
logic around the run completion path in exec.go so the normal success
notification is sent only after checking result.Incomplete, using the existing
result.Incomplete / result.IncompleteReason handling in this block; either move
the success notification below the incomplete branch or emit a separate
incomplete/failure notification from this path.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6b0efa25-ddbf-4e17-8a68-392f212853de

📥 Commits

Reviewing files that changed from the base of the PR and between d29ab98 and c111dc0.

📒 Files selected for processing (6)
  • internal/agent/acceptance_gate_test.go
  • internal/agent/completion_gate_test.go
  • internal/agent/guardrails.go
  • internal/agent/loop.go
  • internal/agent/types.go
  • internal/cli/exec.go

Comment thread internal/agent/guardrails.go
Comment thread internal/agent/loop.go
Comment thread internal/cli/exec.go Outdated
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Code review — completion-honesty gate (high effort, workflow-backed: per-angle finders + independent verifier)

Verified findings below. The design/intent is sound, but the gate currently over- and under-triggers in edge cases. Recommend addressing the High items before merge.

🔴 High — correctness

1. internal/agent/guardrails.goselfReportPhrases matched with no success-negation guard.
"fall back to", "as a fallback", "fell back to", "placeholder value", "as a placeholder", "best guess" go through bare strings.Contains (unlike the inability stems, which are guarded). A legit final answer — "the parser will fall back to UTF-8 when no encoding is set", "I replaced the placeholder value with the computed key" — is downgraded to INCOMPLETE / exit 4. Breaks correct runs. Fix: apply a first-person/context guard to these phrases (or drop the ambiguous ones).

2. internal/cli/exec.go (+exec_writer.go) — --output json reports exit_code:0 on INCOMPLETE.
For non-stream JSON, writer.final() emits {"type":"done","exit_code":0} before the result.Incomplete check, and writer.runEnd is a no-op for json. A consumer reading the documented done.exit_code sees success on an abandoned run (process still exits 4). Only stream-json is wired. Defeats the feature in json mode. Fix: in the Incomplete branch, emit the json done with exitIncomplete (mirror the interrupted path).

3. internal/agent/loop.gopendingPlanItems false-INCOMPLETE on stale plan.
Model completes via tools, then gives a text-only summary without re-calling update_plan to flip the last item (very common). planItemsPending stays >0 → 3 nudges → INCOMPLETE on a done task. Fix: treat pending-plan as a weak signal (don't finalize INCOMPLETE on it alone without a cue/admission), or cap plan-nudges at 1.

4. internal/agent/loop.go — maxTurns exit never sets Incomplete.
A run that keeps issuing tool calls to the turn limit with plan items pending returns successfalse-success via the maxTurns door (the gate only covers the no-tool-call path). Fix: set Incomplete on the maxTurns exit when RequireCompletionSignal && pending/unfinished.

🟡 Medium

5. guardrails.goselfReportedIncompletion uses first-occurrence strings.Index.
"I could not find any examples… so I could not implement it" → first "could not" hits the "find any" guard → continue → the real later admission is missed (false-success). Fix: scan all occurrences of each stem, applying the guard per-occurrence.

6. guardrails.goendsWithContinuationCue too broad.
Trailing : or last-line "Next, I…"/"Let me…" flags genuine closers ("Next, I suggest reviewing the changes.") → false INCOMPLETE. Fix: tighten (don't treat a bare trailing colon / forward-looking recommendation as a mid-step cue).

7. exec.go — cron can't surface the INCOMPLETE reason.
INCOMPLETE signals via warning (not an error event); the cron failure-detail extractor only scans type=="error", so scheduled INCOMPLETE runs record a failure with an empty message. Fix: also emit an error event (or have cron read IncompleteReason).

8. loop.go — gate ordering.
pendingPlanItems is checked before selfReportedIncompletion, so an admitted-impossible task burns 3 wasted nudges and reports the wrong reason. Fix: check the admission (self-report) first.

⚪ Low (cleanup)

9. loop.go — acceptance pass fires on every --self-correct run (even read-only) → one extra model call each. Gate it on "did the run mutate anything."

10. guardrails.goplanStatusRemaining duplicates tools.normalizePlanStatus's status vocabulary → drift risk. Export + reuse the canonical normalizer.


Tests + go test ./... -race are green, but the gate-off (interactive/TUI) path is the only one fully safe today; the headless paths above need the High fixes. Diagnosis from behavior/logs only; no benchmark artifacts in this diff.

Addresses the correctness findings from the PR review.

self-report (#1): drop behavior-describing phrases ("fall back to",
"placeholder value", bare "best guess", "as a fallback", "without proper")
that also match legitimate final answers; keep first-person/uncertainty
admissions only, since these are matched without a context guard.

self-report (#5): scan every occurrence of each inability stem so an early
success-negation ("could not find any examples") no longer masks a later
genuine admission with the same stem ("could not implement it").

continuation cue (#6): require a trailing colon AND an action lead-in on the
final clause; stop flagging recommendations, plain summary colons, and
sign-offs. Still catches the mid-line "...Let me check the config:".

gate order (#8): check the self-report admission BEFORE pending-plan, so an
admitted-impossible task downgrades immediately with the accurate reason
instead of burning continue-nudges.

pending plan (#3): treat a pending/in_progress update_plan item as a
NUDGE-only weak signal -- it no longer forces INCOMPLETE on its own (a
completed run that left stale plan bookkeeping is trusted). Only a
continuation cue or a self-report admission finalizes INCOMPLETE.

max-turns (#4): a run cut off at the MaxTurns ceiling now finalizes
INCOMPLETE under the gate instead of being reported as success.

exec json/cron (#2, #7): for -o json, emit the terminal done with exit 4 on
an incomplete run (final() pre-emits a success done:0 for json that would
otherwise mask it); emit an error event -- not just a warning -- so the cron
failure extractor can recover the reason.

Deferred (noted on the PR): acceptance-only-when-mutated (#9, cost) and
reusing tools.normalizePlanStatus (#10, would widen scope to internal/tools).

Tests: add TestContinuationCueMatching and TestMaxTurnsCutoffIsIncompleteUnderGate,
extend TestSelfReportedIncompletionMatching with the #1/#5 cases, and replace
the in_progress=>incomplete test with TestPendingPlanAloneDoesNotForceIncomplete.
make build / go vet / make lint / go test ./... -race all green.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Review fixes applied — 2f64739

All correctness findings addressed; full suite green (make build / go vet ./... / make lint / go test ./... -race — 70 pkg, 0 fail, 0 races).

# finding resolution
1 self-report false-positives (no guard) Fixed — dropped behavior-describing phrases ("fall back to", "placeholder value", bare "best guess", "as a fallback", "without proper"); kept first-person/uncertainty admissions only
2 -o json reports exit_code:0 on INCOMPLETE Fixed — incomplete handled per-format before final(); json now emits terminal done with exit 4
3 pending-plan false-INCOMPLETE on stale plan Fixed — pending-plan is now a nudge-only weak signal; only a continuation cue or admission finalizes INCOMPLETE
4 max-turns exit never set Incomplete Fixed — a max-turns cutoff now finalizes INCOMPLETE under the gate
5 first-occurrence strings.Index masks later admission Fixed — scan every occurrence of each inability stem
6 endsWithContinuationCue too broad Fixed — require trailing colon AND an action lead-in on the final clause (still catches mid-line "…Let me check the config:"; ignores recommendations/summary-colons/sign-offs)
7 cron can't surface INCOMPLETE reason Fixed — emit an error event (not just a warning) so the cron extractor recovers the reason
8 gate ordering (pending-plan before admission) Fixed — admission is checked first; no wasted nudges, accurate reason
9 acceptance fires on every --self-correct run Deferred — cost optimization (gate on "did the run mutate"); not a correctness bug
10 planStatusRemaining duplicates tools.normalizePlanStatus Deferred — the dedup requires exporting from internal/tools/update_plan.go, which would widen this PR beyond the agent-fix scope

Tests: added TestContinuationCueMatching, TestMaxTurnsCutoffIsIncompleteUnderGate; extended TestSelfReportedIncompletionMatching (#1/#5 cases); replaced the old in_progress ⇒ incomplete test with TestPendingPlanAloneDoesNotForceIncomplete (#3 behavior).

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable then approve it

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

@gnanam1990 Based on the fixes you summarized, the previously blocking correctness issues look addressed, and the added regression coverage around continuation cues, self-reported incompletion, pending-plan handling, max-turn cutoffs, and JSON/cron reporting makes this approvable.

✅ Action performed

Comments resolved and changes approved.

@gnanam1990
gnanam1990 merged commit f8ac5ec into main Jun 27, 2026
7 checks passed
@Vasanthdev2004
Vasanthdev2004 deleted the fix/agent-completion-honesty branch June 28, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant