Skip to content

fix(agent): suppress empty-provider-response from Sentry (TAURI-RUST-4JX)#2790

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/sentry-tauri-rust-4jx-empty-response
May 28, 2026
Merged

fix(agent): suppress empty-provider-response from Sentry (TAURI-RUST-4JX)#2790
graycyrus merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/sentry-tauri-rust-4jx-empty-response

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 27, 2026

Summary

  • Add AgentError::EmptyProviderResponse { iteration } and route agent::harness::session::turn's empty-final-response bail through it, so the existing user-facing string is preserved while the typed variant flows up to run_single.
  • Centralise the "skip Sentry" decision behind a new AgentError::skips_sentry() method covering both MaxIterationsExceeded and the new variant; run_single calls it in place of the inline MaxIterationsExceeded-only check.
  • Keeps Sentry TAURI-RUST-4JX off the radar (~33 events, escalating on 0.56.0) without changing the user-visible error or the AgentError / recoverable semantics.

Why

Latest event payload: Windows user, LM Studio at http://localhost:1234, custom community fine-tune (qwen3.6-27b-heretic-uncensored-finetune-neo-code-di-imatrix-max). Provider response is text_chars=0 thinking_chars=0 tool_calls=0 in 60 ms — completely empty. agent/harness/session/turn.rs:805 then returned:

return Err(anyhow::anyhow!(
    "The model returned an empty response. Please try again."
));

That bubbled to run_single (agent/harness/session/runtime.rs:540), which routes anything other than AgentError::MaxIterationsExceeded through report_error_or_expected("agent", "run_single", …) → Sentry. The result is the same shape as OPENHUMAN-TAURI-99 / -98 (max-iter cap) before that fix: user-state outcome shipped to Sentry as a code-bug.

It's not actionable from Sentry (the user picked a flaky local model), the UI already surfaces the user-facing message, and the deeper "fix" lives in the user's model / provider config. Same call for suppression as the max-iter cap.

What changed

  1. src/openhuman/agent/error.rs — new variant EmptyProviderResponse { iteration: usize } with a Display arm that emits the verbatim user-facing string (so UI surfaces and any external grep/fingerprint contracts hold). New skips_sentry(&self) -> bool method as single source of truth for the suppressed set: today { MaxIterationsExceeded, EmptyProviderResponse }.
  2. src/openhuman/agent/harness/session/turn.rs:805 — replace anonymous anyhow::anyhow! with AgentError::EmptyProviderResponse { iteration: iteration + 1 }.into(). Warn-level breadcrumb that recorded the surfacing decision is preserved.
  3. src/openhuman/agent/harness/session/runtime.rs:540 — replace let is_max_iter = matches!(err.downcast_ref::<AgentError>(), Some(AgentError::MaxIterationsExceeded { .. })) with err.downcast_ref::<AgentError>().is_some_and(AgentError::skips_sentry). Log message generalised to "user-state agent error". Comment updated.
  4. src/openhuman/agent/harness/session/runtime.rs:388 (sanitize_event_error_message) — new arm returning "empty_provider_response" for the Sentry error_kind tag (required by the non-exhaustive match).
  5. src/openhuman/cron/scheduler.rs:37 (agent_error_to_user_message) — new arm returning actionable canned copy: "The model returned an empty response. Try a different model or check your local provider in Settings → AI → LLM." Required by the same non-exhaustive contract; gives cron job failures a useful message instead of the generic fallback.

Tests added (agent::error::tests)

  • empty_provider_response_display_matches_user_facing_string — locks the wire shape against accidental message changes (also a Sentry-fingerprint stability guarantee).
  • skips_sentry_returns_true_for_known_user_state_variantsMaxIterationsExceeded + EmptyProviderResponse.
  • skips_sentry_returns_false_for_real_failures — covers all 7 other variants (ProviderError, ContextLimitExceeded, ToolExecutionError, CostBudgetExceeded, CompactionFailed, PermissionDenied, Other); any future variant that should also suppress must be added to skips_sentry() and this test, both at once.

Test plan

  • cargo test openhuman::agent::error::tests — 9 tests pass (4 new)
  • cargo test openhuman::agent — 751 tests pass, 0 regressions
  • cargo test openhuman::cron::scheduler — 50 tests pass, 0 regressions
  • cargo check --manifest-path Cargo.toml --bin openhuman-core — passes
  • cargo fmt --check on touched files — clean

Post-merge observation: TAURI-RUST-4JX should drop to ~0 events on the next release. The variant still produces structured log::info! lines locally for diagnosability, so a real spike will still be visible in shipped logs (just not in Sentry).

…4JX)

`agent::harness::session::turn` returned an anonymous `anyhow::anyhow!(
"The model returned an empty response. Please try again.")` when the
provider's chat completion contained no text, no thinking, and no tool
calls. That bubbled to `run_single`'s catch-all `report_error_or_expected`
arm and shipped to Sentry as TAURI-RUST-4JX.

The latest event shows the typical trigger: a Windows user running LM
Studio locally with a community fine-tune
(`qwen3.6-27b-heretic-uncensored-finetune-neo-code-di-imatrix-max`) that
returned an empty stream. That's a model / user-config outcome, not an
OpenHuman bug — the UI already surfaces the user-facing string, and
there is no developer remediation path through Sentry.

Mirror the existing `MaxIterationsExceeded` pattern:

1. Add `AgentError::EmptyProviderResponse { iteration }` with a `Display`
   impl that emits the verbatim user-facing string (so UI / fingerprint
   contract is preserved).
2. Replace the anonymous `anyhow!` at `turn.rs:805` with the typed
   variant, retaining the warn-level breadcrumb that records the
   surfacing decision.
3. Introduce `AgentError::skips_sentry()` as the single source of truth
   for which variants get suppressed (`MaxIterationsExceeded` +
   `EmptyProviderResponse`), and call it from `run_single` in place of
   the inline `MaxIterationsExceeded`-only check.
4. Extend `sanitize_event_error_message` (Sentry error_kind tag) and
   `agent_error_to_user_message` (cron job user-facing copy) with arms
   for the new variant — required by the non-exhaustive match contract,
   and gives cron job failures actionable canned copy.

Tests added in `agent::error`:
- `Display` returns the canonical user-facing string (locks the wire
  shape against regressions).
- `skips_sentry()` returns true for both suppressed variants and false
  for every other AgentError variant (positive + negative coverage).

The user still sees the same error, the `Err` still propagates, and
the `AgentError` domain event / `recoverable` semantics are unchanged.
Sentry just stops getting paged for it.

## Test plan
- [x] `cargo test openhuman::agent::error::tests` — 9 tests pass (4 new)
- [x] `cargo test openhuman::agent` — 751 tests pass, 0 regressions
- [x] `cargo test openhuman::cron::scheduler` — 50 tests pass, 0 regressions
- [x] `cargo check --bin openhuman-core` — passes
- [x] `cargo fmt --check` on touched files — clean
@CodeGhost21 CodeGhost21 requested a review from a team May 27, 2026 20:33
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This change introduces typed error classification for degenerate provider responses (no text, no thinking, no tool calls). A new AgentError::EmptyProviderResponse variant with iteration metadata is detected in turn execution, routed through telemetry via a skips_sentry() helper to separate it from true failures, and mapped to user-facing messages in scheduler notifications.

Changes

Empty Provider Response Error Handling

Layer / File(s) Summary
Error Type and Observability Contract
src/openhuman/agent/error.rs
AgentError::EmptyProviderResponse { iteration } variant is added with documentation, Display formatting outputs "The model returned an empty response. Please try again.", and a new public method skips_sentry() returns true for this variant and MaxIterationsExceeded, false otherwise. Tests verify the exact display string and skips_sentry() behavior for suppressed and real-failure variants.
Turn-Level Detection and Error Creation
src/openhuman/agent/harness/session/turn.rs
The empty provider-response failure path returns a typed AgentError::EmptyProviderResponse { iteration: iteration + 1 } instead of a generic anyhow error, enabling type-safe observability downstream.
Telemetry and Event Routing
src/openhuman/agent/harness/session/runtime.rs
sanitize_event_error_message adds a match arm to classify EmptyProviderResponse as "empty_provider_response". run_single generalizes Sentry suppression logic by calling skips_sentry() on the downcast error; when true, it emits structured log::info! instead of reporting through Sentry.
User-Facing Message Mapping
src/openhuman/cron/scheduler.rs
agent_error_to_user_message adds a case for AgentError::EmptyProviderResponse, returning a static notification message indicating the model returned an empty response.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

agent, bug, working

Suggested reviewers

  • M3gA-Mind
  • graycyrus
  • senamakel

Poem

🐰 A response that's empty brings tears to the try,
So we typed it, we routed it, gave users a why.
No Sentry alarms for the model's misstep,
Just logged info softly, with empathy's rep! 🌙

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a new error variant and suppressing it from Sentry reporting, which is the core objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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


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

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. working A PR that is being worked on by the team. bug labels May 27, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

@CodeGhost21 hey! the code looks good to me — clean approach to centralising the Sentry suppression policy behind skips_sentry(), the typed variant is the right call over the anonymous anyhow!, and the test coverage is solid (locking the wire string, both suppressed variants, and all 7 real-failure variants). but there are CI failures on this PR (PR Submission Checklist is failing and most checks are still pending), so i'll hold off on approving until those are green. once CI is clean, i'll come back and approve. let me know if you need any help!

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

@CodeGhost21 CI is green across the board now. The approach is the right one — typed variant instead of anonymous anyhow!, centralised suppression policy behind skips_sentry(), wire string locked by test, and all 7 real-failure variants explicitly excluded from suppression. Good work.

@oxoxDev oxoxDev assigned oxoxDev and unassigned oxoxDev May 28, 2026
@graycyrus graycyrus merged commit 4adf6b6 into tinyhumansai:main May 28, 2026
40 of 49 checks passed
M3gA-Mind pushed a commit to CodeGhost21/openhuman that referenced this pull request May 28, 2026
…eport as expected (Sentry TAURI-RUST-4Z1)

Add `ExpectedErrorKind::EmptyProviderResponse` + `is_empty_provider_response_message`
predicate anchored on the user-facing string `"model returned an empty
response"`. Routes the `web_channel.run_chat_task` re-report through the
demote path (`tracing::warn!`, breadcrumb only) instead of a Sentry
error event.

Root cause: when the provider/model returns a completely empty body
(`text_chars=0 thinking_chars=0 tool_calls=0`), the agent harness
(`agent::harness::session::turn`) bails with the user-facing
`"The model returned an empty response. Please try again."` string.
PR tinyhumansai#2790 (TAURI-RUST-4JX) suppresses the AGENT-layer Sentry event via
the typed `AgentError::EmptyProviderResponse` + `skips_sentry()`. But
`channels::providers::web::run_chat_task` RE-REPORTS the same failure
under `domain=web_channel operation=run_chat_task` after the typed
error was flattened to a `String` at the native-bus boundary — so the
typed suppression can't reach it and it escapes as a fresh Sentry event
(TAURI-RUST-4Z1). Same two-emit-site pattern as the embedding 4P0/4K5
session-expired fix; mirrors how `MaxIterationsExceeded` is suppressed
at both the agent layer and the web_channel `report_error_or_expected`
funnel.

Anchored on `"model returned an empty response"` (not the looser
`"empty response"`) so the internal fall-through paths stay actionable:
`payload_summarizer` (`"summarizer returned empty response, falling
through"`) and `subagent_runner::extract_tool` (`"provider returned an
empty response; returning empty extraction"`) use different subjects
and are NOT silenced — pinned by the rejection test.

3 new tests: web-channel wire shape (verbatim TAURI-RUST-4Z1 body) +
bare user-facing string, and a 3-case rejection contract (summarizer
fall-through, extract-tool graceful empty, generic health-probe empty
body). cargo test --lib core::observability::tests -> 94 passed.

Sentry-Issue: TAURI-RUST-4Z1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants