Skip to content

feat: add OpenCode V2 support - #131

Open
alexhawat wants to merge 2 commits into
DEVtheOPS:mainfrom
alexhawat:feat/opencode-v2-support
Open

alexhawat wants to merge 2 commits into
DEVtheOPS:mainfrom
alexhawat:feat/opencode-v2-support

Conversation

@alexhawat

Copy link
Copy Markdown

Summary

Adds OpenCode V2 support alongside the existing V1 plugin, in one package.

  • V1 keeps the named OtelPlugin export, unchanged.
  • V2 reads a new default export (id: devtheops.otel, setup()), built on V2's granular
    event stream.

Closes #128.

Why the handlers were re-architected

The V1 handlers consume events that no longer exist in V2 (session.idle,
message.updated, message.part.updated, permission.*, command.executed, session.diff).
V2 replaced them with a granular taxonomy, so the handlers were rewritten rather than adapted.

What V2 emits

Signal V2 source
opencode.session run span (AGENT, turn root) prompt hook → session.execution.succeeded/failed/interrupted
Session count + session.created log session.created, or lazily on first sight
session.duration, session.token.total, session.cost.total session.usage.updated, turn end
opencode.llm step span (LLM) + token/cost/cache/model metrics session.step.started/ended/failed
opencode.tool.<name> span (TOOL) + tool.duration session.tool.input.started, called, success/failed
retry.count session.status (retry)
user_prompt log (linked to the run span) first step of each turn
session.error log + error span status session.execution.failed
W3C trace-context header injection model.request hook

Parity gaps

Not ported (no V2 equivalent): message/part spans, permission telemetry, command.executed,
and session.diff (lines-of-code/commit) metrics.

Shared changes (V1 + V2)

  • src/config.ts: additive redactSecrets / redactValues options (V1 ignores them).
  • src/headers.ts: replace the Bun global with node:child_process so the headers helper is
    runtime-agnostic; the "terminated by SIGTERM" error is preserved.
  • src/redact.ts: new, best-effort secret masking.

V2 specifics worth knowing

  • V2 loads a global plugin per location and delivers every event to every instance. The
    V2 entry keeps its OTel providers and tracing state on globalThis (one shared instance,
    flushed but never shut down) and dedupes events by event.id.
  • V2 does not emit session.idle, and session.created is absent for pre-existing sessions,
    so the turn root span is anchored on session.execution.*.
  • session.tool.called/success/failed carry no tool name; it is read from
    session.tool.input.started.data.name and correlated by call id.

Testing

bun run lint, bun run check:jsdoc-coverage (87.96%), bun run typecheck, and bun test
(331 pass) are all green locally. Added tests/redact.test.ts. Verified end-to-end against
OpenCode 2.0.15 with live OTLP export (spans/logs/metrics).

Notes for review

  • src/v2/util.ts is a copy of src/util.ts retyped against the V2 context. Happy to refactor
    to a shared generic util if you prefer.
  • src/v2/ is self-contained and does not touch the V1 files (only src/index.ts gains one
    re-export line), so V1 behaviour is unchanged.

Add V2 support alongside V1 in one package: V1 keeps the named OtelPlugin
export; V2 uses a new default export (id devtheops.otel, setup) built on the
V2 granular event stream (session.step.*, session.tool.*, session.usage.*,
session.execution.*, session.retry.scheduled).

Shared changes:
- config.ts: additive redactSecrets/redactValues options
- headers.ts: use node:child_process instead of the Bun global

Refs DEVtheOPS#128
Subagent sessions produced orphan root run spans and dangling parents:
the run span was created on the prompt hook (before session.created) and
always used the root context. The run span is now created on
session.execution.started, nests under the subagent session span, and is
marked is_subagent; the subagent session span parents to the parent's
opencode.tool.subagent span; session spans end on execution end.
@alexhawat

Copy link
Copy Markdown
Author

Follow-up commit: fix(v2): nest subagent traces under the parent dispatch tool span

Testing against a real subagent surfaced two problems in the initial V2 port:

  • Subagent turns produced an orphan root run span, because the run span was created on the prompt hook (which fires before session.created is processed) and always used the root context.
  • The subagent's session span was only ended on session.idle — which V2 never emits — so LLM/tool spans pointed at a parent span that was never exported.

Changes:

  • Create the run span on session.execution.started (event-ordered after session.created), not the prompt hook.
  • Nest subagent run spans under their session span, marked session.is_subagent.
  • Parent the subagent session span to the parent's opencode.tool.subagent span.
  • End subagent session spans on execution end so they export.
  • Prefer the active run span when resolving LLM/tool parents.

Resulting tree (one trace per execution):

opencode.session (parent run)
  └─ opencode.tool.subagent
       └─ opencode.session (subagent session, is_subagent=true)
            └─ opencode.session (subagent run)
                 ├─ opencode.llm
                 ├─ opencode.tool.read
                 └─ opencode.tool.shell

Verified against OpenCode 2.0.15 with a real subagent dispatch; bun run lint, check:jsdoc-coverage (87.96%), typecheck, and bun test (331 pass) are green.

@alexhawat
alexhawat force-pushed the feat/opencode-v2-support branch from 21b9fe1 to 6808031 Compare September 23, 2026 17:34
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.

[Feature]: Support for OpenCode v2

1 participant