Skip to content

fix(provider): surface incomplete response streams#53

Merged
omarluq merged 3 commits into
mainfrom
fix/retry-stream-errors
May 28, 2026
Merged

fix(provider): surface incomplete response streams#53
omarluq merged 3 commits into
mainfrom
fix/retry-stream-errors

Conversation

@omarluq

@omarluq omarluq commented May 28, 2026

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65b29456-8d90-447d-be82-a3683d392146

📥 Commits

Reviewing files that changed from the base of the PR and between c812d4e and 10f6ac1.

📒 Files selected for processing (1)
  • internal/assistant/sse.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/assistant/sse.go

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Response-stream processing now recognizes explicit completion and terminal-failure events and preserves usage accordingly.
  • Bug Fixes

    • Improved detection and reporting of provider stream errors; clearer provider-facing error messages and added retry handling for incomplete/closed streams.
  • Tests

    • Consolidated and expanded tests covering streaming completion, failure cases, retry decisions, and stricter diagnostic assertions.

Walkthrough

SSE parsing now recognizes typed response.* events, tracks completion and terminal failures, extracts provider-specific errors, fails on incomplete typed responses, updates retry rules for stream failures, and adapts tests and diagnostics to use response.completed events.

Changes

Stream Completion & Error Handling

Layer / File(s) Summary
Stream completion and error state tracking in sseAccumulator
internal/assistant/sse.go
Adds terminalErr, completed, and sawTypedResponseEvent to sseAccumulator. add() records response event state and conditions processing of response deltas/usage.
Response result detection and provider error helpers
internal/assistant/sse.go
Adds responseHasResultData(), sseProviderError(), and sseErrorMessage() to detect result payloads and build/format provider error codes/messages (including incomplete_details.reason).
parseSSEResult terminal error and incomplete stream detection
internal/assistant/sse.go
parseSSEResult() short-circuits on recorded terminal errors and returns responses_stream_incomplete when a typed response event was seen but the stream never completed.

SSE Stream Failure Tests and Retry Logic

Layer / File(s) Summary
SSE stream completion and failure scenario tests
internal/assistant/client_test.go
Updated tests to use completedSSEEvent()/response.completed; added table-driven failure tests covering early stream close, response.failed, and response.incomplete cases and message propagation.
Retry policy for stream completion failures
internal/assistant/retry.go, internal/assistant/retry_test.go
Adds "responses_stream_incomplete" to retryable error codes and "stream closed before completion" to retryable message patterns. New tests assert retry decisions for stream-incomplete vs non-retryable failures.
Usage parsing tests updated to response.completed event model
internal/assistant/usage_test.go
Usage-related SSE tests now end with response.completed events instead of data: [DONE], preserving usage/text assertions.

Test Infrastructure Refactoring

Layer / File(s) Summary
Table-driven provider hook diagnostics test
internal/assistant/provider_diagnostics_internal_test.go
Consolidates provider hook diagnostics into a table-driven test (providerHookDiagnosticsTestCase) with per-scenario constructors and assertion helpers, using a shared runner to collect lifecycle diagnostics.
Tool lifecycle hook error diagnostic assertions
internal/assistant/tool_lifecycle_test.go
Adds assertHookErrorDiagnostic() and replaces loose hook_errors checks with assertions that hook_count == 1 and hook_errors is a non-empty []string.

Dependencies

Layer / File(s) Summary
regexp2 dependency upgrade to v2.1.0
go.mod
Indirect dependency changed to github.com/dlclark/regexp2/v2 v2.1.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • omarluq/librecode#46: Also modifies internal/assistant/retry.go retry classification logic.
  • omarluq/librecode#42: Related changes to retry/stream-failure handling and persistence of partial progress on failed streams.

Poem

I'm a rabbit in the code-lined glen,
I hop through SSEs now and then,
Completed, failed, or incomplete—
I sniff the stream and make it neat,
Hooray for tests that pass again! 🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author; the description field is empty and provides no information about the changeset. Add a description explaining what incomplete response streams are, why surfacing them is important, and how the fix works (e.g., new error codes, retry logic, and SSE event handling).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(provider): surface incomplete response streams' clearly summarizes the main change—adding detection and handling of incomplete response streams from the provider.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/retry-stream-errors

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

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.75862% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.96%. Comparing base (595102d) to head (10f6ac1).

Files with missing lines Patch % Lines
internal/assistant/sse.go 82.45% 5 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #53      +/-   ##
==========================================
+ Coverage   61.83%   61.96%   +0.13%     
==========================================
  Files         174      174              
  Lines       17250    17304      +54     
==========================================
+ Hits        10666    10722      +56     
+ Misses       5531     5525       -6     
- Partials     1053     1057       +4     
Flag Coverage Δ
unittests 61.96% <82.75%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@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: 0

🧹 Nitpick comments (3)
internal/assistant/client_test.go (1)

83-118: ⚡ Quick win

Prefer table-driven coverage for the new parseSSEResult failure cases.

These three tests are structurally the same and can be consolidated into one table-driven test to reduce duplication and make future SSE error cases easier to add.

As per coding guidelines, **/*_test.go: "Prefer table-driven tests for core behavior and regression tests for terminal rendering bugs".

🤖 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 `@internal/assistant/client_test.go` around lines 83 - 118, Consolidate the
three near-identical tests into a single table-driven test (e.g.,
TestParseSSEResultFailureCases) that iterates over cases each containing a
descriptive name, the SSE stream string, and the expected error substring;
inside the loop use t.Run(name, func(t *testing.T){ t.Parallel(); _, err :=
parseSSEResult(strings.NewReader(stream), nil); require.Error(t, err);
assert.Contains(t, err.Error(), expected) }) so you keep the same assertions
(provider stream closed before completion, server overloaded, provider response
incomplete: content_filter) and reduce duplication while still exercising
parseSSEResult.
internal/assistant/provider_diagnostics_internal_test.go (2)

148-149: ⚡ Quick win

Strengthen hook_errors assertion shape/content checks.

Line 148 currently only verifies key presence. Validate type and non-empty content to avoid false positives in diagnostics regressions.

Proposed fix
 	require.Contains(t, diagnostic, lifecycleErrorsKey)
+	hookErrors, ok := diagnostic[lifecycleErrorsKey].([]string)
+	require.True(t, ok)
+	require.NotEmpty(t, hookErrors)
🤖 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 `@internal/assistant/provider_diagnostics_internal_test.go` around lines 148 -
149, The test only checks that lifecycleErrorsKey exists in diagnostic; update
the assertion to also check the value under diagnostic[lifecycleErrorsKey] is
the expected type and non-empty: after require.Contains(t, diagnostic,
lifecycleErrorsKey) retrieve val := diagnostic[lifecycleErrorsKey], use
require.IsType(t, []interface{}{}, val) or the correct expected type and then
require.NotEmpty(t, val) (or require.Greater(t, len(val.([]interface{})), 0)) to
ensure hook_errors contains a non-empty collection; reference lifecycleErrorsKey
and diagnostic to locate and change the assertions.

30-34: ⚡ Quick win

Strengthen the lifecycleErrorsKey check (range-variable capture is not applicable)

  • The module targets Go 1.26, so the parallel subtest range-variable capture concern doesn’t apply here.
  • The error-path test only does require.Contains(t, diagnostic, lifecycleErrorsKey); it can still pass if diagnostic[lifecycleErrorsKey] is the wrong type or empty—assert the expected type/contents (e.g., non-empty and exact shape/value) for diagnostic[lifecycleErrorsKey].
🤖 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 `@internal/assistant/provider_diagnostics_internal_test.go` around lines 30 -
34, The existing test only checks require.Contains(t, diagnostic,
lifecycleErrorsKey) which can pass for wrong types or empty values; update the
assertion (in the test that calls runProviderHookDiagnosticsTest or inside that
helper where `diagnostic` is produced) to assert the concrete type and non-empty
contents for `diagnostic[lifecycleErrorsKey]` (e.g., use require.IsType(t,
[]string{}, diagnostic[lifecycleErrorsKey]) or require.IsType(t,
map[string]interface{}{}, ...) as appropriate, then require.NotEmpty(t,
diagnostic[lifecycleErrorsKey]) and finally assert the expected shape/value with
require.Equal or require.Subset for the specific error strings), referencing the
`lifecycleErrorsKey` symbol and the `runProviderHookDiagnosticsTest` helper to
locate where to add these stronger checks.
🤖 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.

Nitpick comments:
In `@internal/assistant/client_test.go`:
- Around line 83-118: Consolidate the three near-identical tests into a single
table-driven test (e.g., TestParseSSEResultFailureCases) that iterates over
cases each containing a descriptive name, the SSE stream string, and the
expected error substring; inside the loop use t.Run(name, func(t *testing.T){
t.Parallel(); _, err := parseSSEResult(strings.NewReader(stream), nil);
require.Error(t, err); assert.Contains(t, err.Error(), expected) }) so you keep
the same assertions (provider stream closed before completion, server
overloaded, provider response incomplete: content_filter) and reduce duplication
while still exercising parseSSEResult.

In `@internal/assistant/provider_diagnostics_internal_test.go`:
- Around line 148-149: The test only checks that lifecycleErrorsKey exists in
diagnostic; update the assertion to also check the value under
diagnostic[lifecycleErrorsKey] is the expected type and non-empty: after
require.Contains(t, diagnostic, lifecycleErrorsKey) retrieve val :=
diagnostic[lifecycleErrorsKey], use require.IsType(t, []interface{}{}, val) or
the correct expected type and then require.NotEmpty(t, val) (or
require.Greater(t, len(val.([]interface{})), 0)) to ensure hook_errors contains
a non-empty collection; reference lifecycleErrorsKey and diagnostic to locate
and change the assertions.
- Around line 30-34: The existing test only checks require.Contains(t,
diagnostic, lifecycleErrorsKey) which can pass for wrong types or empty values;
update the assertion (in the test that calls runProviderHookDiagnosticsTest or
inside that helper where `diagnostic` is produced) to assert the concrete type
and non-empty contents for `diagnostic[lifecycleErrorsKey]` (e.g., use
require.IsType(t, []string{}, diagnostic[lifecycleErrorsKey]) or
require.IsType(t, map[string]interface{}{}, ...) as appropriate, then
require.NotEmpty(t, diagnostic[lifecycleErrorsKey]) and finally assert the
expected shape/value with require.Equal or require.Subset for the specific error
strings), referencing the `lifecycleErrorsKey` symbol and the
`runProviderHookDiagnosticsTest` helper to locate where to add these stronger
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: faeb4be1-206a-4638-871b-0ef94b8bf463

📥 Commits

Reviewing files that changed from the base of the PR and between 595102d and 23ba152.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • go.mod
  • internal/assistant/client_test.go
  • internal/assistant/provider_diagnostics_internal_test.go
  • internal/assistant/retry.go
  • internal/assistant/retry_test.go
  • internal/assistant/sse.go
  • internal/assistant/tool_lifecycle_test.go
  • internal/assistant/usage_test.go
💤 Files with no reviewable changes (1)
  • go.mod

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 28, 2026
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@omarluq omarluq merged commit 156aecb into main May 28, 2026
13 checks passed
@omarluq omarluq deleted the fix/retry-stream-errors branch May 28, 2026 22:33
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.

2 participants