Skip to content

Tracing

A ThinkLess trace records one unit of work, such as a support ticket or an API request, as a tree of spans. It is the audit record of what the agent decided, which plane decided it, how confident it was, what it cost and where the time went.

Spans

Kind Created by Plane What it records
run engine.run(name) The root. Attributes you pass, such as mode or ticket_id, plus the input when content capture is on
step engine.step(name) A named phase of your workflow
decide decide(), decide_many(), extract() The questions, their thresholds, and every resulting decision
attempt the cascade rule, model or llm One provider call: questions asked, answers, confidences, which were accepted, model, tokens, cost
llm engine.generate() llm Model, tokens, cost, stop reason, prompt and completion
tool @thinkless.tool tool Arguments and result
rule engine.rule(name, value) rule A deterministic check made in application code

Span ids are W3C Trace Context compatible (128-bit trace ids, 64-bit span ids), so they map directly onto OpenTelemetry.

Setting up a tracer

from thinkless import ConsoleSink, Engine, JSONLSink, Tracer

tracer = Tracer(
    sinks=[JSONLSink(".thinkless/traces"), ConsoleSink()],
    capture_content=True,
)
engine = Engine(providers, llm=llm, tracer=tracer)

with engine.run("support_ticket", ticket_id=ticket["id"], mode="hybrid") as run:
    ...
print(run.summary())

engine.run() returns a handle whose summary() gives a TraceSummary for that run: LLM calls, decisions by plane, escalations, tokens, cost and time by plane.

Sinks

Sink Purpose
JSONLSink(dir) One JSON Lines file per trace, named <UTC time>_<run name>_<trace id>.jsonl. The root span is always the last line. This is the audit log.
MemorySink() Keeps spans in memory; used by tests and benchmarks.
ConsoleSink() Prints each finished trace as a tree in the terminal.
OTelSink(tracer_provider) Mirrors spans into OpenTelemetry. Requires the otel extra.

A sink is any object with optional on_start(span), on_end(span), flush() and close() methods. Sink failures are logged and never reach application code: tracing must not take the agent down.

Content capture and privacy

With capture_content=True (the default, THINKLESS_CAPTURE_CONTENT), traces include the run input, decision states, prompts, completions, extracted values and tool arguments and results. That is what makes a trace useful for debugging, and also what makes it sensitive.

With capture_content=False, those fields are replaced by [redacted], while structure, timings, labels, confidences and token counts are kept. Choice labels and yes/no values come from your question definitions, not from user input, so they are always recorded; extracted field values are treated as user content.

OTelSink drops content attributes by default even when the tracer captures them, because telemetry backends are often shared more widely than local audit files. Pass include_content=True to export them.

OpenTelemetry

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from thinkless import Tracer
from thinkless.tracing.otel import OTelSink

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
tracer = Tracer([OTelSink(provider)])

Spans are named <kind> <name>, carry their ThinkLess fields as thinkless.* attributes, and generation spans carry the GenAI semantic convention attributes (gen_ai.operation.name, gen_ai.request.model, gen_ai.provider.name, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens). Any OTLP backend works: Jaeger, Grafana Tempo, Honeycomb, Langfuse, Arize Phoenix and others.

The viewer

thinkless trace view                  # every trace under THINKLESS_TRACE_DIR
thinkless trace view path/to/traces --out report.html --no-open

The viewer is a single HTML file with the data embedded and no external requests. It shows a per-mode comparison when traces carry a mode attribute, a filterable run list, and for each run a waterfall colored by plane with the decisions of every decide span inline. Clicking a span shows its attributes, prompt and completion.

From the terminal:

thinkless trace ls
thinkless trace show .thinkless/traces/demo/20260925T044421Z_support_ticket_1a2b3c4d.jsonl

Cost

Attempt and generation spans carry cost_usd, computed from token usage and a price table (thinkless/data/pricing.toml, overridable with THINKLESS_PRICING). Local models cost 0. Hosted models without a price entry are recorded with cost_known=false rather than a guessed number. Prices change; the bundled table lists its source and date for every entry.