fix(agents): recover pi JSON output that follows prose#155
Open
cjunxiang wants to merge 3 commits into
Open
Conversation
PiAgent parsed its final assistant message with raw JSON.parse, unlike every other adapter (acp, copilot, opencode, rovodev) which route through parseAgentJson. When the pi agent prefixes a short prose summary before the JSON object (e.g. "Iteration 3 complete. ..."), JSON.parse throws on the first non-JSON token, every iteration fails, and gnhf hits its maxConsecutiveFailures limit and aborts. Route PiAgent through parseAgentJson with the schema validator as the accept predicate and a no-predicate fallback (mirroring opencode), so schema-invalid JSON still surfaces as "Invalid pi output" instead of "Failed to parse pi output". Add a regression test for prose-then-JSON. Fixes kunchenguid#154
The parseAgentJson + validateAgentOutput fallback shape was duplicated identically in parsePiOutput, parseOpenCodeOutput, and parseCopilotOutput (only the function name and the no-JSON error string differed). Extract a single parseAgentOutput(text, schema, agentLabel) next to validateAgentOutput in types.ts and route all three adapters through it. Behavior is preserved: the per-agent SyntaxError message is retained via agentLabel, and each adapter keeps its own caller-side error formatting (pi distinguishes Failed vs Invalid; opencode/copilot use a single message).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gnhf --agent pifails every iteration withFailed to parse pi output: ... is not valid JSON, tripsmaxConsecutiveFailures, and aborts — even though the model did emit valid JSON.PiAgentwas the only adapter parsing its final assistant message with rawJSON.parse; this routes it through the sharedparseAgentOutputlike every other adapter, recovering JSON that follows a prose summary or is wrapped in fences.Closes #154.
Two commits:
fix(agents): recover pi JSON output that follows prose— the bug fix + regression test.refactor(agents): share parseAgentOutput helper across adapters— extract the now-triplicatedparseAgentJson+validateAgentOutputfallback into oneparseAgentOutput(text, schema, agentLabel)intypes.ts; routepi,opencode, andcopilotthrough it. Net −44 lines.Root cause
PiAgentwas the only built-in adapter that used rawJSON.parseon the final assistant message.acp,copilot,opencode, androvodevall route throughparseAgentJson(src/core/agents/json-extract.ts), which strips Markdown fences and recovers the rightmost balanced JSON object — i.e. it tolerates prose/fences around the JSON.Pi frequently writes a short prose summary before the JSON object. The final assistant message captured in a real failing run was:
The leading
Iteration 3 complete.makesJSON.parse(finalText)throwUnexpected token 'I', "Iteration "... is not valid JSON, so the iteration is recorded as a failure. Reproduced across three consecutive iterations ("Build gree...","All work f...","Iteration "). Verified thatparseAgentJsonrecovers the object from that exact text (all four required keys present,success: true).This is also why the run appeared "stuck": the guaranteed 3 failures abort the orchestrator, after which the TUI stays open (
keepTui = status === "aborted" && isTTY) with the elapsed timer still incrementing, so a stopped run looks still-active until Ctrl+C.The fix
parseAgentOutput(text, schema, agentLabel)(now intypes.ts, next tovalidateAgentOutput):parseAgentJson(text, validateAgentOutput-as-predicate)— returns the first (rightmost) JSON object that validates.parseAgentJson(text)— so schema-invalid JSON still surfaces asInvalid pi output(via a re-thrown validation error) rather than being miscategorized asFailed to parse pi output.throw new SyntaxError(...)when no JSON is present at all → preserved asFailed to parse pi output.The refactor preserves behavior exactly: the per-agent no-JSON error string is retained via
agentLabel, and each adapter keeps its own caller-side error formatting (pi distinguishesFailedvsInvalid; opencode/copilot use a single message). Existing error messages are unchanged:"not json"(no object) →Failed to parse pi output✓Invalid pi output✓Verification
tsc --noEmit— cleanvitest run(unit suite, excludinge2e/) — 619/619 passing, including a new regression testrecovers JSON that follows a prose summaryeslint+prettier --checkon changed files — clean