Debugging issues
Reading what the engine did
A diagnostic report is a record of everything Parlant did in one turn: which rules it matched, which tools it ran, and what each model call returned. Parlant captures one for every turn. When your agent gives a surprising answer, you download that turn's report and read the real decisions instead of guessing. When a turn fails, the report also holds the error.
- How to find the turn you care about by its trace id
- How to list and download diagnostic reports over the admin API
- What each file in a report holds, and how to read the trace and logs
- What a report cannot tell you
When your agent gives an unexpected answer, the question is rarely what it said. You already know that. The question is why: what information it used, which instructions applied, and what its tools returned. The report answers that question directly.
This page is about getting the report for the turn you care about, and then reading it.
Note that diagnostic reporting is on by default. p.Server(diagnostic_reports=False) turns off all capture and persistence. Leave it on in production, since the reports are exactly what you will want when something goes wrong there.
Finding the turn
Before you can download a report, you need to know which turn you are looking for.
Everything a turn produces shares one trace_id: the customer message, the status events, the tool calls, and the reply. That id is the key to its report.
You can get it from whichever event you are already looking at: in your frontend, in a session's event feed, or in the session's usage map (the per-turn token record, which is keyed by trace id).
For a turn that failed, you do not even have to look it up. The status event Parlant emits on an engine error carries the report's URI, ready to use:
{
"metadata": {
"__parlant__": {
"schema_version": 1,
"diagnostic_report_uri": "/debug/reports/6658572371514ad7897b3bef729a4603"
}
}
}
Note that successful turns carry no such metadata. For those, you find the report through the listing described next.
Downloading a report
Once you have the trace id, the next step is to pull the report itself.
The debug API is administrator-only, so you call it with the admin key. That is the static credential you set in AuthConfig, as Authentication shows.
First, list recent reports, newest first. Each entry gives you the trace id, session id, outcome, timestamps, and a download URI:
GET /debug/reports
GET /debug/reports?limit=10
GET /debug/reports?outcome=error
Then download the one you want. It arrives as a ZIP named after its trace id:
curl -fL -H "Authorization: Bearer $PARLANT_ADMIN_API_KEY" \
-o report.zip \
"$PARLANT_URL/debug/reports/6658572371514ad7897b3bef729a4603"
/healthz also lists the most recent reports with their download links, so an operator who is already looking at health can jump straight to the latest turns. Parlant shows that section only to callers with the admin key, and PARLANT_HEALTHZ_REPORT_LINKS sets how many appear.
Reading a report
Every archive contains:
| File | What it holds |
|---|---|
manifest.json | Schema version, trace and session ids, Parlant version, timestamps, outcome, whether the archive is complete, an exception summary, and the member list. |
trace.json | The span timeline: each stage of the turn, each model call, each tool call, with timings. |
logs.jsonl | Every log line the turn produced, at TRACE and DEBUG detail regardless of the server's log level. |
session.json, events.jsonl | The session and its events as persisted, when available. |
traceback.txt | Error reports only: the full formatted traceback. |
Start with the trace
Start with trace.json to see the shape of the turn.
Two terms come up throughout it. A rule is a plain-language policy or observation that Parlant matches to each turn. A review is a separate check that Parlant runs on a step's proposed tool calls or drafted reply.
The trace shows you which rules matched, which tools ran and for how long, and whether a review rejected a step. If one did, it shows how many times the step regenerated.
Then read the logs
Once the trace has pointed you at the stage that matters, open logs.jsonl at that span.
The matching stage logs each rule's verdict and its reasoning. This is where the question "why didn't my policy fire?" gets answered.
A rule that did not influence the reply was either not loaded, not matched, or matched and outranked. Each case has its own control to check: signals or wording for discovery, the policy's content or observation's condition for matching, and priority or weight for ranking.
Designing policies covers each of them in detail.
Why didn't my policy come up at all?
That question is about discovery: the cheap check that decides whether a rule is relevant to the session at all, before matching judges the specific turn. Rules that pass discovery join the working set (the rules Parlant carries along for the rest of the session).
To see what discovery decided, search logs.jsonl for Rule discovery results. Parlant lists each candidate there with its score and verdict.
Three cases explain most surprises in that listing.
First, a rule that never appears in the results at all. Either it has a code evaluator, meaning your own function decides when it applies instead of the model, or it is already in the working set, so there is nothing left to discover.
Second, a rule that scores as relevant but never joins the working set. That rule depends on another rule that has not matched yet.
Third, a rule whose signals (the example phrasings you give a rule so discovery can find it) seem ignored after you edited them. Parlant builds the classifiers trained on those signals at startup, so an edit needs a restart to take effect.
Log lines from tools you wrote appear in logs.jsonl too, provided they go through p.Server.current.logger, as Accessing state shows. That puts your tool's view of the turn right next to the engine's.
Retention
Parlant treats successful and failed turns differently. It keeps successful turns as bounded snapshots in process memory and builds the ZIP only when you request it, so a successful trace costs no disk.
Errors are different: Parlant writes an error report to disk as soon as the exception is caught, and finalizes it when the turn completes. That is why an error report survives a restart and its URI works right away.
What is available at any time is the newest PARLANT_TRACE_HISTORY_SIZE turns in memory (32 by default), plus the newest PARLANT_ERROR_HISTORY_SIZE error archives on disk (8 by default). Raise the first if you debug successful turns after the fact. Raise the second to keep more error history. Set both to 0 to disable capture entirely.
Note that error archives live in PARLANT_ERROR_REPORTS_DIR, under the Parlant home directory by default. Reports contain customer messages, tool data, and metadata, so treat that directory as sensitive.
What a report can't tell you
The archive is one process's view of one turn. When you run several replicas, you need to download the report from the replica that actually served the turn. The load balancer's access log, or a header you add, will tell you which one that was.
And a report describes a turn that has already happened. If you want to watch decisions as they are made, or to step in, that is what engine hooks are for.
Hooking into the engine covers the hooks in detail.