fix(opencode): implement reformatOutput so structured-output agents recover - #803
fix(opencode): implement reformatOutput so structured-output agents recover#803PaysNatal wants to merge 1 commit into
Conversation
…ecover The reformatOutput fallback was a stub that always threw "SDK not implemented", so when an opencode agent (e.g. the planner) emitted non-JSON output — it spends its turn on tool-calls instead of emitting the final JSON block — primary extraction failed and the cluster died with "Agent ... output missing required JSON block" (becoming a zombie). Trivial tasks skip the planner, so only complex tasks were affected. Implement reformatOutput using the opencode CLI as the reformatting backend: - Add a strict "do not use tools / do not read files" instruction to the reformat prompt; otherwise opencode re-enters tool-use on filenames mentioned in the raw output and hangs (>120s vs ~15s). - Spawn the CLI with stdin ignored (stdio:['ignore','pipe','pipe']); execFile leaves stdin as an open pipe and opencode blocks waiting for stdin EOF. Verified: unit test (reformat yields schema-valid JSON in ~14s) and end-to-end (a complex opencode task now passes the planner and progresses instead of becoming a zombie).
Greptile SummaryImplements the opencode-backed structured-output recovery path.
Confidence Score: 2/5This PR is not safe to merge until the fallback is scoped to an available backend and its retry window participates in task cancellation. The common structured-output path now depends on an opencode installation even for other providers, while a hanging backend can keep post-processing alive for three sequential three-minute attempts. Files Needing Attention: src/agent/output-reformatter.js
|
| Filename | Overview |
|---|---|
| src/agent/output-reformatter.js | Implements the missing CLI-backed fallback, but introduces an unconditional optional-provider dependency and an uncancellable worst-case nine-minute retry window. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Initial agent output] --> B{JSON extraction succeeds?}
B -->|Yes| C[Validate structured result]
B -->|No, schema configured| D[Build no-tools reformat prompt]
D --> E[Spawn opencode CLI]
E --> F{Extract and validate JSON}
F -->|Valid| C
F -->|Invalid and retries remain| D
F -->|Retries exhausted| G[Structured-output failure]
Reviews (1): Last reviewed commit: "fix(opencode): implement reformatOutput ..." | Re-trigger Greptile
| // JSON via `--format json`. This makes the reformat fallback work for agents | ||
| // whose own output isn't directly parseable (e.g. an opencode planner that | ||
| // spends its turn on tool-calls instead of emitting the final JSON block). | ||
| const REFORMAT_PROVIDER_BIN = 'opencode'; |
There was a problem hiding this comment.
Fallback requires unrelated provider CLI
When a structured-output agent using any non-opencode provider needs reformatting, this fallback still launches the optional opencode CLI. Systems configured only for the selected provider therefore exhaust every attempt when opencode is absent or unauthenticated, causing the agent to terminate with the same missing-required-JSON error the fallback is intended to recover from.
| // whose own output isn't directly parseable (e.g. an opencode planner that | ||
| // spends its turn on tool-calls instead of emitting the final JSON block). | ||
| const REFORMAT_PROVIDER_BIN = 'opencode'; | ||
| const REFORMAT_TIMEOUT_MS = 180000; |
There was a problem hiding this comment.
Reformat retries bypass task cancellation
When the reformat subprocess hangs, three sequential 180-second attempts run without an abort signal or lifecycle-state check, causing task completion or shutdown to stall for up to nine minutes and allowing further subprocesses to start after cancellation was requested.
Problem
When an agent runs on the opencode provider and is required to emit structured JSON (any agent with a
jsonSchema, e.g. the planner infull-workflow), the cluster frequently dies with:The cluster's backing process exits and the cluster becomes a zombie, failing the whole task.
Root cause
The opencode planner spends its turn on tool-calls (reading files to plan) and does not emit the final JSON plan block as text.
extractJsonFromOutputtherefore finds nothing to parse.parseResultOutputthen falls back toreformatOutput— the mechanism designed exactly for "the agent produced non-JSON output, reformat it into the schema". ButreformatOutputwas shipped as a stub that always rejects:So both the primary extraction and the fallback fail → the agent fails → zombie cluster.
Fix
Implement
reformatOutputusing the opencode CLI as the reformatting backend (opencode reliably emits clean JSON via--format json). Two details were essential:Disable tools in the reformat prompt. Without an explicit "do NOT use any tools / do NOT read files" instruction, opencode sees filenames mentioned in the raw output and re-enters tool-use, hanging the reformat call (>120s observed vs ~15s with the instruction).
Spawn with stdin ignored.
execFileleaves the child's stdin as an open pipe; opencode blocks waiting for stdin EOF and never returns. Usingspawnwithstdio: ['ignore', 'pipe', 'pipe']fixes this. A timeout +SIGKILLguards against any residual hang.The reformat result is still run through
extractJsonFromOutputand validated against the agent'sjsonSchema(existingvalidateAgainstSchema), with up tomaxAttemptsretries.Verification
reformatOutputreturns schema-valid JSON in ~14s.Notes / discussion
opencodebinary. If a generalprovider.callSimpleSDK lands later, this can switch to the commented design (reformat via the agent's own provider). Happy to adjust to whichever direction maintainers prefer.