Explain — Root-Cause an Opik Trace and Ground It in the Code
Definition of done: a grounded root cause for the requested trace (or pattern), tied to specific evidence spans and paired with exactly one suggested next step. "Grounded" means the explanation names the failing/anomalous span and connects it to the code or data that produced it — not a restatement of the trace. If the target can't be fetched or read, stop at the first genuine blocker and return one concrete next step. A trace dump is not an explanation.
Operate: investigate over the real trace data, reason against the repo, commit to a single most-likely root cause with its evidence — and change no code. This skill is read-only by design.
Inputs
The entry point is /opik-explain <trace-id> (one trace) or /opik-explain <describe the behavior> (a pattern to find and explain). Infer the rest; treat these as optional overrides:
- project name (default: inferred from config/repo) · time window for a pattern (default: recent) · a known-good trace to compare against.
Ask only at a genuine, non-inferable blocker (see Blockers).
Activation — the only in-scope work
1. Resolve the target
- A trace id (uuid-shaped): explain that one trace.
- A behavior/pattern ("hallucinations since the prompt change", "slow responses"): search for the matching set (below), then explain the shared cause.
- Confirm Opik is reachable: if
~/.opik.configexists orOPIK_API_KEYis set, use it. Otherwise → Blocker ("runopik configure, then rerun").
2. Fetch the trace and spans — MCP first, SDK fallback
Check whether the hosted Opik MCP is connected and prefer it; fall back to SDK scripting when it isn't.
- MCP connected: use the MCP to
readthe trace andlist/readits spans. - No MCP: fall back to the SDK.
Either way, read every span's input/output/error/duration.
import opik
client = opik.Opik()
tid = "<trace_id>"
trace = client.get_trace_content(tid) # TracePublic: exposes project_id, input, output, error info — NOT project_name (accessing .project_name raises)
spans = client.search_spans(trace_id=tid) # spans come from a SEPARATE call, not from the trace object
# Reconstruct the tree via each span's parent_span_id (the root span has none).
# Your anchor is the first span that errored, returned wrong output, or dominates the duration.
For a pattern, pull the matching set scoped to the project, then look for the shared failing span across them. With the MCP, one list call does the filtering and ordering server-side — filters is an OQL string, sort is "<field> [asc|desc]", since takes "1h" / "7d":
list(entity_type="trace", project_name="<project>", since="7d",
filters="error_info is_not_empty", sort="start_time desc") # error traces
list(entity_type="trace", project_name="<project>", since="7d",
sort="duration desc") # slow traces (duration in ms)
list(entity_type="trace", project_name="<project>", since="7d",
filters="feedback_scores.hallucination > 0.5") # low-scored traces
list(entity_type="span", project_name="<project>", since="7d",
filters='name = "<span name>" AND error_info is_not_empty') # the shared failing span, across traces
The applied filter is echoed on the first line; a rejected one comes back with what fixes it (schema("list.trace") is the full field reference). Without the MCP, the SDK takes the same grammar:
traces = client.search_traces(project_name="<project>", filter_string="error_info is_not_empty")
Traces are asynchronous; if you just produced the trace, allow a few seconds and confirm the flush ran.
3. Root-cause it
The coding agent root-causes over the fetched data, against the repo — it has the one thing a generic reasoner lacks: the code. Whether the trace came from the MCP or the SDK, the analysis is the same: find the anchor span (error / wrong output / latency dominator), read its input and output, connect it to the code (grep the repo for the span name / function), and state the single most-likely root cause with its evidence spans. Prefer one well-evidenced cause over a list of maybes.
4. Report
Return the root cause, the evidence spans as clickable Opik UI links (the trace redirect URL Opik emits, e.g. .../session/redirect/...?trace_id=THE_ID — never a bare id), and one next step (see Output). If a fix is obvious, name it as the next step; do not apply it (this skill changes no code — handing off to opik-instrument/opik-test or the developer is the next step).
Blockers
Stop at the earliest blocker and return exactly one next step:
- "Run
opik configure, then rerun/opik-explain <trace-id>." - "No trace found for
<id>in project<name>— confirm the id and project, then rerun." - "This environment can't reach Opik — open the trace in the UI and paste its error/output, or run where Opik is configured."
- "Which behavior should I explain? Give a trace id or describe what went wrong."
Output
User-facing: a short human message — the root cause in one or two sentences, the evidence spans as clickable Opik UI links (name + why each matters), and the single next step. Not a raw trace dump, not JSON.
Underneath (for composition / evals), one shape whether the MCP or the SDK path produced it:
status:explained|blocked|not_foundtarget:trace_id+trace_url(the Opik UI link; for a pattern, thetrace_ids+trace_urlssampled)root_cause: one grounded statementevidence:spans(each:name,type, atrace_urldeep-link where available, why it's evidence)next_step: exactly onereasoner:agent(the coding agent root-causes over the trace data and the repo)
Invariants: explained must carry a root_cause and at least one evidence span; blocked/not_found carry exactly one next_step; every path leaves the codebase unchanged.
Examples
Single trace — tool failure. /opik-explain 019fd8a7-.... Fetch trace + spans; the retrieve (tool) span returned empty and the llm span then hallucinated. Open retrieve() in the repo: the query filter is wrong. Root cause = the retrieval filter, evidence = the empty tool span feeding the llm span; next step = "fix the filter in retrieve() (or /opik-test it)". → explained.
Pattern — slowness. /opik-explain why responses got slow this week. list('trace', since="7d", sort="duration desc") (or search_traces without the MCP) for the slowest traces; the same external tool span dominates each. Root cause = that call's latency; evidence = the shared slow span across N traces; next step = "add a timeout/cache around it". → explained.
Blocked — bad id. /opik-explain 123. get_trace_content finds nothing. → not_found: "No trace 123 in project X — confirm the id/project and rerun." (No code touched.)
Anti-patterns
Dumping the span tree without naming a cause; guessing a cause without reading the anchor span's input/output; listing five maybes instead of the one best-evidenced cause; returning bare span/trace ids instead of clickable Opik UI links; editing code (this skill explains; opik-instrument/opik-test or the developer make changes); calling .project_name on a TracePublic (it raises — use project_id).
References
SDK and observability detail live in the opik skill, installed beside this one. Read the files directly — paths are relative to this file: ../opik/SKILL.md (Searching traces — the OQL filter grammar shared by the MCP list tool and search_traces), ../opik/references/production.md (search_traces, error/latency/cost analysis), ../opik/references/tracing-python.md (SDK read APIs), ../opik/references/observability.md (span-type model). If your host lays skills out differently, locate the opik skill's references/ directory.
If the opik skill isn't installed, say so in the report and use https://www.comet.com/docs/opik/ rather than working from memory.