Skip to content

fix(opencode): implement reformatOutput so structured-output agents recover - #803

Open
PaysNatal wants to merge 1 commit into
the-open-engine:mainfrom
PaysNatal:fix/opencode-reformat-output
Open

fix(opencode): implement reformatOutput so structured-output agents recover#803
PaysNatal wants to merge 1 commit into
the-open-engine:mainfrom
PaysNatal:fix/opencode-reformat-output

Conversation

@PaysNatal

Copy link
Copy Markdown

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 in full-workflow), the cluster frequently dies with:

🔴 AGENT OUTPUT MISSING REQUIRED JSON BLOCK
Agent: planner, Role: planning, Provider: opencode
Error: Agent planner output missing required JSON block

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. extractJsonFromOutput therefore finds nothing to parse.

parseResultOutput then falls back to reformatOutput — the mechanism designed exactly for "the agent produced non-JSON output, reformat it into the schema". But reformatOutput was shipped as a stub that always rejects:

Output reformatting not available: SDK not implemented for provider "opencode". Agent output must be valid JSON.

So both the primary extraction and the fallback fail → the agent fails → zombie cluster.

Note: TRIVIAL/SIMPLE tasks skip the planner, so they are unaffected. Only tasks complex enough to invoke a structured-output agent (planner, etc.) hit this.

Fix

Implement reformatOutput using the opencode CLI as the reformatting backend (opencode reliably emits clean JSON via --format json). Two details were essential:

  1. 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).

  2. Spawn with stdin ignored. execFile leaves the child's stdin as an open pipe; opencode blocks waiting for stdin EOF and never returns. Using spawn with stdio: ['ignore', 'pipe', 'pipe'] fixes this. A timeout + SIGKILL guards against any residual hang.

The reformat result is still run through extractJsonFromOutput and validated against the agent's jsonSchema (existing validateAgainstSchema), with up to maxAttempts retries.

Verification

  • Unit: feeding a tool-call-style raw output + a schema to reformatOutput returns schema-valid JSON in ~14s.
  • End-to-end: a complex opencode task that previously produced a zombie cluster (planner failing with "missing required JSON block") now passes the planner and progresses (planner completes, worker starts) instead of dying.

Notes / discussion

  • The reformatting backend is hardcoded to the opencode binary. If a general provider.callSimple SDK lands later, this can switch to the commented design (reformat via the agent's own provider). Happy to adjust to whichever direction maintainers prefer.
  • This makes the fallback functional for opencode today without waiting on a full SDK implementation.

…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-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

Implements the opencode-backed structured-output recovery path.

  • Adds a subprocess wrapper with ignored stdin, captured output, and timeout handling.
  • Strengthens the conversion prompt to prohibit tool use.
  • Extracts and schema-validates reformatted output with retries and improved terminal errors.

Confidence Score: 2/5

This 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

Important Files Changed

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]
Loading

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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

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.

1 participant