Skip to content

feat(agents): add OpenCode agent adapter (#5001)#701

Open
drewdrewthis wants to merge 5 commits into
mainfrom
feat/5001-opencode-adapter
Open

feat(agents): add OpenCode agent adapter (#5001)#701
drewdrewthis wants to merge 5 commits into
mainfrom
feat/5001-opencode-adapter

Conversation

@drewdrewthis

@drewdrewthis drewdrewthis commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an OpenCodeAgentAdapter (TypeScript) so scenario can simulate and evaluate the OpenCode coding agent (sst/opencode) via the official @opencode-ai/sdk. This brings scenario's evaluation loop to the coding-agent use case for the first time.

Modeled on the in-house Claude Code adapter (a class extends AgentAdapter + a lowercase openCodeAgent(config) factory) and the realtime adapter's dependency-injection idiom. The genuinely new bit is stateful session-per-thread: OpenCode keeps the transcript server-side, so the adapter creates one session per threadId and sends only the new user turn each call — no full-history replay.

Relocation note: this supersedes langwatch/langwatch#5004, where the adapter was built in the wrong repo. Per #5001 it belongs in langwatch/scenario (next to the other adapters). #5004 should close in favor of this PR.

What changed

File What
javascript/src/agents/opencode/opencode-agent.adapter.ts OpenCodeAgentAdapter class + helpers (extractNewUserText, partsToText, renderContent, …)
javascript/src/agents/opencode/index.ts barrel + openCodeAgent(config) factory
javascript/src/agents/index.ts export * from "./opencode" (re-exported through @langwatch/scenario)
javascript/src/agents/__tests__/opencode-adapter.test.ts 23 unit tests (injected fake client) + 1 env-gated live e2e
javascript/package.json pin @opencode-ai/sdk@1.17.9
docs/docs/pages/agent-integration/opencode.mdx + docs/vocs.config.tsx docs page + sidebar (AC-5)
docs/adr/005-opencode-agent-adapter.md architecture decision record

Acceptance criteria — all met

# AC Evidence
AC-1 Implements the scenario AgentAdapter interface instanceof AgentAdapter, role === AGENT; typecheck:all green; consumer import { openCodeAgent } from "@langwatch/scenario" typechecks
AC-2 New session on first call() per threadId; reuse after unit tests assert one session.create per thread across N calls; same path.id reused
AC-3 Sends latest user msg, returns scenario-compatible reply unit tests assert prompt body carries the user text; return is the concatenated assistant text
AC-4 Awaits true completion — real multi-turn coding task, coherent reply ×3 live e2e ran 3/3 PASS (see proof below)
AC-5 Docs: auto-start via createOpencode() + provider key env vars agent-integration/opencode.mdx (docs build green)
AC-6 Part[]→message handles text + skips unknown parts gracefully unit test over [text, tool, step-start, reasoning, unknown, text] → only text, no throw

Design (ADR-005) — went through /decide + devils-advocate

Key calls (full rationale in docs/adr/005-opencode-agent-adapter.md):

  • Dependency-injected OpencodeClient (not vi.mock of the SDK) — the realtime-adapter idiom; tests run against the real OpencodeClient interface, so an SDK envelope change fails the fake's compile.
  • New-user-delta payload, not full-history replay — OpenCode holds state server-side; re-feeding the agent's own prior replies as user text would double-seed context (a defect the devils-advocate flagged). Branch on session-exists only for create-vs-reuse.
  • Two-layer error handlingresult.error (transport) and result.data.info.error (an HTTP-200 reply can carry ProviderAuthError | MessageOutputLengthError | … with empty text). Continuation failures evict the stale session id.
  • Never a silent empty turn — a tool-only turn returns a readable fallback, not "" (AC-4 forbids empty/truncated).
  • R-2 pinned — the session.prompt envelope (result.data.parts for text, result.data.info for metadata) was verified against the installed @opencode-ai/sdk@1.17.9, then re-confirmed live (a real reply's parts are ["step-start","text","step-finish"]).
  • model required — a product choice for reproducible evals (honestly: the SDK's model is optional with a server default).

Human verification

To run the live adapter yourself:

cd javascript
npm i -g opencode-ai            # the adapter shells out to the `opencode` binary
export OPENAI_API_KEY=sk-...    # OpenCode's provider key + scenario's judge/user-sim key
RUN_OPENCODE_E2E=1 npx vitest run src/agents/__tests__/opencode-adapter.test.ts -t "multi-turn coding scenario"

The unit tests need no creds: npx vitest run src/agents/__tests__/opencode-adapter.test.ts.

How I can prove I was successful

AC-4 — live multi-turn scenario (the real proof), captured run. A real scenario.run(...) drives openCodeAgent (auto-spawned opencode serve, openai/gpt-4o-mini) through a two-turn coding task with a real user-simulator and a real LLM judge. Captured transcript of one run — success=true, messageCount=4 (two user turns, two coherent opencode assistant turns; judge returned PASS):

success: true    messageCount: 4
[0] user      : Write a JavaScript function called `add` that takes two numbers and returns their sum.
[1] assistant : Here's the JavaScript function definition for `add`:
                  function add(a, b) { return a + b; }
[2] user      : Now write a simple unit test for the `add` function you just wrote.
[3] assistant : Here's a simple unit test for the `add` function using the Jest testing framework:
                  test('adds 1 + 2 to equal 3', () => { expect(add(1, 2)).toBe(3); });

Run repeatedly (env-gated RUN_OPENCODE_E2E=1, real binary + OPENAI_API_KEY), all green, no truncated/empty turns across runs:

RUN 1 → 1 passed  30.8s      RUN 2 → 1 passed  14.1s  (port 4096 free after → close() teardown verified)
RUN 3 → 1 passed  28.0s      post-review-refactor re-verify → 1 passed  22.8s

SDK chain confirmed live (isolates the binary/key/envelope from the framework):

[smoke] server up at http://127.0.0.1:4096 (1167ms)
[smoke] session.create -> id: ses_10a5cad42ffe195IuYMTWW8ika | error: undefined
[smoke] prompt resolved (5487ms)  | result.error: undefined  | info.error: undefined
[smoke] part types: ["step-start","text","step-finish"]   ← partsToText extracts only "text"
[smoke] ASSISTANT TEXT: "SMOKE_OK"

Gates — CI (green at HEAD) + local:

  • CI (ci-checks (24.x) → the required javascript-complete; plus docs-complete): build:all · lint:all · typecheck:all · test:ci all pass. The opencode unit file runs in CI as 27 tests | 1 skipped = 26 creds-free unit tests (AC-1/2/3/6) + the 1 env-gated AC-4 e2e (skips in CI).
  • Local full vitest run across all workspace projects (CI splits these): 875 passed | 2 skipped | 0 failed — a superset run for my own verification, not the CI gating number.

Note: AC-4's e2e is env-gated (RUN_OPENCODE_E2E=1) and skips in CIcreateOpencode spawns the real opencode binary, absent from CI. AC-4's proof is therefore the captured local run above (transcript + repeated green), not a CI artifact — the same env-gated pattern the Claude Code adapter uses. AC-1/2/3/6 are fully covered by the 26 unit tests in CI.

Closes #5001

Surface declaration

backend-only, no UI surface — scenario SDK adapter (library internals, alongside the other scenario adapters: Claude Code, realtime, judge, red-team); user proof = the AC-4 live multi-turn run above, NOT a langwatch-app UI change. (This is the rare-valid backend-only case — an SDK adapter consumed by developers writing scenario tests, not app UI.)

@drewdrewthis drewdrewthis self-assigned this Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57fe5a1b-8f2f-431f-8ac9-78f86945e3be

📥 Commits

Reviewing files that changed from the base of the PR and between b60ceae and b24d369.

📒 Files selected for processing (1)
  • docs/adr/005-opencode-agent-adapter.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/adr/005-opencode-agent-adapter.md

Walkthrough

Adds OpenCodeAgentAdapter and openCodeAgent wiring for sst/opencode via @opencode-ai/sdk, with session-per-threadId state, delta-only prompts, error handling, tests, ADR, and docs.

Changes

OpenCode Agent Adapter

Layer / File(s) Summary
Config contract and text rendering utilities
javascript/src/agents/opencode/opencode-agent.adapter.ts
Defines the logger/config contracts, owned-server tracking, prompt/result shaping, user-text extraction, assistant text rendering, non-text fallback formatting, error description formatting, and safe stringification helpers.
Session lifecycle and prompt handling
javascript/src/agents/opencode/opencode-agent.adapter.ts
Implements the adapter call path with per-thread session reuse, lazy client/server initialization, delta-only prompt dispatch, timeout handling, transport and semantic error checks, response formatting, stale-session eviction, and owned-server teardown.
Public API wiring: factory, exports, and dependency
javascript/src/agents/opencode/index.ts, javascript/src/agents/index.ts, javascript/package.json
Adds the opencode submodule entrypoint, the openCodeAgent factory, the top-level agents re-export, and the @opencode-ai/sdk dependency.
Adapter unit and integration tests
javascript/src/agents/__tests__/opencode-adapter.test.ts
Adds the Vitest suite with a fake OpencodeClient, covering adapter shape, session reuse, prompt payloads, response parsing, error branches, empty-input rejection, stale-session eviction, timeout validation, close() behavior, and an env-gated integration run.
ADR and user-facing documentation
docs/adr/005-opencode-agent-adapter.md, docs/docs/pages/agent-integration/opencode.mdx, docs/vocs.config.tsx
Adds ADR-005, the OpenCode adapter documentation page, and the OpenCode sidebar entry, covering the adapter design, installation, usage, teardown, configuration, runtime caveats, and navigation link.

Sequence Diagram(s)

sequenceDiagram
  participant Scenario
  participant OpenCodeAgentAdapter
  participant OpencodeClient
  participant OpenCodeServer

  Scenario->>OpenCodeAgentAdapter: call(input)
  OpenCodeAgentAdapter->>OpenCodeAgentAdapter: extractNewUserText(newMessages)
  OpenCodeAgentAdapter->>OpenCodeServer: spawn opencode serve
  OpenCodeServer-->>OpenCodeAgentAdapter: OpencodeClient
  OpenCodeAgentAdapter->>OpencodeClient: session.create(model)
  OpencodeClient-->>OpenCodeAgentAdapter: sessionId
  OpenCodeAgentAdapter->>OpencodeClient: session.prompt(sessionId, deltaText, AbortSignal.timeout)
  OpencodeClient-->>OpenCodeAgentAdapter: {data, error}
  OpenCodeAgentAdapter-->>Scenario: assistant text string
Loading

Suggested labels

ai-reviewed, prove-it-clean

Suggested reviewers

  • 0xdeafcafe
  • sergioestebance
  • rogeriochaves

Poem

🐇 A new OpenCode path begins to gleam,
One session per thread, a hopping stream.
Fresh messages in, and responses out,
With tests and docs to map it out.
The rabbit twitches, ears held high —
This little burrow learned to fly.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding the OpenCode agent adapter.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the OpenCode adapter work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/5001-opencode-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
javascript/src/agents/opencode/opencode-agent.adapter.ts (1)

170-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reorder class methods to keep public methods first and private helpers at the bottom.

call/close (public API) currently appear after private helpers. Reordering improves scanability and aligns with project conventions.

As per coding guidelines, **/*.ts: “In TypeScript classes, place public methods first, private methods at the bottom, and group related methods together.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@javascript/src/agents/opencode/opencode-agent.adapter.ts` around lines 170 -
317, The public methods `call()` and `close()` are currently positioned after
private helper methods like `logger`, `ensureClient()`, `directoryQuery()`, and
`resolveSessionId()`. Reorder the class methods so that the public `call()` and
`close()` methods appear first, followed by all private helper methods. This
improves code scanability and aligns with the project convention of placing
public methods before private ones.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/docs/pages/agent-integration/opencode.mdx`:
- Around line 35-158: The documentation page references several scenario
framework APIs (scenario.run, userSimulatorAgent, judgeAgent, scenario.user,
scenario.agent, scenario.judge) and mentions a comparison to Claude Code without
providing links to their respective documentation pages. Add markdown links to
connect these API references to their documentation: link scenario.run,
userSimulatorAgent(), and judgeAgent() to the Writing Scenarios page, add a
dedicated link to the Judge Agent page when judgeAgent() is first mentioned,
link userSimulatorAgent() to the User Simulator Agent page, and ensure the
Claude Code comparison section references the agent integration guides. Use the
existing markdown link patterns from the codebase to maintain consistency with
other documentation pages.

In `@javascript/src/agents/__tests__/opencode-adapter.test.ts`:
- Around line 351-355: The current try/catch block around adapter.call() allows
the test to silently pass if no error is thrown, since the message-length
assertion inside the catch block would never execute. Replace this try/catch
pattern with a single await expect() assertion using .rejects to strictly verify
that adapter.call() throws an error and that the error message has length
greater than zero. This ensures the test fails if adapter.call() unexpectedly
succeeds without throwing.

In `@javascript/src/agents/opencode/opencode-agent.adapter.ts`:
- Around line 273-275: The condition in the ternary operator for the timeout
configuration uses a truthiness check on this.config.timeout, which treats zero
as falsy and skips timeout wiring when the value is explicitly set to 0. Change
the condition to explicitly check whether this.config.timeout is not undefined
(or not null) instead of relying on truthiness, so that an explicitly configured
timeout value of 0 is properly respected and passed to the AbortSignal.timeout()
method.
- Around line 259-261: The logger.log call in the OpenCode agent adapter is
logging raw sessionId and input.threadId values verbatim, which exposes
sensitive session and conversation metadata if logs are shared or centralized.
To fix this, replace the raw sessionId and input.threadId identifiers with
hashed or truncated versions before including them in the log message. Consider
using a hashing function or displaying only a partial identifier (like the first
few characters) to maintain logging utility while protecting sensitive metadata.
- Around line 206-229: The resolveSessionId method has a race condition where
two concurrent calls with the same threadId can both observe that no session
exists and then both call client.session.create, creating duplicate sessions.
Fix this by adding a private Map to track pending session creation promises
(e.g., pendingSessionCreations). At the start of resolveSessionId, after
checking this.sessions.get(threadId), also check if there is a pending creation
promise in the pendingSessionCreations Map for that threadId and await it if one
exists. When creating a new session, store the creation promise in
pendingSessionCreations before calling client.session.create, then remove it
after the session is successfully stored in this.sessions. This ensures only one
session creation happens per threadId even with concurrent calls.

---

Nitpick comments:
In `@javascript/src/agents/opencode/opencode-agent.adapter.ts`:
- Around line 170-317: The public methods `call()` and `close()` are currently
positioned after private helper methods like `logger`, `ensureClient()`,
`directoryQuery()`, and `resolveSessionId()`. Reorder the class methods so that
the public `call()` and `close()` methods appear first, followed by all private
helper methods. This improves code scanability and aligns with the project
convention of placing public methods before private ones.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec49c418-4f63-4323-accb-d83320fb9c0e

📥 Commits

Reviewing files that changed from the base of the PR and between b819849 and 2a87b72.

⛔ Files ignored due to path filters (1)
  • javascript/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • docs/adr/005-opencode-agent-adapter.md
  • docs/docs/pages/agent-integration/opencode.mdx
  • docs/vocs.config.tsx
  • javascript/package.json
  • javascript/src/agents/__tests__/opencode-adapter.test.ts
  • javascript/src/agents/index.ts
  • javascript/src/agents/opencode/index.ts
  • javascript/src/agents/opencode/opencode-agent.adapter.ts

Comment thread docs/docs/pages/agent-integration/opencode.mdx
Comment thread javascript/src/agents/__tests__/opencode-adapter.test.ts Outdated
Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts Outdated
Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts
Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts Outdated
drewdrewthis added a commit that referenced this pull request Jun 23, 2026
…0, strict test, doc links

Addresses CodeRabbit findings on #701:
- Stability (race): `resolveSessionId` was check-then-create — two concurrent
  first-calls on the same threadId could both create. Now stores the in-flight
  CREATE PROMISE per thread (one session.create, evicted on failure).
- Functional (timeout=0): a truthiness check silently ignored an explicit `0`.
  Add `timeoutSignal()` validation — non-positive/non-finite timeout throws a
  clear error before any RPC (mirrors the Claude Code sibling).
- Test quality: the R2 semantic-error test used a try/catch that passed silently
  if call() resolved → a strict `.rejects.toThrow(/MessageOutputLengthError/)`.
- Docs: add See-also cross-links (writing-scenarios, user-simulator, judge-agent).

Adds 3 unit tests (concurrent-create dedup, timeout=0 throws, negative timeout).
All 26 unit tests + AC-4 live e2e green; typecheck/lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts Fixed
drewdrewthis added a commit that referenced this pull request Jun 23, 2026
Multi-reviewer pass on #701 (principles/hygiene/test + Uncle-Bob/Metz-Beck/
Fowler personas; design-soundness PASSed — correctly uses opencode's stateful
session primitive, the opposite of the #687 replay miss):

- partsToText: narrow on the `Part` discriminant (`Extract<Part,{type:"text"}>`)
  instead of a `Record<string,unknown>` cast — restores the compile-time safety
  the injection seam exists for.
- renderContent: collapse to a text-only flattener; delete `renderContentBlock`
  (its tool-call/tool-result/reasoning branches were dead — `extractNewUserText`
  only renders user-role messages — and duplicated `utils.summarizeToolMessage`).
- safeStringify: fix a real bug — the WeakSet was global-seen (added, never
  removed), mislabeling sibling refs as "[Circular]". Use the repo's
  try/JSON.stringify/String pattern (true circular → String fallback).
- Rename public `Logger` → `OpenCodeLogger` (collided with utils Logger on the
  package barrel); drop the unused `warn`.
- Extract `interpretPromptResult` + a single identity-guarded `evictSession`;
  compute `directoryQuery()` once per RPC; harden `close()` against a failed
  spawn; align domain imports to the no-extension house style.
- Tests: deterministic concurrent-dedup (gated deferred), `/prompt failed/`
  transport assertion, + a session.create-failure/eviction test.

27 unit tests + AC-4 live e2e green; build/lint:all/typecheck:all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@drewdrewthis

drewdrewthis commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Review verdict: READY

Reviewed at: b24d369 · Run: review (own-PR)

No blocking concerns. All review threads resolved at this SHA; CI green at HEAD.

Since the originally-reviewed SHA (1522aee)

This verdict re-affirms the multi-agent review below at the current HEAD. The only changes in this branch's own commits since 1522aee are:

  • Rebase onto origin/main — resolved the pnpm-lock.yaml conflict by regenerating the lockfile (pnpm install --frozen-lockfile + build + typecheck all green locally, then CI green at HEAD). The adapter source is byte-identical to 1522aee: git diff 1522aee b24d369 -- javascript/src/agents/opencode javascript/src/agents/__tests__/opencode-adapter.test.ts javascript/src/agents/index.ts is empty.
  • Two ADR doc-nits (docs/adr/005-opencode-agent-adapter.md, commit b24d369) closing the two CodeRabbit threads raised on the rebased commit: added a text language specifier to the directory-tree fence (MD040), and corrected the documented config field type LoggerOpenCodeLogger to match the implementation. Both threads replied + resolved.
  • AC-4 re-verified live this session — the env-gated RUN_OPENCODE_E2E=1 multi-turn coding scenario ran 3/3 PASS (17.8s / 17.9s / 19.1s; real opencode binary, openai/gpt-4o-mini, real LLM judge; close() teardown verified — no leaked port-4096 server).

Headline (multi-agent review at 1522aee — still valid; adapter code unchanged)

  • [design-soundness] PASS — the opposite of the scenario#687 miss. Verified against the installed @opencode-ai/sdk@1.17.9 .d.ts: the adapter uses opencode's stateful session primitive (session.create once per thread → reuse id on session.prompt) instead of hand-rolling replay. Every hand-rolled piece (partsToText, describeError, create-dedup, AbortSignal timeout) fills a real SDK gap — none duplicates a shipped capability.
  • [security] No blocking issues. No secrets/PII in the diff; .env (real key) is gitignored and uncommitted; the auto-spawn passes no user-controlled data; sessionId/threadId logging routes through a no-op-default logger.
  • [principles / hygiene / test / proof] All Fix items addressed. The original review's 10 Fix findings were resolved in 0d339f8/08d4b8d/1522aee (replayed verbatim as the rebased commits): partsToText narrowed on the Part discriminant; dead renderContentBlock removed; safeStringify de-globalized; LoggerOpenCodeLogger rename; one-RPC directoryQuery; extracted interpretPromptResult + identity-guarded evictSession; deterministic concurrency/eviction/create-failure tests; AC-4 proof artifacted. No blocking threads remain.

Non-blocking (Decide / follow-up) — not gating

  • [uncle-bob] server-lifecycle as a collaboratorserverPromise/ensureClient/close go inert under an injected client; extractable to an OpenCodeServer class. New Issue candidate.
  • [design-soundness] session.abort on timeoutAbortSignal cancels the HTTP request but not the server-side run; doc already says "best-effort." Follow-up hardening.
  • [hygiene / security] rotate the test OPENAI_API_KEY (in the gitignored worktree .env, never committed) and export+share utils.stringifyValue instead of the local copy. Follow-up.

Verdict is prose, not a GitHub approval; this does not flip GitHub review state. NEVER merge — the human merges.

@drewdrewthis drewdrewthis requested review from rogeriochaves and removed request for rogeriochaves June 25, 2026 11:34
drewdrewthis and others added 4 commits June 26, 2026 17:20
Add OpenCodeAgentAdapter (TypeScript) so scenario can simulate/evaluate
the OpenCode coding agent (sst/opencode) via @opencode-ai/sdk, mirroring
the in-house Claude Code adapter (class + lowercase `openCodeAgent`
factory) and the realtime adapter's injection idiom.

- Session-per-threadId: one server-side OpenCode session per thread,
  reused across turns; sends only the new user delta (not a full-history
  replay) because OpenCode holds the transcript server-side.
- Completion primitive: awaits `session.prompt()` (resolves only after
  the assistant finishes; no SSE). Two-layer error handling (transport
  `result.error` + semantic `info.error`); empty-text fallback so a
  tool-only turn never yields a silent "".
- Testability via dependency-injected OpencodeClient (no SDK module mock).
- Pins @opencode-ai/sdk@1.17.9 (response envelope verified against the
  installed package, R-2). Design recorded in docs/adr/005.

Closes #5001

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hable `describeError`

Addresses a github-code-quality finding: the `error == null` guard in
`describeInfoError` was provably dead (its only caller is guarded by
`if (infoError)`, so `error` is always truthy). Merge the two near-identical
`describeTransportError`/`describeInfoError` helpers into a single
`describeError` — the null guard is now reachable via the `session.create`
call site (`created.error` may be null when only `!data.id` tripped), and the
duplication is gone. Output behavior is preserved (transport: "<msg> (status N)";
semantic: "<name>: <msg>"); all 23 unit tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0, strict test, doc links

Addresses CodeRabbit findings on #701:
- Stability (race): `resolveSessionId` was check-then-create — two concurrent
  first-calls on the same threadId could both create. Now stores the in-flight
  CREATE PROMISE per thread (one session.create, evicted on failure).
- Functional (timeout=0): a truthiness check silently ignored an explicit `0`.
  Add `timeoutSignal()` validation — non-positive/non-finite timeout throws a
  clear error before any RPC (mirrors the Claude Code sibling).
- Test quality: the R2 semantic-error test used a try/catch that passed silently
  if call() resolved → a strict `.rejects.toThrow(/MessageOutputLengthError/)`.
- Docs: add See-also cross-links (writing-scenarios, user-simulator, judge-agent).

Adds 3 unit tests (concurrent-create dedup, timeout=0 throws, negative timeout).
All 26 unit tests + AC-4 live e2e green; typecheck/lint clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-reviewer pass on #701 (principles/hygiene/test + Uncle-Bob/Metz-Beck/
Fowler personas; design-soundness PASSed — correctly uses opencode's stateful
session primitive, the opposite of the #687 replay miss):

- partsToText: narrow on the `Part` discriminant (`Extract<Part,{type:"text"}>`)
  instead of a `Record<string,unknown>` cast — restores the compile-time safety
  the injection seam exists for.
- renderContent: collapse to a text-only flattener; delete `renderContentBlock`
  (its tool-call/tool-result/reasoning branches were dead — `extractNewUserText`
  only renders user-role messages — and duplicated `utils.summarizeToolMessage`).
- safeStringify: fix a real bug — the WeakSet was global-seen (added, never
  removed), mislabeling sibling refs as "[Circular]". Use the repo's
  try/JSON.stringify/String pattern (true circular → String fallback).
- Rename public `Logger` → `OpenCodeLogger` (collided with utils Logger on the
  package barrel); drop the unused `warn`.
- Extract `interpretPromptResult` + a single identity-guarded `evictSession`;
  compute `directoryQuery()` once per RPC; harden `close()` against a failed
  spawn; align domain imports to the no-extension house style.
- Tests: deterministic concurrent-dedup (gated deferred), `/prompt failed/`
  transport assertion, + a session.create-failure/eviction test.

27 unit tests + AC-4 live e2e green; build/lint:all/typecheck:all clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@drewdrewthis drewdrewthis force-pushed the feat/5001-opencode-adapter branch from 1522aee to b60ceae Compare June 26, 2026 17:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
docs/adr/005-opencode-agent-adapter.md (3)

107-107: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove spaces inside code span elements.

Line 107 has spaces inside the code span `session.create` — specifically the leading space before session.create within the backticks. This triggers MD038.

-On a thread's first call: ` client.session.create({ body: { title: \`scenario:${threadId}\` } })`.
+On a thread's first call: `client.session.create({ body: { title: \`scenario:${threadId}\` } })`.

Wait, re-reading: the backtick escaping in the markdown source shows ``` for template literal. Let me re-check the actual source. The line shows:

On a thread's first call: ` client.session.create({ body: { title: \`scenario:${threadId}\` } })`.

The space after the opening backtick and before client is the issue.

-On a thread's first call: ` client.session.create({ body: { title: \`scenario:${threadId}\` } })`.
+On a thread's first call: `client.session.create({ body: { title: \`scenario:${threadId}\` } })`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/005-opencode-agent-adapter.md` at line 107, Remove the extra leading
space inside the inline code span in the ADR text so the `client.session.create`
example starts immediately after the opening backtick. Update the markdown
sentence containing the `client.session.create` snippet to eliminate the
whitespace that triggers MD038 while preserving the rest of the example
unchanged.

Source: Linters/SAST tools


109-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove spaces inside code span elements.

Line 109 has a leading space inside the code span before path.id.

-On all subsequent calls for that thread: reuse the stored ` sessionId`. This is the
+On all subsequent calls for that thread: reuse the stored `sessionId`. This is the
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/005-opencode-agent-adapter.md` at line 109, Remove the leading space
inside the inline code span in this ADR text so the `path.id` reference is
rendered without extra spacing. Update the markdown content around the
session/`claude --resume` discussion to keep the code span clean and consistent
with other inline code formatting.

Source: Linters/SAST tools


178-183: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Verify table formatting renders correctly.

The table uses <name> in the fallback render column which may be interpreted as HTML. Consider escaping or using backticks.

-| Empty text but non-text parts present | Return a readable fallback render (e.g. `[tool: <name>]`) — AC-4 forbids silent empty turns |
+| Empty text but non-text parts present | Return a readable fallback render (e.g. ``[tool: `<name>`]``) — AC-4 forbids silent empty turns |

Actually, looking more carefully, the <name> is inside backticks in the source? Let me re-check. The source shows:

| Empty text but non-text parts present | Return a readable fallback render (e.g. `[tool: <name>]`) — AC-4 forbids silent empty turns |

The <name> is inside square brackets inside backticks, so it should render as literal text. However, some markdown parsers may still interpret <name> as HTML. Safer to use &lt;name&gt; or rephrase.

-| Empty text but non-text parts present | Return a readable fallback render (e.g. `[tool: <name>]`) — AC-4 forbids silent empty turns |
+| Empty text but non-text parts present | Return a readable fallback render (e.g. `[tool: &lt;name&gt;]`) — AC-4 forbids silent empty turns |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/005-opencode-agent-adapter.md` around lines 178 - 183, The table
entry in the ADR is using a literal <name> placeholder inside the fallback
render example, which may be parsed as HTML by some markdown renderers. Update
the text in the table row under the response handling conditions to either
escape the angle brackets or rephrase the example, and verify the rendered
markdown for the ADR section still reads correctly with the placeholder shown as
plain text.
javascript/src/agents/opencode/opencode-agent.adapter.ts (2)

186-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move public methods before private helpers.

Line 186 starts private helpers before the public call, and close appears after interpretPromptResult. Please reorder the class so public methods (call, close) are grouped before private helpers.

As per coding guidelines, **/*.ts: “In TypeScript classes, place public methods first, private methods at the bottom, and group related methods together.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@javascript/src/agents/opencode/opencode-agent.adapter.ts` around lines 186 -
407, Reorder OpenCodeAgentAdapter so the public API comes first: move the public
call and close methods above the private helpers, then keep the private
utilities like logger, ensureClient, directoryQuery, timeoutSignal,
evictSession, resolveSessionId, and interpretPromptResult together at the
bottom. Preserve the existing behavior and method bodies, only change the class
method ordering to match the TypeScript class guideline.

Source: Coding guidelines


299-304: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete the public API JSDoc surface.

call should list its thrown error conditions, and the exported helpers should either be made module-private or documented as public utilities with @param, @returns, and usage examples.

As per coding guidelines, javascript/**/*.{ts,tsx}: “Document all public APIs and interfaces with JSDoc comments including parameter descriptions, return types, error conditions, and usage examples.”

Also applies to: 433-542

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@javascript/src/agents/opencode/opencode-agent.adapter.ts` around lines 299 -
304, The public API JSDoc is incomplete in opencode-agent.adapter.ts: the `call`
method needs explicit `@throws` documentation for its error conditions, and the
exported helper functions around the same area should either be made
module-private or documented as public utilities. Update the JSDoc on `call` and
any exported helpers with `@param`, `@returns`, error conditions, and usage
examples so the documented surface matches the exported API.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/005-opencode-agent-adapter.md`:
- Line 70: The fenced code block in the OpenCode agent adapter ADR is missing a
language specifier, which triggers the markdown lint rule. Update the fenced
block around the interface declaration in the document to use the appropriate
TypeScript fence by matching the existing OpenCodeAgentAdapterConfig snippet,
and ensure the closing fence remains unchanged.
- Around line 85-91: The ADR’s OpenCodeAgentAdapterConfig is documenting the
wrong logger type name, which conflicts with the actual adapter interface.
Update the logger field in the documented config to match the implementation
symbol OpenCodeLogger from opencode-agent.adapter.ts so cross-references stay
consistent. Keep the rest of the config description unchanged.

---

Nitpick comments:
In `@docs/adr/005-opencode-agent-adapter.md`:
- Line 107: Remove the extra leading space inside the inline code span in the
ADR text so the `client.session.create` example starts immediately after the
opening backtick. Update the markdown sentence containing the
`client.session.create` snippet to eliminate the whitespace that triggers MD038
while preserving the rest of the example unchanged.
- Line 109: Remove the leading space inside the inline code span in this ADR
text so the `path.id` reference is rendered without extra spacing. Update the
markdown content around the session/`claude --resume` discussion to keep the
code span clean and consistent with other inline code formatting.
- Around line 178-183: The table entry in the ADR is using a literal <name>
placeholder inside the fallback render example, which may be parsed as HTML by
some markdown renderers. Update the text in the table row under the response
handling conditions to either escape the angle brackets or rephrase the example,
and verify the rendered markdown for the ADR section still reads correctly with
the placeholder shown as plain text.

In `@javascript/src/agents/opencode/opencode-agent.adapter.ts`:
- Around line 186-407: Reorder OpenCodeAgentAdapter so the public API comes
first: move the public call and close methods above the private helpers, then
keep the private utilities like logger, ensureClient, directoryQuery,
timeoutSignal, evictSession, resolveSessionId, and interpretPromptResult
together at the bottom. Preserve the existing behavior and method bodies, only
change the class method ordering to match the TypeScript class guideline.
- Around line 299-304: The public API JSDoc is incomplete in
opencode-agent.adapter.ts: the `call` method needs explicit `@throws`
documentation for its error conditions, and the exported helper functions around
the same area should either be made module-private or documented as public
utilities. Update the JSDoc on `call` and any exported helpers with `@param`,
`@returns`, error conditions, and usage examples so the documented surface
matches the exported API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72800338-6327-4e45-8a0d-ab8fb353d2c2

📥 Commits

Reviewing files that changed from the base of the PR and between 0d339f8 and b60ceae.

⛔ Files ignored due to path filters (1)
  • javascript/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • docs/adr/005-opencode-agent-adapter.md
  • docs/docs/pages/agent-integration/opencode.mdx
  • docs/vocs.config.tsx
  • javascript/package.json
  • javascript/src/agents/__tests__/opencode-adapter.test.ts
  • javascript/src/agents/index.ts
  • javascript/src/agents/opencode/index.ts
  • javascript/src/agents/opencode/opencode-agent.adapter.ts
✅ Files skipped from review due to trivial changes (2)
  • javascript/src/agents/index.ts
  • docs/docs/pages/agent-integration/opencode.mdx
🚧 Files skipped from review as they are similar to previous changes (4)
  • javascript/src/agents/opencode/index.ts
  • javascript/package.json
  • docs/vocs.config.tsx
  • javascript/src/agents/tests/opencode-adapter.test.ts

Comment thread docs/adr/005-opencode-agent-adapter.md Outdated
Comment thread docs/adr/005-opencode-agent-adapter.md
Address CodeRabbit review on PR #701:
- add a text language specifier to the directory-tree fence (MD040)
- correct config interface logger type Logger -> OpenCodeLogger to match the implementation in opencode-agent.adapter.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@drewdrewthis drewdrewthis added the slack-requested Slack PR review request posted label Jun 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Automated low-risk assessment

This PR was evaluated against the repository's Low-Risk Pull Requests procedure and does not qualify as low risk.

The PR introduces a new runtime integration with a third‑party system (@OpenCode‑ai/sdk and the opencode binary), including spawning a local process and adding a pinned dependency and lockfile entries. That changes external integrations and runtime behavior, which is explicitly excluded from the low-risk category in the policy.

This PR requires a manual review before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

slack-requested Slack PR review request posted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant