Skip to content

fix(worker): gate text-only exits on explicit outcome signal#327

Merged
jamiepine merged 1 commit intomainfrom
fix/worker-outcome-gate
Mar 5, 2026
Merged

fix(worker): gate text-only exits on explicit outcome signal#327
jamiepine merged 1 commit intomainfrom
fix/worker-outcome-gate

Conversation

@jamiepine
Copy link
Member

Summary

  • Workers could silently reach Done state without completing their task — the model would return text-only after a few tool calls and the system accepted it as a legitimate completion
  • The tool nudge system only protected against cold-start text responses (saw_tool_call permanently disabled it after the first tool call), leaving workers unprotected mid-task
  • Replace the broken saw_tool_call/completion_calls guards with an outcome gate: workers must call set_status(kind: "outcome") before the system allows a text-only exit

Problem

Observed in production: workers spawned for email-sending tasks would call read_skill + set_status("Researching...") then return narration text like "Let me create the email now..." — and the worker would exit as Done with success: true. The email was never sent. This happened 7+ times in a row on the same task.

Root cause: Rig's agent loop treats any text-only response (no tool calls) as the termination signal. The should_nudge_tool_usage hook was supposed to catch this, but it had two guards that disabled nudging too early:

  1. saw_tool_call — any tool call ever → nudge permanently disabled
  2. completion_calls > 2 — more than 2 LLM completions → nudge disabled

Both meant the nudge only worked on the very first 1-2 LLM calls when the model had never touched a tool.

Changes

src/hooks/spacebot.rs — Core fix

  • Removed saw_tool_call field entirely
  • Removed completion_calls guard from should_nudge_tool_usage
  • Added outcome_signaled atomic flag, set when set_status(kind: "outcome") is detected in on_tool_call args
  • Nudge now fires on every text-only worker response unless outcome has been signaled
  • Updated nudge prompt to instruct workers about outcome signaling
  • Rewrote 3 stale tests, added 4 new tests covering the exact bug case

src/tools/set_status.rs — Outcome signal mechanism

  • Added StatusKind enum (Progress | Outcome) with #[serde(default)]
  • Added kind field to SetStatusArgs and tool schema
  • Backward compatible: omitting kind defaults to Progress

prompts/en/worker.md.j2 — Worker instructions

  • Workers must call set_status(kind: "outcome") with a result summary before finishing
  • Updated rules and set_status documentation with examples

docs/design-docs/tool-nudging.md — Full rewrite reflecting outcome-gated design

Behavior change

Scenario Before After
Worker returns text after read_skill + set_status(progress) Exits as Done Nudged back to work
Worker returns text without any tool calls Nudged (2 retries) Nudged (2 retries)
Worker calls set_status(kind: "outcome") then returns text N/A Exits as Done
Worker exhausts nudge retries without outcome N/A (exits as Done) Fails with PromptCancelled

Testing

  • 421 lib tests pass (15 hook tests, 7 new/rewritten)
  • 12 integration tests pass
  • cargo fmt clean, cargo clippy zero warnings
  • gate-pr.sh all green

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

The changes implement a new tool-nudging/outcome-gate: workers must explicitly call set_status(kind: "outcome") before exiting with text-only responses; text-only without an outcome triggers nudges and retries. A StatusKind enum and kind field were added to status tooling and propagated through hooks, prompts, docs, and tests.

Changes

Cohort / File(s) Summary
Documentation & Prompts
AGENTS.md, docs/design-docs/tool-nudging.md, prompts/en/tools/set_status_description.md.j2, prompts/en/worker.md.j2
Rewrote nudging guidance to require explicit set_status(kind: "outcome") before final text output, added progress vs outcome guidance, tiered compaction notes, and examples.
Tooling & Public API
src/tools/set_status.rs, src/tools.rs
Added public StatusKind enum (Progress, Outcome), added kind: StatusKind to SetStatusArgs and SetStatusOutput, and re-exported StatusKind from src/tools.rs. Propagated kind into tool schema and emitted events.
Hook Implementation
src/hooks/spacebot.rs
Replaced saw_tool_call with outcome_signaled flag; updated nudging logic (should_nudge_tool_usage) to require outcome for text-only exits; detect set_status(kind: "outcome") in tool result paths; adjusted reset behavior and nudging prompt text; expanded and renamed tests for outcome scenarios.
Tests
tests/tool_nudge.rs
Relaxed assertion on TOOL_NUDGE_PROMPT to check for substring set_status instead of exact match; other test adjustments to reflect new outcome semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(worker): gate text-only exits on explicit outcome signal' clearly and specifically describes the main change: replacing the broken tool nudge guards with an outcome gate that requires explicit signaling before text-only exits are allowed.
Description check ✅ Passed The description comprehensively explains the problem (workers prematurely reaching Done state), the root cause (broken saw_tool_call and completion_calls guards), and the solution (outcome gate with set_status kind: outcome), with behavior change examples and testing verification.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/worker-outcome-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment on lines +48 to +50
pub const TOOL_NUDGE_PROMPT: &str = "You have not completed the task yet. Continue working using the available tools. \
When you have reached a final result, call set_status with kind \"outcome\" \
before finishing.";
Copy link
Contributor

Choose a reason for hiding this comment

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

Small nit: the nudge prompt wording is close, but it’d be nice to align it exactly with the worker docs + tool schema (kind: "outcome") and format set_status consistently.

Suggested change
pub const TOOL_NUDGE_PROMPT: &str = "You have not completed the task yet. Continue working using the available tools. \
When you have reached a final result, call set_status with kind \"outcome\" \
before finishing.";
pub const TOOL_NUDGE_PROMPT: &str = "You have not completed the task yet. Continue working using the available tools. \
When you have reached a final result, call `set_status` with `kind: \"outcome\"` before finishing.";

@@ -36,13 +36,18 @@ pub struct SpacebotHook {
event_tx: broadcast::Sender<ProcessEvent>,
tool_nudge_policy: ToolNudgePolicy,
completion_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
Copy link
Contributor

Choose a reason for hiding this comment

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

completion_calls looks unused now (it no longer gates nudging, and I don’t see it read anywhere). Might be worth removing to avoid future confusion, unless you’re keeping it for telemetry/debug output elsewhere.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/tool_nudge.rs (1)

17-20: Consider asserting outcome guidance explicitly as well.

The current check is flexible, but adding a second substring assertion for kind/outcome would better protect the core contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/tool_nudge.rs` around lines 17 - 20, Add a second assertion guarding
the nudge prompt's explicit guidance for signaling the outcome: in the test
alongside
spacebot::hooks::SpacebotHook::TOOL_NUDGE_PROMPT.contains("set_status"), assert
that TOOL_NUDGE_PROMPT also contains the substring "outcome" (or
"kind"/equivalent token your prompt uses) so the prompt explicitly documents the
outcome/kind signaling contract; update the assertion message accordingly to
indicate the nudge prompt should reference outcome/kind for outcome signaling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/spacebot.rs`:
- Around line 476-484: The current code sets outcome_signaled based on the
intent to call the "set_status" tool (checking tool_name and parsing args),
which lets a malformed or failed call still mark outcome signaled; instead, move
the check so outcome_signaled is set only when the tool actually returns a
successful result. Locate the call site that processes tool results (the
function/method that receives the tool response/result for "set_status" — e.g.,
where tool responses are handled or where apply_tool_result/handle_tool_response
is implemented), parse the returned tool output there (inspect the successful
result payload), and only call self.outcome_signaled.store(true,
Ordering::Relaxed) when the tool execution succeeded and the parsed JSON has
kind == "outcome"; remove or stop using the current intent-time check that uses
tool_name/args for this purpose.

---

Nitpick comments:
In `@tests/tool_nudge.rs`:
- Around line 17-20: Add a second assertion guarding the nudge prompt's explicit
guidance for signaling the outcome: in the test alongside
spacebot::hooks::SpacebotHook::TOOL_NUDGE_PROMPT.contains("set_status"), assert
that TOOL_NUDGE_PROMPT also contains the substring "outcome" (or
"kind"/equivalent token your prompt uses) so the prompt explicitly documents the
outcome/kind signaling contract; update the assertion message accordingly to
indicate the nudge prompt should reference outcome/kind for outcome signaling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74fa9c90-ad6b-4a6a-bece-b79fd899fa6f

📥 Commits

Reviewing files that changed from the base of the PR and between 7f22fcb and bafef6e.

📒 Files selected for processing (8)
  • AGENTS.md
  • docs/design-docs/tool-nudging.md
  • prompts/en/tools/set_status_description.md.j2
  • prompts/en/worker.md.j2
  • src/hooks/spacebot.rs
  • src/tools.rs
  • src/tools/set_status.rs
  • tests/tool_nudge.rs

Workers could silently reach Done state without completing their task by
returning text-only responses mid-work. The tool nudge system only
protected against cold-start text (saw_tool_call disabled it after the
first tool call), so workers that made a few calls then narrated instead
of acting would exit as successful with no useful output.

Replace the saw_tool_call/completion_calls guards with an outcome gate:
workers must call set_status(kind: "outcome") before the system allows
a text-only exit. Without it, every text-only response triggers a nudge
retry. After retries exhaust, the worker fails loudly instead of
silently succeeding.

- Add StatusKind (progress|outcome) to set_status tool, backward compat
- Track outcome_signaled flag in SpacebotHook, detected from tool args
- Remove saw_tool_call field and completion_calls guard from nudge logic
- Update worker prompt to require outcome signal before finishing
- Rewrite tests to cover the exact bug case and outcome gate behavior
@jamiepine jamiepine force-pushed the fix/worker-outcome-gate branch from bafef6e to fac4e23 Compare March 5, 2026 08:44
@jamiepine jamiepine merged commit 6b3d6c0 into main Mar 5, 2026
3 of 4 checks passed
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