Skip to content

feat(agents): add OpenCodeAgent for OpenCode integration#125

Open
0xCaso wants to merge 3 commits intocodeplaneapp:mainfrom
0xCaso:main
Open

feat(agents): add OpenCodeAgent for OpenCode integration#125
0xCaso wants to merge 3 commits intocodeplaneapp:mainfrom
0xCaso:main

Conversation

@0xCaso
Copy link
Copy Markdown

@0xCaso 0xCaso commented Apr 15, 2026

NOTE: im currently building a workflow based on this where i'll be able to test everything more thoroughly :))

In the meantime i open the PR also for discussion if needed 🤝

Summary

  • Adds OpenCodeAgent, a new CLI agent wrapper for OpenCode — an open-source, terminal-native AI coding agent
  • Implements nd-JSON stream parsing verified against OpenCode's actual source code (run.ts + message-v2.ts)
  • Includes 26 unit tests covering args construction, output interpretation, multi-step tool interactions, reasoning events, token accumulation, error handling, system prompt forwarding, per-call resume, returned usage extraction, structured error propagation, capability registry completeness, and edge cases
  • Includes 5 E2E tests against the real OpenCode CLI (auto-skipped when opencode is not on PATH)

What's included

New files

  • packages/agents/src/OpenCodeAgent.js — Agent implementation (~492 lines) with buildCommand() and createOutputInterpreter()
  • packages/agents/src/OpenCodeAgent.ts — Type declarations (OpenCodeAgentOptions)
  • packages/agents/tests/opencode-support.test.js — 26 unit tests using fake binaries that emit real nd-JSON
  • packages/agents/tests/opencode-e2e.test.js — 5 E2E tests against the real CLI (skipped via describe.skipIf when not installed)

Modified files

  • packages/agents/src/capability-registry/AgentCapabilityRegistry.ts — Added "opencode" to the engine union type
  • packages/agents/src/index.js / index.ts — Added OpenCodeAgent exports
  • packages/agents/src/BaseCliAgent/BaseCliAgent.js — Added stripOscSequences() for OSC escape sequence handling, extractErrorFromJsonPayload() for structured error preservation, step_finish.part.tokens usage parsing for OpenCode token data, and totalTokens fix to prefer cliUsage.totalTokens when present
  • packages/agents/src/BaseCliAgent/extractTextFromJsonValue.js — Added value.part traversal path for nd-JSON formats that nest text inside { part: { text: "..." } }

Bugs discovered and fixed (10 total)

Found and fixed with TDD across two commits:

Commit 1 (5d35219) — initial implementation:

  1. -f flag consuming positional prompt — Fix: added -- separator before prompt in buildCommand()
  2. OSC escape sequences in final stdout — Fix: added OSC stripping pattern to stdoutBannerPatterns
  3. OSC sequences breaking step_start parsing — Fix: added OSC stripping in parseLine() before JSON parsing
  4. OPENCODE_PERMISSION format wrong — Was '"allow"', should be '{"*":"allow"}'
  5. OPENCODE_SYSTEM_PROMPT dead code — Removed entirely (env var doesn't exist in OpenCode)

Commit 2 (adb5922) — contract alignment fixes:
6. systemPrompt dropped by OpenCodeAgent — Fix: prepend params.systemPrompt to prompt text in buildCommand()
7. Per-call resumeSession ignored — Fix: read params.options.resumeSession and map to --session, with per-call precedence over constructor default
8. generate().usage missing OpenCode token data — Fix: taught extractUsageFromOutput() to parse step_finish.part.tokens including total, reasoning, cache.read, cache.write
9. Structured errors overwritten by generic CLI failure — Fix: extractErrorFromJsonPayload() preserves provider error messages on nonzero exit
10. Capability registry incomplete — Fix: added apply_patch, list, websearch, codesearch, question

BaseCliAgent changes are safe for other agents

All changes are format-gated and shape-gated:

  • stripOscSequences() — only affects raw nd-JSON parsing; agents without OSC sequences are unaffected
  • step_finish.part.tokens usage extraction — only matches OpenCode's exact event shape; other agents use message_start, message_delta, turn.completed etc.
  • extractErrorFromJsonPayload() — only runs when outputFormat is json or stream-json; falls back to old stderr/exit behavior otherwise
  • totalTokens fix — uses cliUsage.totalTokens if present, otherwise falls back to sum calculation (same as before)

How to test

# Unit tests (no OpenCode CLI needed)
bun test packages/agents/tests/opencode-support.test.js

# E2E tests (requires `opencode` on PATH)
bun test packages/agents/tests/opencode-e2e.test.js

Verified against OpenCode source

The nd-JSON format, event types, permission model, and CLI flags were all verified directly from OpenCode's source code:

  • packages/opencode/src/cli/cmd/run.ts — emit function, CLI flags
  • packages/opencode/src/tool/registry.ts — built-in tool list
  • packages/opencode/src/config/config.ts — OPENCODE_PERMISSION parsing

0xCaso added 3 commits April 14, 2026 23:54
Preserve Smithers system prompts and per-call session resume semantics,
parse OpenCode step_finish token usage into generate() results, and keep
structured provider errors when the CLI exits nonzero.
feat(agents): add OpenCodeAgent for OpenCode CLI integration
Copy link
Copy Markdown
Contributor

@roninjin10 roninjin10 left a comment

Choose a reason for hiding this comment

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

Review by Codex, not fucory.

I found a few issues that should be fixed before landing:

  1. OpenCodeAgent.buildCommand() can pass both --continue and --session when the agent is constructed with continueSession: true and a call provides resumeSession. OpenCode's run.ts resolves the base session with args.continue ? lastSession : args.session, so the explicit Smithers resumeSession is ignored in that case. This breaks the per-call resume precedence described in the PR. The --continue flag should not be emitted when an explicit per-call or constructor session id is being used, and the test should cover continueSession: true plus resumeSession.

  2. JSON error events currently only emit an AgentCliEvent with ok: false; they do not make generate() fail unless the process exits nonzero. The upstream OpenCode run.ts records session.error in a local error variable but does not appear to use it to set a nonzero exit, while these tests force process.exit(1). If OpenCode exits 0 after a provider/session error, this adapter will resolve successfully, likely with raw NDJSON as result.text. Please either have the interpreter/BaseCliAgent promote a completed ok: false event to a failed result, or have the OpenCode adapter retain the structured error and fail on exit even when the exit code is 0. Add a fake-CLI test that emits errorEvent(...) and exits 0.

  3. The new adapter is exported, but CLI capability discovery still omits it. packages/agents/src/cli-capabilities/getCliAgentCapabilityReport.js still lists only claude/codex/gemini/kimi/pi, and CliAgentCapabilityAdapterId.ts does not include opencode, so smithers agents capabilities and smithers agents doctor will not report the new built-in adapter.

  4. The generic extractTextFromJsonValue() fallback now descends into any value.part. That means OpenCode reasoning events with part.text can be included in the final generate().text path, even though OpenCodeAgent.createOutputInterpreter() deliberately surfaces reasoning as thought events and does not add it to fullText. This is easy to hit with extraArgs: ["--thinking"]. The fallback should only accumulate OpenCode type === "text" parts, or otherwise skip reasoning/tool parts.

Verification notes: in a clean temp checkout, bun test packages/agents/tests/opencode-support.test.js initially failed before running tests because workspace deps/exports were incomplete (@mdx-js/esbuild, then effect, then @smithers/memory/metrics). bun run --cwd packages/agents typecheck passed after adding the missing temp-only dev deps needed by this checkout.

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