fix(agent): clear retryable error when agent ends its turn without JSON - #813
fix(agent): clear retryable error when agent ends its turn without JSON#813Lifferado wants to merge 1 commit into
Conversation
When a pipeline agent ends its final turn with plain prose - an early
narrative stop or a turn aborted mid-task by provider stream errors -
every structured-output candidate failed on the first byte and the raw
encoding/json decoder error ("invalid character 'L' looking for
beginning of value") leaked as the step error, misdirecting diagnosis
toward format problems and ending the run with no retry (kunchenguid#811).
parseStructuredTextOutput now returns a dedicated sentinel,
errTurnEndedWithoutJSON, when no candidate parsed AND the text shows no
attempt at JSON at all (no leading '{', no ```json fence). Malformed
output that does attempt JSON keeps today's precise decoder/validation
error. classifyTransient treats the sentinel as retryable, and
runWithRetry re-invokes the next attempt (same session for adapters
that resume one) with a one-time prompt reminder that the final message
must be exactly the bare JSON object.
Regressions: TestFinalizeTextResult_ProseFinalTurnReturnsDedicatedError,
TestFinalizeTextResult_AttemptedJSONKeepsRawParseError,
TestClassifyTransient_TurnEndedWithoutJSONIsRetryable,
TestRunWithRetry_TurnEndedWithoutJSONRetriesWithReminder,
TestRunWithRetry_TurnEndedWithoutJSONRecoversWhenNextAttemptSucceeds.
Fixes kunchenguid#811
Confidence Score: 4/5The malformed embedded-JSON classification should be corrected before merging so truncated JSON attempts do not trigger misleading and unnecessary retries. A response that begins with narration and then starts an incomplete JSON object bypasses the new attempt detector, replacing its precise decoder error with the retryable missing-JSON sentinel. Files Needing Attention: internal/agent/agent.go, internal/agent/agent_test.go Reviews (1): Last reviewed commit: "fix(agent): clear retryable error when a..." | Re-trigger Greptile |
| if strings.HasPrefix(strings.TrimLeft(text, " \t\r\n"), "{") { | ||
| return true | ||
| } |
There was a problem hiding this comment.
Embedded JSON attempts are missed
When narration precedes a truncated JSON object such as Let me finish... {"findings": [, this prefix-only check classifies the response as having made no JSON attempt, causing unnecessary retries and replacing the precise malformed-JSON error with the misleading missing-result sentinel.
| if strings.HasPrefix(strings.TrimLeft(text, " \t\r\n"), "{") { | |
| return true | |
| } | |
| if strings.Contains(text, "{") { | |
| return true | |
| } |
|
Speaking as Kun's firstmate: inspected the live diff vs main at Classification: corrective. Fixes #811: a prose-only final turn no longer leaks VISION: R1 pass (no core-step change); R2 n/a; R3 pass (judgment stays human; retry is mechanical); R4 n/a; R5 pass (clearer attributed error + recorded retry); R6 pass (all adapters get the same reminder path); R7 pass (field incident #811). Required CI: Not auto-merging while the no-mistakes attestation is missing. This is waiting on the author to re-run |
Fixes #811
Root cause
When a pipeline agent ends its final turn with plain prose - an early narrative stop, or a turn aborted mid-task by provider stream errors (usage caps, content filters, 503s) - opencode reports no
info.error = StructuredOutputError, so the #375 path does not trigger. The text fallback infinalizeTextResult/parseStructuredTextOutputthen feeds the narration toencoding/json, and every candidate (whole text, fences, last bare object) fails on the first byte. The step dies withopencode output parse: invalid character 'L' looking for beginning of value (output snippet: "Let me check the remaining ...")after minutes of real work, with no retry: prose is neither transient nor a StructuredOutputError.Fix
internal/agent/agent.go): when no candidate parses AND the text shows no attempt at JSON at all - it neither starts with{nor contains a ```json fence -parseStructuredTextOutputreturns the new sentinel `errTurnEndedWithoutJSON` ("agent ended its turn without the required JSON result"). The step error now reads `opencode output parse: agent ended its turn without the required JSON result (output snippet: "...")`. Malformed output that DOES attempt JSON (leading `{`, fenced JSON, embedded bare object) keeps today's precise decoder/validation error unchanged.internal/agent/retry.go):classifyTransienttreats the sentinel as retryable (identity check viaerrors.Is, so the same words echoed in provider output never retry), bounded by the existing per-adapter retry budget. On such a retry,runWithRetryappends a one-time prompt reminder that the final assistant message must be exactly one bare JSON object; adapters that resume a durable session therefore re-invoke that same session with the reminder.runOncenow receives the effectiveRunOptsso every adapter gets this uniformly.finalizeTextResultasserts the sentinel and that no decoder error leaks; attempted-JSON cases assert old behavior is preserved; classifier tests assert retryability and identity semantics; retry-loop tests assert the untouched first prompt, the appended reminder, exactly-once appending, bounded attempts, and recovery when the next attempt succeeds.The telemetry failure bucket is unaffected: the wrapped message still contains "output parse", which
classifyInvocationFailurealready maps to "parse".