Auto-Instrumentation: Activity, Not Story
Auto-instrumentation is two lines and it is genuinely enough to see every LLM call your code makes. This page is about the boundary of what it can show — so you know when to reach for the next mode.
What you get
import waxell_observe
waxell_observe.init(api_key="wax_sk_...", api_url="https://waxell.dev")
from openai import OpenAI # imported AFTER init() → patched
client = OpenAI()
client.chat.completions.create(model="gpt-4o", messages=[...])
Every call is captured with model, tokens in/out, cost, and latency. Calls made outside any decorator or context are buffered by the background collector into runs named auto:{model}.
Open one of those runs and you'll see:
auto:gpt-4o ✓ 2.1s
chat gpt-4o ✓ 2.1s 412 tok $0.0021
That is the whole trace. It is accurate and it is useful — but it is a list of events, not an account of what the agent was doing.
Where it stops
Three things are structurally unavailable in this mode, and no amount of extra calls changes that:
- No task or result. There is no run you opened, so there is nothing to attach a conclusion to.
ctx.set_result()doesn't exist here because there is noctx. waxell.step()andwaxell.reason()silently no-op. They look up the current context; with none active they return without recording. Sprinkling them into auto-instrumented code does nothing.- Degrades are invisible. If your code decides not to call the model (a read failed, a key was missing), no LLM call happens — and so nothing at all is recorded. The run simply doesn't exist, or exists with fewer spans than you expect, and nothing explains why.
waxell.step("read health") looks like it should work anywhere. It only records inside an active decorator or context. If you're adding narrative and nothing appears in the trace, this is almost always why.
The one-line fix
Wrap the entry point in @observe. That opens a run, captures the return value, and — crucially — activates a context so the narrative helpers start working:
import waxell_observe as waxell
from waxell_observe import observe
waxell.init()
@observe(agent_name="otlp-debugger") # ← the one line
def diagnose(health: dict) -> str:
waxell.step("read health", output={"legs": list(health)}) # now records
down = [k for k, v in health.items() if v.get("status") != "healthy"]
waxell.reason("classify", thought=f"{len(down)} legs unhealthy",
conclusion="healthy" if not down else "degraded")
return "OTLP pipeline healthy" if not down else f"down: {', '.join(down)}"
You keep every auto-instrumented LLM span you already had. You gain a named run, the input, the return value as the agent's response, and the steps and reasoning in between. Continue in the decorator walkthrough to finish the story with a decision and an explicit result.
When to stay here
Auto-instrumentation alone is the right stopping point when you only need cost and usage visibility — a shared internal library, a script, a notebook, third-party code you can't wrap. The moment a human will open a run and ask "what did it do?", move up a mode.