Skip to content

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

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

feat(agents): add OpenCode agent adapter (#5001)#701
drewdrewthis wants to merge 4 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.)

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>
@drewdrewthis drewdrewthis self-assigned this Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds OpenCodeAgentAdapter and an openCodeAgent factory that drive the sst/opencode coding agent via @opencode-ai/sdk as a scenario agent-under-test. The adapter manages one server-side session per threadId, sends only new user message deltas per turn, applies two-layer error handling (transport + semantic), and includes text-extraction helpers, a full Vitest suite with a fake client, an ADR, and user docs.

Changes

OpenCode Agent Adapter

Layer / File(s) Summary
Config contract and text rendering utilities
javascript/src/agents/opencode/opencode-agent.adapter.ts
Defines Logger interface, noopLogger, OwnedServer structure, OpenCodeAgentAdapterConfig, and all helpers: extractNewUserText, renderContent, renderContentBlock, partsToText, renderNonTextPart, describeError, and safeStringify with circular-reference protection.
Core adapter: session lifecycle, call(), and close()
javascript/src/agents/opencode/opencode-agent.adapter.ts
Implements OpenCodeAgentAdapter with per-thread session tracking, memoized server spawning via ensureClient(), resolveSessionId() for once-per-threadId session creation, call() with user-text validation, delta-only prompt dispatch, session eviction on transport failure, two-layer error checking, text/fallback/no-parts response handling, and close() with ownership semantics.
Public API wiring: factory, exports, and dependency
javascript/src/agents/opencode/index.ts, javascript/src/agents/index.ts, javascript/package.json
Adds openCodeAgent factory function and re-exports OpenCodeAgentAdapter, OpenCodeAgentAdapterConfig, and Logger; wires the submodule into agents/index.ts; adds @opencode-ai/sdk 1.17.9 to dependencies.
Adapter unit and integration tests
javascript/src/agents/__tests__/opencode-adapter.test.ts
Vitest suite using a fake OpencodeClient with spies. Covers adapter/interface conformance, session-per-threadId lifecycle, prompt body correctness, text concatenation, parts filtering, semantic and transport error rejection, empty-parts rejection, non-text fallback, empty-input guard, stale session eviction (R4), close() teardown, and an env-gated e2e integration test.
ADR and user-facing documentation
docs/adr/005-opencode-agent-adapter.md, docs/docs/pages/agent-integration/opencode.mdx, docs/vocs.config.tsx
ADR-005 records all design decisions (class/factory shape, required model, session strategy, DI seam, error handling, memoized spawn, timeouts, teardown ownership, alternatives, and consequences). opencode.mdx documents installation, usage example, auto-start/teardown, API keys, config table, comparison with ClaudeCode adapter, and caveats. Sidebar entry added to vocs.config.tsx.

Sequence Diagram(s)

sequenceDiagram
  participant Scenario
  participant OpenCodeAgentAdapter
  participant OpencodeClient
  participant OpenCodeServer

  rect rgba(70, 130, 180, 0.5)
    note over OpenCodeAgentAdapter,OpenCodeServer: First call — server + session creation
    Scenario->>OpenCodeAgentAdapter: call(input, threadId="t1")
    OpenCodeAgentAdapter->>OpenCodeServer: spawn opencode serve (memoized)
    OpenCodeServer-->>OpenCodeAgentAdapter: OpencodeClient
    OpenCodeAgentAdapter->>OpencodeClient: session.create(model)
    OpencodeClient-->>OpenCodeAgentAdapter: sessionId
  end

  rect rgba(60, 179, 113, 0.5)
    note over OpenCodeAgentAdapter,OpencodeClient: Prompt dispatch and response parsing
    OpenCodeAgentAdapter->>OpencodeClient: session.prompt(sessionId, deltaText, AbortSignal.timeout)
    OpencodeClient-->>OpenCodeAgentAdapter: {data: {parts, info}, error}
    OpenCodeAgentAdapter->>OpenCodeAgentAdapter: check result.error (transport)
    OpenCodeAgentAdapter->>OpenCodeAgentAdapter: check info.error (semantic)
    OpenCodeAgentAdapter->>OpenCodeAgentAdapter: partsToText(parts) or renderNonTextPart fallback
    OpenCodeAgentAdapter-->>Scenario: assistant text string
  end

  rect rgba(220, 120, 60, 0.5)
    note over OpenCodeAgentAdapter,OpencodeClient: Subsequent turn — session reused
    Scenario->>OpenCodeAgentAdapter: call(input, threadId="t1")
    OpenCodeAgentAdapter->>OpencodeClient: session.prompt(same sessionId, newDelta)
    OpencodeClient-->>OpenCodeAgentAdapter: {data, error}
    OpenCodeAgentAdapter-->>Scenario: assistant text string
  end
Loading

Possibly related issues

Poem

🐇 A new agent joins the warren today,
OpenCode sessions hop turn by turn,
Only fresh messages sent on their way,
Two-layer errors have nowhere to squirm.
close() tidies up at the end of the run —
This rabbit's refactoring is finally done! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding an OpenCode agent adapter to the agents module, which is the primary deliverable.
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.
Description check ✅ Passed The PR description is well-detailed and directly related to the changeset, describing an OpenCodeAgentAdapter implementation with clear rationale, design decisions, and acceptance criteria.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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 and others added 2 commits June 23, 2026 18:25
…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>
Comment thread javascript/src/agents/opencode/opencode-agent.adapter.ts Fixed
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>
@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 adapter that integrates with the third‑party @opencode-ai/sdk and spawns the external opencode binary, and it modifies JavaScript package dependencies and lockfiles. Because it adds an integration with an external API/binary (and runtime behavior around session management), it does not meet the low‑risk criterion forbidding changes to third‑party integrations.

This PR requires a manual review before merging.

@drewdrewthis

Copy link
Copy Markdown
Collaborator Author

Review verdict: READY

Multi-agent review of PR #701 (OpenCodeAgentAdapter): 5 reviewers (principles, hygiene, security, test, proof) + 4 personas (Uncle Bob, Metz/Beck, Fowler, design-soundness). All Fix items were addressed in the review-response commits 0d339f85 / 08d4b8de / 1522aee2; no blocking threads remain.

Headline

  • [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) rather than hand-rolling replay. Every hand-rolled piece (partsToText, describeError, create-dedup, AbortSignal timeout) fills a gap the SDK leaves open — none duplicates a shipped capability.
  • [security] No blocking issues. No secrets/PII in the diff; .env (real key) is gitignored and not committed; the auto-spawn passes no user-controlled data; logging sessionId/threadId through a no-op-default logger is an acceptable dev-SDK posture (concurring with the resolved CodeRabbit thread).

Findings addressed (Fix)

# Finding (reviewers) Resolution
1 [principles][uncle-bob][fowler] partsToText cast Record<string,unknown> defeated the type safety the injection seam exists for Narrowed on the Part discriminant (Extract<Part,{type:"text"}>) — 1522aee2
2 [metz-beck][principles] renderContentBlock tool/reasoning branches dead for the user-only caller + duplicated utils.summarizeToolMessage Collapsed renderContent to text-only; deleted renderContentBlock1522aee2
3 [metz-beck] safeStringify WeakSet was global-seen (added, never removed) → mislabeled sibling refs as [Circular] Replaced with the repo's try/JSON.stringify/String pattern — 1522aee2
4 [hygiene] public Logger collided with utils/logger.ts on the barrel; warn unused Renamed → OpenCodeLogger, dropped warn1522aee2
5 [hygiene] domain imports used /index.js vs the no-extension house style Aligned to ../../domain/agents1522aee2
6 [principles][hygiene][fowler] directoryQuery() called twice per RPC Computed once per scope — 1522aee2
7 [uncle-bob][fowler] call() long method + eviction smeared across two sites Extracted interpretPromptResult + one identity-guarded evictSession1522aee2
8 [test] concurrent-dedup test could pass without the fix; transport test had no message assertion; create-failure path untested Deterministic deferred-gated dedup test; /prompt failed/ assertion; added create-failure/eviction test — 1522aee2
9 [proof-reviewer] AC-4 ticked on un-artifacted prose; "875 passed" ≠ CI; test count off PR body now carries the captured run-shape (success=true, messageCount=4, 2 user + 2 assistant turns) and separates CI (27 tests | 1 skipped) from the local 875 superset
10 [principles] CodeRabbit describeInfoError dead null-guard; session-create race; timeout=0 ignored; strict R2 assertion; doc cross-links All resolved in 0d339f85/08d4b8de (the 6 CodeRabbit/github-code-quality threads, all replied + resolved)

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

  • [metz-beck vs prior-design] empty-text fallback vs throw. A tool-only turn returns a readable fallback (never silent "", satisfying AC-4); Metz/Beck argue throwing is more honest for an eval tool. Kept the tested fallback; reasonable follow-up to revisit if tool-only turns prove common. Decide.
  • [uncle-bob] server-lifecycle as a collaborator. serverPromise/ensureClient/close go inert under an injected client — extractable to an OpenCodeServer class. Deferred to keep this PR focused. New Issue candidate.
  • [design-soundness] session.abort on timeout. AbortSignal cancels the HTTP request but not the server-side run; firing session.abort would stop the orphaned generation. Doc already says "best-effort." Follow-up hardening.
  • [hygiene][security] rotate the test OPENAI_API_KEY (in the gitignored worktree .env, never committed) and [hygiene] export+share utils.stringifyValue instead of a local copy. Follow-up.

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