fix(plugin): make a Laminar trace red iff the turn failed
Problem
Laminar trace status for v4 runs is uninformative in both directions.
Green when the run failed. Staging run fb5dad0c (session 2cbca552, ENG-5640) failed on its 6th LLM step when the provider stream went silent mid-body and the control plane sent a terminal event: error frame. Laminar trace dc9b5851 reports status: success, 0 errored spans — and the failing session.llm / ai.streamText.doStream spans are absent entirely: the turn span ends 00:22:34.59 while its last child ends 00:21:02.13.
The AI SDK opens both spans with endWhenDone: false and ends them inside a transform flush (ai/dist/index.mjs:6834, :7494). An error arriving as a stream chunk becomes Effect.fail(event.error) in session/llm/ai-sdk.ts:264, the consumer stops pulling, flush never runs, and the spans are never ended or exported. Nothing else in the pipeline sets ERROR on anything, so the trace reads as a clean, shorter run.
Red when the run succeeded. Tool.wrap uses Effect.orDie (tool/tool.ts:145), so a tool that throws rejects execute(), and the AI SDK calls recordErrorOnSpan on the ai.toolCall span (ai/dist/index.mjs:2845). Laminar sets a trace to error if any of its spans is error, so one buggy LLM-generated browser_execute snippet reports the whole run as a failure. That is not an edge case — prod, 2 days:
1058 browser_execute (TOOL)
59 apply_patch (TOOL)
50 webfetch (TOOL) 50 grep (TOOL) 45 read (TOOL)
A typical one is the model writing sizeElements where it defined sizeButtons — the agent reads the tool-error, fixes the snippet, and finishes the task successfully.
Change
Establish one invariant: a trace is red iff the turn failed.
processor.ts— demote ERROR to UNSET onai.toolCallspans inonEnd, preserving the message on abcode.tool.errorattribute and leaving the recordedexceptionevent untouched. Mutated in place becausesetStatus/setAttributeare no-ops once a span has ended.plugin.ts— mark the turn span ERROR onsession.error.SessionProcessor.halt(session/processor.ts:679,693) publishes it immediately beforestatus.set(idle), so the span is still open; the existingsession.idlecase ends it.MessageAbortedErroris excluded — stopping a run is not a failure. The recoverableContextOverflowErrorpath deliberately publishes nothing, so auto-compaction retries stay green.
Verification
Ran the real OTel SDK + InMemorySpanExporter through this processor with an ai.toolCall span carrying an ERROR status and a recorded exception, plus a turn span failed via the session.error path:
ai.toolCall status=UNSET | | bcode.tool.error="browser_execute snippet threw: TypeError" | events=1
turn status=ERROR | APIError: stream inactive >90s | bcode.tool.error=undefined | events=0
bun typecheck clean in packages/bcode-laminar.
Not addressed
The dropped LLM spans themselves. When a provider error arrives mid-stream, the failing call's tokens, cost, latency and msToFirstChunk are lost because the AI SDK never ends the span — that needs an upstream fix or a patch to ai. This PR makes the failure visible on the turn span; it does not recover the measurements.
Summary by cubic
Make Laminar traces go red only when the turn fails, and stay green when tools error as part of normal recovery. Fixes misleading status in v4 runs and aligns with ENG-5640.
- Bug Fixes
- Demote ERROR on
ai.toolCallspans to UNSET; preserve the message inbcode.tool.errorand keep the recordedexceptionevent. - Mark the turn span ERROR on
session.errorwhile it’s still open; ignoreMessageAbortedError, and keep auto-compaction (ContextOverflowError) retries green.
- Demote ERROR on