Inspect Scout scans agent transcripts with
scanners: functions that read one transcript and return results citing the messages they are
about. It reads Inspect eval logs, and imports traces from Arize Phoenix, LangSmith, Logfire, W&B
Weave, MLflow, Claude Code and Harbor (ATIF). tracelint ships its rules as Scout scanners, so a
Scout user gets reference-free trajectory checks with no model call: tool errors, loops,
redundant and malformed calls, and with a contract the schema, side-effect and integrity rules. They
run beside the scanners you already use, and every finding links to its message in scout view.
pip install "tracelint[scout]" # Inspect Scout needs Python 3.11+The scanners register under the tracelint namespace (through the inspect_ai entry point), so the
Scout CLI finds them by name. No --model is needed:
scout scan tracelint/tracelint -T ./logs # the keyless rules
scout scan tracelint/tracelint -T ./logs -S tools=tools.json # + the contract rules
scout scan tracelint/tracelint -T ./logs -S rules=R4,R5 -S min_tier=hard_event
scout scan tracelint/tool_loops -T ./logs # one ruleTraces from another platform are imported first, then scanned the same way:
scout import claude_code -T ./transcripts # ~/.claude/projects
scout import atif -T ./transcripts -P path=jobs/ # Harbor (pip install harbor)
scout import phoenix -T ./transcripts -P project=my-agent
scout scan tracelint/tracelint -T ./transcriptsFrom Python:
from inspect_scout import scan, scan_results_df, transcripts_from
from tracelint.integrations.scout.scanners import tool_loops, tracelint
status = scan([tracelint(tools="tools.json"), tool_loops()], transcripts=transcripts_from("./logs"))
rules = scan_results_df(status.location).scanners["tracelint"]
print(rules[["transcript_id", "label", "value"]])| Scanner | Rules | Result |
|---|---|---|
tracelint |
R2a, R4, R5, R6; with tools, also R1, R7, R8, R9, R10, R12 and R13 (only if the contract declares off_limits) |
one result per rule, labelled with its finding type |
tool_errors |
R2a: a tool returned an error | one result |
tool_loops |
R4: the same call repeated with no progress | one result |
redundant_calls |
R5: a call repeating an identical earlier call and result | one result |
malformed_arguments |
R6: arguments the model emitted as invalid JSON | one result |
record_quality |
not a rule: is the record complete? (below) | one result; value is a list of issue codes |
The rule scanners take:
tools: atools.jsoncontract, as a path or its dict.tracelint initdrafts one from a trace, andtracelint init --from-mcpfrom an MCP server's tool list.min_tier: the lowest tier counted.candidate(the default) counts everything,hard_eventdrops heuristics,hard_defectkeeps only provable defects.
tracelint also takes rules, the rule ids to run (-S rules=R4,R13, or a list in Python). Any
rule can be named, including R2b, R3 and R11. Each rule is described in the
rule reference.
Each rule scanner's result follows Scout's grep_scanner:
valueis the number of findings at or abovemin_tier. It is0when there were none, and also when the rule has nothing to look at (R4 on a run with two calls). It isNonewhen the rule could not check the transcript: the contract lacks what it needs (no schema for any called tool, nooff_limits), or the transcript did not record it. A rule that could not run is never reported as a clean zero.explanationlists each finding with its tier and the messages it cites, for example[M5] [M7] [M9] (candidate) 'search' called 3 times in a row with identical arguments and an identical result. Message numbers count from 1 over the transcript's messages, as Scout's own scanners do. It ends with anything not checked (Not checked: no schema for tool 'web_search').referenceslink each cited message, soscout viewjumps to it.metadatahas the same keys on every result:rule(R4),tiers(the count per tier),findings(each finding's full evidence, plus the messages it cites),coverage(how many tool calls or results the rule could check), andnot_checked.labelis the rule's finding type (tool_error_event,loop,redundant_call,malformed_arguments,schema_violation,off_limits_source, …) on thetracelintscanner, and empty on the single-rule ones.
scan_results_df gives the tracelint scanner one row per transcript and rule. It spreads the
metadata of these rows into columns: metadata.rule, metadata.tiers, metadata.findings, and
so on. The single-rule scanners keep metadata as one JSON column.
The rule scanners judge what the agent did. record_quality asks something earlier: does the
transcript record the run completely enough to trust those verdicts, or a judge's? It runs the
record-quality checks behind tracelint doctor, one result per
transcript, and never counts anything against the agent.
scout scan tracelint/record_quality -T ./transcripts-
valueis the list of the record's issues, as codes. It is[]when there are none, the same shape as a Scout multi-label answer.scan_results_dfkeeps the list as JSON text, so read it withjson.loads.Code Meaning emptythe transcript has no steps results_missingtool calls with no recorded result arguments_missingtool calls whose arguments were not recorded placeholder_turnsassistant turns that are a placeholder line ( Executed Bash call_…) instead of the agent's text. Heuristic: known placeholder forms.ends_unansweredthe run stops on a call with no result, so the record may be truncated subagents_missinga subagent was launched ( Task,Agent,spawn_agent, …) but its run is not in the transcript. Heuristic: by tool name.duplicate_call_idstwo calls share an id, so results can't be matched to calls reliably redactionsvalues a filter replaced ( [REDACTED]) -
explanationgives one line per issue, indoctor's words, with the heuristic ones marked(heuristic)and the messages they concern cited, for exampleends on a call with no result: [M10]. -
metadataholds the counts fromtracelint doctor --json(calls,results_recorded,outcome_recorded,placeholder_turns,subagent_runs, …), this transcript'scoveragepercentages (results,outcome,arguments,reasoning), andheuristic, which lists the codes present that rest on a heuristic.
Scout keeps a subagent it imported, such as Claude Code's Task, as an agent span in the
transcript's events, linked from the launching call. A launch whose span is there counts as a
recorded run, so a complete Claude Code import is not reported as missing its subagents.
Outcome coverage runs low on every Inspect transcript: Inspect records when a tool fails but never when it succeeds. That is a measure in the metadata, not an issue.
The scanners read the transcript's messages and its tool events (record_quality also reads
its span_begin events, for subagents). Inspect's chat messages map onto
tracelint's trace:
- System, user and assistant text become messages, and so does assistant reasoning unless it is redacted.
- Each
tool_calls[]entry becomes a tool call, and eachtoolmessage becomes its result, paired bytool_call_id. - A server-side tool use in assistant content (a provider's web search, MCP call or code execution, recorded with its result) becomes a call and its result. R13 reads it like any other call, so a web search that names an off-limits source is caught.
- Arguments that Inspect could not parse (
parse_error) are the model's own text. R6 reports them as ahard_defect.
A tool failed only when a structured signal says so:
- the tool message's
error(Inspect'sToolCallError: a timeout, a parsing failure, a denied approval, …); - the
errorof the call'stoolevent, which is where Scout's importers record a failure (Claude Code'sis_error, a failed span) while leaving the message's field empty; - a truthy
errorfield in a JSON result; - the line
[error] tool reported failurethat Harbor's Claude Code converter appends to a failed result. tracelint's ATIF reader reads the same line, so a Harbor run gets the same verdict either way.
Inspect has no field that says a tool succeeded, so any other result is unknown. R2a then reports error-like output only as a review candidate.
All checks below ran Inspect Scout 0.6.0, importing and scanning through scout import and
scout scan tracelint/....
- Harbor (ATIF), 18 trajectories from the public
Terminal-Bench 2.0 leaderboard:
6 trials each from a Claude Code, a Gemini CLI and a Terminus 2 submission, imported with
scout import atif. On every trajectory, each rule's findings had the same count per tier astracelint check --format atifon the same file. That is 72 rule results, with R2a at 19hard_eventand 36 candidates. Without reading Harbor's failure line, the 19 errors arrived only as candidates or not at all, because the importer drops the flag behind them. - R13 through Scout, 25 trajectories. These were the Terminal-Bench runs where the integrity
pilot found answer lookups, plus other trials of the same tasks, scanned with the Terminal-Bench
off_limitslist from the ATIF page. Both paths flagged the same 12 runs with identical findings. The 7 runs that requested a benchmark source werehard_defect; the other 5 only got one back in a search result (hard_event). The citations pointed at the right messages: the WebSearch call, then its result. - Claude Code, 6 sessions imported with
scout import claude_code, from 23 to 7,214 messages each. The scan ran with no errors. 145 tool errors came through ashard_event; reading the messages alone, all of them were candidates or missed. Those failures exist only on the transcript's tool events, which is why the scanners load events as well as messages. - Record quality. On the same 43 Harbor trajectories,
record_qualityreported the same issue codes astracelint.recordrun through the native ATIF reader, on every trajectory. On the Claude Code sessions, the 12Tasksubagents Scout kept as agent spans counted as recorded. Without the span events, all 12 would have been reported missing.
The scanners lint the transcript's message thread. A subagent whose conversation appears only in
the transcript's events (a Claude Code Task, an Inspect handoff) is not linted as its own run.
tracelint checks what the trajectory proves. It does not judge whether the agent's answer was
right; Scout's LLM scanners and your scorers do that.