Skip to content

No LLM spans emitted on opencode 1.14.x — event stream renamed (message.updated and message.part.updated never fire) #9

Description

Summary

Running @braintrust/trace-opencode@0.0.8 against opencode CLI 1.14.48 (the version bundled in cloudflare/sandbox:0.10.1-opencode), the plugin loads correctly and the root OpenCode: … span lands in Braintrust with a duration — but zero LLM spans are emitted for any turn. Braintrust dashboards show LLM calls: 0, Input: —, Output: — for every session.

Root cause

opencode 1.14.x emits a different event stream than the plugin handles. I captured a full event-receive histogram from the plugin's own event hook over a real ~25-minute session (5 opencode sub-sessions, lots of tool activity):

event_type                        count   plugin handler?
─────────────────────────────────  ─────  ─────────────────────────────────────
message.part.delta                  548   NO  (catchall "unhandled event")
session.status                      122   NO  (catchall)
session.diff                         61   NO  (catchall)
file.watcher.updated                 16   NO  (catchall)
file.edited                          16   NO  (catchall)
todo.updated                         10   NO  (catchall)
session.updated                       5   NO  (catchall)
session.idle                          5   YES (closes turn span ✓)
tui.toast.show                        1   NO  (catchall)

message.updated                       0   needed by handleMessageUpdated to emit LLM span
message.part.updated                  0   needed to accumulate llmOutputParts (text + tool calls + reasoning)
session.created                       0   plugin works around this via chat.message lazy-init ✓

Cross-check from the plugin's own I/O ndjson (LOG_TO_FILE=auto):

    784 \"hook\":\"event\"
    222 \"hook\":\"tool.execute.before\"
    220 \"hook\":\"tool.execute.after\"
    112 \"hook\":\"experimental.chat.system.transform\"
     10 \"hook\":\"chat.message\"               # = 5 calls × (input+output)

So chat.message fires correctly (5 opencode sessions × 1 hook call each — the lazy-init root-span path in the chat.message handler runs). What's broken is everything after the turn span gets created:

  • dist/index.js's handleMessageUpdated is gated on message.updated, which opencode 1.14.x never sends. The LLM span is emitted from this handler with the message's time.completed, tokens, providerID/modelID, and accumulated text — none of which the plugin can collect.
  • handleMessagePartUpdated is gated on message.part.updated, which is also never sent. The plugin's state.llmOutputParts / state.llmToolCalls / state.llmReasoningParts maps stay empty for every turn.

opencode now streams partials via message.part.delta (token-by-token) and never publishes a single "message complete" or "part complete" event the plugin recognises.

Reproduction

  1. npm install @braintrust/trace-opencode@0.0.8.
  2. Run inside cloudflare/sandbox:0.10.1-opencode (opencode 1.14.48), with the plugin registered in ~/.config/opencode/opencode.json:
    {
      \"plugin\": [\"@braintrust/trace-opencode\"]
    }
  3. Set BRAINTRUST_API_KEY, TRACE_TO_BRAINTRUST=true, BRAINTRUST_PROJECT=…, BRAINTRUST_DEBUG=true, LOG_TO_FILE=auto.
  4. Start a session, fire any prompt, let it finish.
  5. Observe in Braintrust: root span lands, has a duration, but LLM calls: 0.
  6. grep 'service=braintrust-trace unhandled event' $XDG_DATA_HOME/opencode/log/*.log | awk '{print $NF}' | sort | uniq -c → reproduces the histogram above.

Suggested fix direction

The minimal path (without changing the public schema in Braintrust):

  1. Accumulate output via message.part.delta. Treat the delta.field=\"text\" payload as an append into state.llmOutputParts.get(messageID). Tool-call deltas land via partial JSON in delta too — buffer and parse on completion.
  2. Use session.idle (or a synthesized terminal marker per-message) as the LLM-completion event. When session.idle fires for a session that has a currentTurnSpanId, emit one LLM span with the buffered output, using currentTurnStartTime for start and wallClock.now() for end (token counts may be unavailable — set tokens: undefined instead of 0 so the dashboard doesn't show "0 tokens").
  3. Stop logging the catchall unhandled event … at INFO — opencode 1.14+ sends thousands of these per session and they're swamping the log file.

I'd be happy to draft a PR if helpful. Reference data and full plugin-decision dump available; happy to share if it'd help triage.

Environment

  • @braintrust/trace-opencode: 0.0.8 (latest as of writing)
  • opencode CLI: 1.14.48 (from cloudflare/sandbox:0.10.1-opencode base image)
  • @opencode-ai/sdk: 1.16.0 (host)
  • Runtime: Cloudflare Workers + Sandbox containers, hosting opencode in server mode and driving it via client.session.promptAsync

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions