weave-opencode
v0.2.1
Published
W&B Weave observability for opencode — reconstructs gen_ai OpenTelemetry spans from opencode's plugin event stream and ships them to Weave, in-process and off the critical path.
Downloads
472
Readme
weave-opencode
W&B Weave observability for the opencode CLI. Every opencode turn — model calls, token usage, tool calls, and reasoning — becomes a trace in Weave.
This is a port of weave-codex to
opencode. It reuses that project's entire span/emit half verbatim (OpenTelemetry
GenAI conventions → Weave's Agents OTLP endpoint) and swaps the ingest half:
where weave-codex parses Codex rollout files behind a Stop hook, weave-opencode
consumes opencode's in-process plugin event stream and can backfill
completed headless runs from opencode's SQLite store.
Status: live plugin + SQLite collect path. The pipeline builds, type-checks, is unit-tested end-to-end (events/SQLite → spans), and the live plugin has been smoke-tested against a real opencode 1.17.x server/TUI session. Tool, MCP, web-search, and subagent payloads still need broader live validation; see Known caveats / open questions.
How it works
opencode session (long-lived server, plugin loaded in-process)
├─ message.updated / message.part.updated → Accumulator buffers the turn
└─ session.idle
└─ reconstruct completed turn(s) → OTEL span tree → OTLP → Weave
invoke_agent opencode (the turn)
├─ chat <model> (one per assistant message: usage, output)
└─ execute_tool <name> (one per tool: args, result)
headless opencode run
└─ opencode.db
└─ weave-opencode collect --json → reconstruct completed turn(s) → same exporter- In-process, not a daemon. opencode plugins run inside its long-lived
server, so — unlike weave-codex's fire-and-forget shell shim + detached worker
- Unix socket + byte-offset cursor — there is no separate process. The
Accumulatorbuffers the structured event stream in memory;takeCompletedTurns()is the cursor's moral equivalent (emits complete, not-yet-seen turns once, then prunes them).
- Unix socket + byte-offset cursor — there is no separate process. The
- One trace per turn. Turns are stitched into a conversation server-side via
gen_ai.conversation.id(the opencode session id) on every span. - Backdated timelines. Span start/end come from opencode's message/part timestamps, so durations reflect what actually happened. (This is why we build spans through the raw OTel API rather than the Weave TS SDK, same as weave-codex.)
- Never throws into opencode. Every hook is wrapped; failures log to
~/.weave-opencode/logs/collector.logand are swallowed. - Durable idempotency. Both the live plugin and
collectrecord successful emits in~/.weave-opencode/collect-state.json, keyed by project, DB path, conversation id, and turn id.
Quick start
npm install -g weave-opencode # (or run from a local build; see Development)
wandb login # or: export WANDB_API_KEY=...
export WEAVE_PROJECT="entity/project"
weave-opencode install # writes a plugin shim into ~/.config/opencode/plugins/
weave-opencode doctor # confirms plugin path + Weave configRestart opencode so it loads the plugin. Run weave-opencode doctor to confirm
everything resolves. Each completed live turn appears in Weave shortly after
session.idle.
If you do not have an opencode binary on your PATH, the published CLI package
is opencode-ai:
pnpm dlx opencode-ai .Configuration
| Setting | Env var | settings.json key | Default |
| --------------- | -------------------------------- | ------------------- | ---------------------------------- |
| W&B API key | WANDB_API_KEY | wandb_api_key | from wandb login (netrc) |
| Weave project | WEAVE_PROJECT | weave_project | — (required, entity/project) |
| Base URL | WANDB_BASE_URL | wandb_base_url | https://trace.wandb.ai |
| OpenCode DB | OPENCODE_DB | — | ${OPENCODE_DATA_DIR}/opencode.db |
| Capture content | WEAVE_OPENCODE_CAPTURE_CONTENT | capture_content | true |
| Debug logging | WEAVE_OPENCODE_DEBUG | debug | off (errors always log) |
State lives in ~/.weave-opencode/ (settings, collect-state.json, and
logs/collector.log).
Data captured
By default weave-opencode captures span content: prompts, the model's
responses and reasoning, tool-call arguments, and tool results (which include
shell commands, command output, and file contents). This is sent to your
W&B/Weave instance. PII scrubbing and redaction are not implemented — the
same caveat weave-codex carries. Set WEAVE_OPENCODE_CAPTURE_CONTENT=0 to send
structure, token usage, model, and timing only (no prompts/code/output).
Manual smoke test
Use a long-lived opencode process (the TUI, mini UI, or server). This gives the
plugin enough time to flush spans after session.idle.
WEAVE_PROJECT="entity/project" WEAVE_OPENCODE_DEBUG=1 pnpm dlx opencode-ai .In another terminal:
tail -f ~/.weave-opencode/logs/collector.logAfter a turn completes, expect log entries like:
weave-opencode plugin loaded
session flush ... turns=1
emitted 1 turn(s)For a headless smoke test, run a server first and attach a single run request
to that server:
WEAVE_PROJECT="entity/project" WEAVE_OPENCODE_DEBUG=1 pnpm dlx opencode-ai serve --port 4099
WEAVE_PROJECT="entity/project" pnpm dlx opencode-ai run --attach http://127.0.0.1:4099 "Reply with exactly: weave-opencode smoke ok"Plain one-shot opencode run ... starts its own short-lived server and may exit
before the live plugin's async OTLP exporter finishes. For headless and CI
workflows, prefer a normal run followed by collection:
opencode run "Reply with exactly: weave-opencode smoke ok"
weave-opencode collect --jsoncollect reads OpenCode's SQLite store in read-only mode, reconstructs complete
turns, and sends them through the same exporter as the live plugin.
Headless collection
weave-opencode collect [--json] [--db <path>] [--session <id>] [--since <iso-or-epoch-ms>] [--cwd <path>] [--all]- DB path resolution:
--db, thenOPENCODE_DB, then${OPENCODE_DATA_DIR}/opencode.db. - Default scope: sessions for the current working directory updated since the last successful default collect. The first run looks back 24 hours.
- Watermark scans include a 60-second overlap; already emitted turns are skipped through durable state.
--session <id>collects that session regardless of cwd or time filters.--allremoves cwd and time filters while still skipping already emitted turns.--sinceaccepts ISO timestamps or epoch milliseconds.
collect --json prints top-level IDs for turns that are present in Weave,
including newly emitted turns and already_emitted skips:
{
"version": "0.2.1",
"project": "entity/project",
"db": "/abs/path/opencode.db",
"conversation_ids": ["ses_..."],
"trace_ids": ["..."],
"emitted_turns": [
{"turn_id": "...", "conversation_id": "ses_...", "trace_id": "..."}
],
"skipped_turns": [
{
"turn_id": "...",
"conversation_id": "ses_...",
"trace_id": "...",
"reason": "already_emitted"
}
],
"errors": []
}Invalid config, missing/unreadable DBs, missing SQLite tables, malformed selected session data, and export failures exit nonzero. Locked, incomplete, and already emitted turns are reported as skips.
What ports from weave-codex, and what's new
| Layer | File(s) | Status |
| ----------------------------------- | --------------------------------------------------- | --------------------------------------------- |
| OTLP tracer → Weave Agents endpoint | spans/tracer.ts | reused verbatim |
| Span tree + GenAI attributes | spans/emit.ts | reused; multi-provider + subagent attrs added |
| GenAI message payloads | spans/messages.ts | reused verbatim |
| Domain model (ReconstructedTurn) | model.ts | reused; provider/parentSessionId added |
| Config / netrc / settings | config.ts | reused (env vars renamed) |
| Per-session lock, logger | lock.ts, log.ts | reused (renamed) |
| Ingest | rollout/* → source/* | rewritten for opencode |
| Trigger | Codex Stop hook → opencode plugin (plugin.ts) | rewritten |
| Headless backfill | collect/* | SQLite-backed for opencode |
| CLI install | merge hooks.json → plugin shim (install.ts) | rewritten |
New ingest half, all in src/source/:
types.ts— the opencode event/message/part wire shape consumed by the collector. The basic message/part/session-idle path is live-validated; richer part shapes are still the validation seam.reconstruct.ts— pure: a session's buffered messages+parts →ReconstructedTurn[], marking each turn complete/incomplete.accumulator.ts— stateful in-memory buffer;takeCompletedTurns().collector.ts— filters already emitted turns, builds the tracer, and ships turns (reusesemitTurn).
Headless collection lives in src/collect/: SQLite hydration,
default-scope watermarking, durable emitted-turn state, and the collect runner.
Validating the event mapping
All the opencode-shape risk is contained in src/source/types.ts and
src/source/reconstruct.ts; the emit half downstream is provider-agnostic.
To validate a new opencode release or a richer interaction:
- Run an opencode TUI/server session with
WEAVE_OPENCODE_DEBUG=1. - Capture a few real
message.updated/message.part.updated/session.idlepayloads when adding support for a new part shape. - Diff the field names against
types.tsand the fixtures insrc/test-support/fixtures.ts; adjust, and the unit tests will confirm the reconstruction still holds.
Known caveats / open questions
- Chat granularity. Each assistant message becomes one
chatspan. If opencode exposes per-model-call (step) boundaries with per-step usage, splitchatspans per step for parity with weave-codex's per-round-trip spans. - MCP / web-search detection is heuristic (tool name /
state.metadata); confirm against real tool parts. - One-shot live plugin flushes. A run that owns its own short-lived server
can exit before async live export finishes. Use
weave-opencode collect --jsonafteropencode runfor reliable headless/CI capture. - Subagents. opencode child sessions carry a
parentID; this is recorded asopencode.parent_session.id. Nesting child traces under the spawning tool span is not yet done. - Aborted turns. Handled on a best-effort basis via
session.error.
Development
npm install
npm run build # tsc → dist/ (+ chmod the CLI bin)
npm test # vitest: reconstruct + accumulator + end-to-end span tree
npm run typecheck
npm run lint # prettier --check + tsc --noEmitNode >=24 is required for node:sqlite. Tests run against src/ directly
(no dist drift) and point all state at temp dirs, so they never touch a real
~/.config/opencode or ~/.weave-opencode.
npm run check is the same gate CI runs: build, lint, tests, and a repository
secret scan. Install gitleaks locally
before running the full check, or run npm run setup-hooks once to enable the
local pre-commit hook that scans staged changes and rebuilds before each commit.
Release
package.json and package-lock.json are the version source of truth. To
publish:
- Bump the package version in both files, for example with
npm version. - Merge the version bump to
main. - Create and push a matching tag, for example
v0.1.2. - Dispatch the “Publish Package” GitHub workflow with that exact tag.
The workflow publishes to npm via trusted publishing, then creates or updates the GitHub Release for the same tag and marks it as latest. Rerunning the workflow is safe when the package version is already npm's latest version.
Derived from wandb/weave-codex (MIT).
