fix(agent): normalize empty successful tool output#5565
Conversation
theonlyhennygod
left a comment
There was a problem hiding this comment.
Review β PR #5565: fix(agent): normalize empty successful tool output
Comprehension Summary
This PR normalizes empty successful tool output to "(no output)" in execute_one_tool (in src/agent/tool_execution.rs) before the result is passed to scrub_credentials and forwarded to providers. The motivation is that some providers (e.g. Deepseek V3.2 via GCP) reject empty tool-role content with HTTP 400. The blast radius is narrow: only the success branch of execute_one_tool is affected, and only when the tool returns an empty string on success. Downstream consumers (loop detector, hooks, trace recording, message assembly) will now see "(no output)" instead of "" in this edge case.
Security / Performance Assessment
- Security: No security impact identified. No change to access control, input validation, secret handling, or attack surface. The
scrub_credentialscall is preserved. - Performance: No meaningful performance impact. The change adds one string comparison and, in the empty case, one small allocation. See suggestion below about an unnecessary
.clone()in the non-empty path.
What was reviewed and verified
- CI Required Gate: all checks passing (lint, test, build on all platforms, security audit, 32-bit check).
- PR template: fully completed with all required sections.
- Privacy/data hygiene: pass β no PII, credentials, or identity-specific language in the diff.
- Scope: single concern, minimal patch, no unrelated changes.
- Duplicate scan: no overlapping open PRs found.
- Architectural alignment: no new dependencies, no trait bypass, no security weakening.
- Downstream consumers: reviewed all callers of
execute_one_tooland all uses ofToolExecutionOutcome.outputinloop_.rsβ the change is compatible. The loop detector will now hash"(no output)"instead of""for this edge case, which is correct behavior (empty outputs were already degenerate for detection purposes).
Findings
-
[suggestion]Unnecessary.clone()in the non-empty path (line 96 in the diff):r.output.clone()
scrub_credentialstakes&str, so the original code passed&r.outputdirectly without allocating. The new code clonesr.outputintonormalized_outputeven in the common (non-empty) case, only to take a reference to it on the next line. Consider usingCow<'_, str>or restructuring to avoid the extra allocation:let normalized: &str = if r.output.is_empty() { "(no output)" } else { &r.output }; Ok(ToolExecutionOutcome { output: scrub_credentials(normalized), ... })
Why: Avoids an unnecessary heap allocation on every successful non-empty tool call β the hot path.
Action: Consider restructuring to avoid the clone. -
[suggestion]No test for the new normalization behavior:
The existing tests inloop_.rscover the unknown-tool error path and the activated-tool success path, but neither exercises the empty-successful-output normalization. A small test that creates a tool returningToolResult { success: true, output: String::new(), error: None }and asserts the outcome output is"(no output)"would lock in this behavior.
Why: Prevents future regressions if this normalization is accidentally removed.
Action: Consider adding a targeted test.
Verdict: Needs author action
Thank you for this clean, well-scoped fix β the PR template is thorough, the motivation is clear, and the change is minimal. The two suggestions above are non-blocking improvements that would strengthen the patch before merge. Please address or acknowledge them, and this should be ready for maintainer merge.
| Field | Content |
|---|---|
| PR | #5565 β fix(agent): normalize empty successful tool output |
| Author | @aliasliao |
| Summary | Normalizes empty successful tool output to "(no output)" to prevent provider-side HTTP 400 errors for empty tool-role content. Blast radius: success branch of execute_one_tool only. |
| Action | Needs-action |
| Reason | Two non-blocking suggestions: unnecessary .clone() and missing test coverage. |
| Security/performance | No security impact. Minor unnecessary allocation in non-empty path (suggestion filed). |
| Changes requested | (1) Avoid unnecessary .clone() in non-empty path. (2) Add test for empty-output normalization. |
| Architectural notes | No footprint, dependency, or design concerns. |
| Tests | CI all green. No new tests added for the changed behavior. |
| Notes | Well-motivated bugfix with clear provider-side evidence. Straightforward to finalize. |
|
@theonlyhennygod review comments are addressed: the success path now avoids the extra clone, and I added a regression test for empty successful tool output. I also verified the branch is current with master (no merge conflict), ran |
|
@singlerider @WareWolf-MoonWall could you please take another look when you have a moment? This issue #5564 can still be reproduced in master branch latest commit: 9f0de18. Reproduce prompt:
Captured llm http payload:
|
singlerider
left a comment
There was a problem hiding this comment.
Agent Review β Routing: Needs Maintainer Review
@WareWolf-MoonWall @JordanTheJet β this PR modifies crates/zeroclaw-runtime/src/agent/ (runtime path), requiring maintainer sign-off.
DRY note: @theonlyhennygod reviewed this PR and raised two suggestions. @aliasliao has addressed both:
- β
Unnecessary
.clone()removed β non-empty path now uses&r.outputdirectly - β
Regression test added β
execute_one_tool_normalizes_empty_success_outputasserts"(no output)"for empty successful tool output
What the change does: Normalizes empty successful tool output to "(no output)" before forwarding to providers. Fixes HTTP 400 errors from Deepseek V3.2 via GCP (and any other provider that rejects empty tool-role content). Blast radius: success branch of execute_one_tool only. 57 lines added, 1 deleted.
Zero remaining findings. Code is correct and ready for maintainer merge.
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
PR Review β #5565 fix(agent): normalize empty successful tool output
I've read the full diff, the linked issue (#5564), and all prior review threads.
What this change does
In execute_one_tool, when a tool returns success: true with an empty output string, the fix normalizes that to "(no output)" before passing it to scrub_credentials and forwarding to the provider. Without this, providers that reject empty tool-role content β including Deepseek V3.2 via GCP β return HTTP 400, breaking the tool-calling workflow entirely. S1 severity, confirmed reproduced on current master by the author with a screenshot.
The fix is minimal, single-concern, and correctly scoped to the success branch only. The failure path is unchanged. The non-empty success path is unchanged.
β Commendation
Both of @theonlyhennygod's prior suggestions were addressed cleanly:
The unnecessary .clone() is gone β the non-empty path now passes &r.output directly to scrub_credentials as it did before, avoiding the heap allocation on the hot path. The fix uses a &str branch (if r.output.is_empty() { "(no output)" } else { &r.output }) which is the right shape β zero-cost for the common case.
The regression test execute_one_tool_normalizes_empty_success_output is well-structured: EmptySuccessTool is a purpose-built mock that returns success: true with empty output, and the assertion confirms both that outcome.output == "(no output)" and that error_reason.is_none() β correctly distinguishing the success-with-no-output case from the failure case. This is the FND-006 Β§4.3 standard applied correctly: a test that would have caught the bug.
π‘ Conditional β validation evidence section is now stale
The PR body still states that cargo clippy --all-targets -- -D warnings and cargo test "were not run." The author confirmed in the thread that both were run after addressing @theonlyhennygod's feedback, and CI is green. The PR body should be updated to reflect the current state. This is a template hygiene item β the evidence exists, it's just not recorded where the template requires it.
No new blocking findings
The fix is correct, the test covers the regression, and both prior requests have been addressed. No architectural concerns, no security impact, no performance impact on the non-empty path. Risk label (risk: high applied, risk: medium in body) follows the same pattern seen in other runtime PRs β the label is correct per AGENTS.md.
Ready for maintainer merge once the validation evidence section is updated.
singlerider
left a comment
There was a problem hiding this comment.
Thanks for the persistence on this one, @aliasliao β the fix is solid.
Comprehension summary
Normalizes empty successful tool output to "(no output)" in the success branch of execute_one_tool before forwarding to scrub_credentials and the provider. Fixes HTTP 400 errors from providers (Deepseek V3.2 via GCP confirmed) that reject empty tool-role content. Failure path and non-empty success path are unchanged. Single-concern, minimal diff.
What was verified
- CI fully green on Apr 15 run β lint, test, build (linux/mac/windows), security audit, all gates pass
- Fix correctly scoped to success branch only;
&r.outputused on non-empty path (no unnecessary clone) - Regression test
execute_one_tool_normalizes_empty_success_outputcovers the bug:EmptySuccessToolreturnssuccess: truewith empty output, assertsoutcome.output == "(no output)"anderror_reason.is_none() - PR body validation evidence section updated (was stale β original stated clippy/test not run; author confirmed in thread both were run post-feedback)
Security / performance assessment
No security impact. No performance impact on the non-empty hot path β &r.output branch is zero-cost.
This PR is ready for maintainer merge.
tool θΏε success=true δ½ output δΈΊη©ΊζΆοΌε LLM δΌ η©ΊδΈ²δΌζ±‘ζεε²γ ζδΊ provider θΏδΌζ₯ invalid contentγ ζη©ΊθΎεΊε½δΈεδΈΊ "(no output)"οΌεΉΆε¨ tracing ζ₯εΏιδΉη¨ε½δΈεζζ¬γ Ports upstream fbb5ae9 (zeroclaw-labs#5565) to master_wecom's pre-workspace-split layout. Co-authored-by: Liao Jinyuan <jinyuanovo@gmail.com>
β¦ changelog - Bump workspace version 0.7.0 β 0.7.1 in root Cargo.toml - Revert release workflow to gh release create --target for workflow_dispatch (the git-push approach from zeroclaw-labs#5860 is blocked by the org Restrict creations rule; --target uses the Releases API which bypasses it, and v0.7.1 has no immutable release lock so the previous blocker does not apply) - Update CHANGELOG-next.md: retitle to v0.6.9 β v0.7.1, restore full comprehensive notes from the upstream draft, and add entries that were missing from the original v0.7.0 draft: - feat(observability): otel_headers for authenticated OTLP exporters (zeroclaw-labs#5700) - feat: GitHub Copilot provider onboarding (zeroclaw-labs#5321) - fix(channels/telegram): inline_keyboard for tool approval requests (zeroclaw-labs#5790) - fix(provider): strip tool_stream for non-Z.AI providers (zeroclaw-labs#5806) - fix(agent): normalize empty successful tool output (zeroclaw-labs#5565) - fix(web): theme mode switch not applying correctly (zeroclaw-labs#5724) - fix(web): add visual preview swatches to theme selector (zeroclaw-labs#5767) - fix: cron_run tool output not delivered to configured channels
β¦ changelog - Bump workspace version 0.7.0 β 0.7.1 in root Cargo.toml - Revert release workflow to gh release create --target for workflow_dispatch (the git-push approach from zeroclaw-labs#5860 is blocked by the org Restrict creations rule; --target uses the Releases API which bypasses it, and v0.7.1 has no immutable release lock so the previous blocker does not apply) - Update CHANGELOG-next.md: retitle to v0.6.9 β v0.7.1, restore full comprehensive notes from the upstream draft, and add entries that were missing from the original v0.7.0 draft: - feat(observability): otel_headers for authenticated OTLP exporters (zeroclaw-labs#5700) - feat: GitHub Copilot provider onboarding (zeroclaw-labs#5321) - fix(channels/telegram): inline_keyboard for tool approval requests (zeroclaw-labs#5790) - fix(provider): strip tool_stream for non-Z.AI providers (zeroclaw-labs#5806) - fix(agent): normalize empty successful tool output (zeroclaw-labs#5565) - fix(web): theme mode switch not applying correctly (zeroclaw-labs#5724) - fix(web): add visual preview swatches to theme selector (zeroclaw-labs#5767) - fix: cron_run tool output not delivered to configured channels
Version
- Bump workspace version 0.7.0 β 0.7.1 in root Cargo.toml + Cargo.lock
CI rationalisation (FND-004 Phase 1 β Rationalise)
- Delete checks-on-pr.yml and ci-run.yml β two workflows doing identical
work on every PR, producing duplicate signal and double compute cost
- Add ci.yml (name: Quality Gate) β single staged pipeline replacing both:
Stage 1: fmt + clippy --workspace (fast gate)
Stage 2: build matrix, check all-features / no-default-features / 32-bit,
benchmarks compile (parallel, gated on Stage 1)
Stage 3: nextest (gated on Stage 1)
Stage 4: cargo deny check β licenses, sources, advisories (deny.toml
already present and triaged)
Stage 5: CI Required Gate composite job (branch protection target)
- Remove rust_strict_delta_gate.sh β workspace-aware clippy --workspace
makes delta comparison implicit (clean baseline = any warning fails)
- pre-release-validate.yml: remove pull_request trigger (secrets unavailable
on fork PRs caused guaranteed failure on every Cargo.toml bump); remove
stale CARGO_REGISTRY_TOKEN check (crates.io publishing removed in zeroclaw-labs#5858)
Release workflow
- Revert release-stable-manual.yml to gh release create --target for
workflow_dispatch (git push approach from zeroclaw-labs#5860 blocked by org Restrict
creations rule; Releases API bypasses it; v0.7.1 has no immutable lock)
Changelog
- Retitle CHANGELOG-next.md to v0.6.9 β v0.7.1, restore full release notes,
add entries missing from original draft: otel_headers (zeroclaw-labs#5700), GitHub
Copilot onboarding (zeroclaw-labs#5321), Telegram inline_keyboard (zeroclaw-labs#5790), tool_stream
fix (zeroclaw-labs#5806), empty tool output (zeroclaw-labs#5565), web theme fixes (zeroclaw-labs#5724, zeroclaw-labs#5767),
cron_run delivery fix
Version
- Bump workspace version 0.7.0 β 0.7.1 in root Cargo.toml + Cargo.lock
CI rationalisation (FND-004 Phase 1 β Rationalise)
- Delete checks-on-pr.yml and ci-run.yml β two workflows doing identical
work on every PR, producing duplicate signal and double compute cost
- Add ci.yml (name: Quality Gate) β single staged pipeline replacing both:
Stage 1: fmt + clippy --workspace (fast gate)
Stage 2: build matrix, check all-features / no-default-features / 32-bit,
benchmarks compile (parallel, gated on Stage 1)
Stage 3: nextest (gated on Stage 1)
Stage 4: cargo deny check β licenses, sources, advisories (deny.toml
already present and triaged)
Stage 5: CI Required Gate composite job (branch protection target)
- Remove rust_strict_delta_gate.sh β workspace-aware clippy --workspace
makes delta comparison implicit (clean baseline = any warning fails)
- pre-release-validate.yml: remove pull_request trigger (secrets unavailable
on fork PRs caused guaranteed failure on every Cargo.toml bump); remove
stale CARGO_REGISTRY_TOKEN check (crates.io publishing removed in zeroclaw-labs#5858)
Release workflow
- Revert release-stable-manual.yml to gh release create --target for
workflow_dispatch (git push approach from zeroclaw-labs#5860 blocked by org Restrict
creations rule; Releases API bypasses it; v0.7.1 has no immutable lock)
Changelog
- Retitle CHANGELOG-next.md to v0.6.9 β v0.7.1, restore full release notes,
add entries missing from original draft: otel_headers (zeroclaw-labs#5700), GitHub
Copilot onboarding (zeroclaw-labs#5321), Telegram inline_keyboard (zeroclaw-labs#5790), tool_stream
fix (zeroclaw-labs#5806), empty tool output (zeroclaw-labs#5565), web theme fixes (zeroclaw-labs#5724, zeroclaw-labs#5767),
cron_run delivery fix
β¦ 1) (#5867) Version - Bump workspace version 0.7.0 β 0.7.1 in root Cargo.toml + Cargo.lock CI rationalisation (FND-004 Phase 1 β Rationalise) - Delete checks-on-pr.yml and ci-run.yml β two workflows doing identical work on every PR, producing duplicate signal and double compute cost - Add ci.yml (name: Quality Gate) β single staged pipeline replacing both: Stage 1: fmt + clippy --workspace (fast gate) Stage 2: build matrix, check all-features / no-default-features / 32-bit, benchmarks compile (parallel, gated on Stage 1) Stage 3: nextest (gated on Stage 1) Stage 4: cargo deny check β licenses, sources, advisories (deny.toml already present and triaged) Stage 5: CI Required Gate composite job (branch protection target) - Remove rust_strict_delta_gate.sh β workspace-aware clippy --workspace makes delta comparison implicit (clean baseline = any warning fails) - pre-release-validate.yml: remove pull_request trigger (secrets unavailable on fork PRs caused guaranteed failure on every Cargo.toml bump); remove stale CARGO_REGISTRY_TOKEN check (crates.io publishing removed in #5858) Release workflow - Revert release-stable-manual.yml to gh release create --target for workflow_dispatch (git push approach from #5860 blocked by org Restrict creations rule; Releases API bypasses it; v0.7.1 has no immutable lock) Changelog - Retitle CHANGELOG-next.md to v0.6.9 β v0.7.1, restore full release notes, add entries missing from original draft: otel_headers (#5700), GitHub Copilot onboarding (#5321), Telegram inline_keyboard (#5790), tool_stream fix (#5806), empty tool output (#5565), web theme fixes (#5724, #5767), cron_run delivery fix


Summary
Describe this PR in 2-5 bullets:
masterfor all contributions):master(no output)before scrubbing and forwarding it.Label Snapshot (required)
risk: low|medium|high):risk: mediumsize: XS|S|M|L|XL, auto-managed/read-only):size: XSagent,provider,toolagent: tool execution,provider: custom,tool: shelltrusted contributorChange Metadata
bugmultiLinked Issue
Supersede Attribution (required when
Supersedes #is used)Co-authored-bytrailers added:Noβ not applicablePassValidation Evidence (required)
Commands and result summary:
cargo fmt --all -- --check cargo clippy --all-targets -- -D warnings cargo testcargo fmtclean,cargo clippy --all-targets -- -D warnings0 warnings,cargo test --lib agent::loop_::tests::execute_one_tool_normalizes_empty_success_output -- --exactpasses. CI green on Apr 15 run (lint, test, build, security audit, all gates). (Validation evidence updated by @singlerider β original body incorrectly stated clippy and test were not run; author confirmed in thread that both were run post-feedback.)Security Impact (required)
NoNoNoNoPrivacy and Data Hygiene (required)
passCompatibility / Migration
YesNoNoi18n Follow-Through (required when docs or user-facing wording changes)
Noβ no docs or user-facing copy changed.Human Verification (required)
execute_one_toolsuccess branch to confirm empty successful outputs now become(no output)before provider-facing scrubbing.Side Effects / Blast Radius (required)
(no output)instead of an empty string for this success edge case.Agent Collaboration Notes (recommended)
ghCLIsrc/agent/tool_execution.rs, formatted, committed, pushed, then opened linked issue and PR.AGENTS.md+CONTRIBUTING.md): confirmedRollback Plan (required)
eac18a94or restore the previous success-path assignment insrc/agent/tool_execution.rs.Risks and Mitigations