Skip to main content

Decorator: Narrate With Three Lines

The @observe decorator already gives you a run per call, the inputs, and the return value recorded as the agent's response. This walkthrough adds the three things a reader needs on top of that: what phases it went through, what it decided, and what it concluded.

Start: the plain decorated agent​

import waxell_observe as waxell
from waxell_observe import observe

waxell.init()

@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict]) -> str:
past_due = [a for a in accounts if a["days_past_due"] > 30]
return f"{len(past_due)} accounts past due; drafted recovery for {len(past_due)}"

Open the run. You'll see a run named dunning-officer, its inputs, and the returned string as agent_response. Good — but the middle is a black box.

Step 1 — name the phases​

waxell.step() works inside a decorated function because the decorator has activated a context. Each call becomes a step span the UI groups under the run and lists on the Timeline tab.

@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict]) -> str:
waxell.step("read billing", output={"accounts": len(accounts)})

past_due = [a for a in accounts if a["days_past_due"] > 30]
waxell.step("classify", output={"past_due": len(past_due)})

return f"{len(past_due)} accounts past due; drafted recovery for {len(past_due)}"

Step 2 — record the decision​

Decisions are where a reader most wants a why. Ask for the context (waxell_ctx is injected when your function declares it) and record the options, the choice, and the reasoning:

@observe(agent_name="dunning-officer")
def review_accounts(accounts: list[dict], waxell_ctx=None) -> str:
waxell.step("read billing", output={"accounts": len(accounts)})
past_due = [a for a in accounts if a["days_past_due"] > 30]
waxell.step("classify", output={"past_due": len(past_due)})

action = "escalate" if len(past_due) > 5 else "soft-reminder"
waxell_ctx.record_decision(
name="recovery_action",
options=["soft-reminder", "escalate"],
chosen=action,
reasoning=f"{len(past_due)} past-due accounts; threshold is 5",
)
return f"{len(past_due)} accounts past due; action={action}"

Step 3 — set an explicit result​

The decorator already records your return value as the response. set_result is for the structured conclusion — the thing dashboards, delivery, and the narrator should read rather than parse out of a sentence:

    waxell_ctx.set_result({
"summary": f"{len(past_due)} past due; action={action}",
"past_due": len(past_due),
"action": action,
})
return f"{len(past_due)} accounts past due; action={action}"

What the run looks like now​

dunning-officer                       ✓  0.4s
step read billing accounts=12
step classify past_due=3
decision recovery_action → soft-reminder (3 past-due; threshold 5)
result {summary: "3 past due; action=soft-reminder", ...}
response "3 accounts past due; action=soft-reminder"

A reader gets the whole arc from the run header without opening a single span — and the AI narrator now has the agent's own account to summarize instead of guessing from span names.

Don't forget the failure path​

If the read fails, don't just return early — that leaves a run with no explanation. Record the failure so it shows as a red span:

    if accounts is None:
waxell_ctx.record_tool_call(name="billing.read", status="error",
error="no admin token", tool_type="api")
waxell_ctx.set_result({"summary": "could not read billing (no admin token)"})
return "could not read billing"

Full pattern in Tell the Story of a Run → Recording what degraded.