{% from 'runs/_runtime.html' import what_ran %} {% extends "base.html" %} {% block title %}{{ trial.task_base }} — Nullius{% endblock %} {% block crumb %}/ {{ job.job_id }} / {{ trial.task_base }}{% endblock %} {# The chart grid and the label chips are shared with the compare page's sides, so there is one definition of each and the two cannot drift apart. They take `view_url` below as their link builder. #} {% import "traces/_charts.html" as ui %} {# Every filter control is a link to this same page with a different `hide` set, so a filtered view has a URL — shareable, bookmarkable, and exactly what save-trace writes out. `viewing` rides along so toggling a chip while reading a saved view does not silently drop back to the producer's capture. `cf` is the charts' own legend state, which is a different query key and a different thing: it changes what one chart draws, never what the trace contains. It defaults to the current one so a control that only means to change `hide` carries the legend through untouched, and vice versa. #} {% macro view_url(f, cf=none) -%} {{ url_for('trial', job_id=job.job_id, trial_name=trial.trial_name, trace=viewing or none, tab='trace', **view_query(f, cf if cf is not none else chart_filter)) }} {%- endmacro %} {% from 'traces/_judgement.html' import judgement_panel %} {% block body %}
{{ model }}· {{ trial.source|dsname }} {%- if trial.agent_version %}·v{{ trial.agent_version }}{% endif %} {%- if job.runner %}·{{ what_ran(job) }}{% endif %}
These passed before the patch was applied and fail after it — a regression, not just a missed fix.
{{ trial.exception_message }}
The bar spans the trial's full {{ trial.dur_total|secs }}. {% if job.runner %}“Other” is the wait for the judge, which reads the answers once its share of the {{ items }} is answered.{% else %}“Other” is harness overhead Harbor doesn't break out — the same tax for every agent.{% endif %} {% if trace and trace.has_spans %}The Trace tab shows recorded span timing and its measured or inferred provenance.{% elif traj and traj.has_step_clock %}Conversation reports time between source steps; these are not per-tool durations.{% else %}No per-step clock is available. Conversation preserves the recorded order.{% endif %}
{{ traj.error }}One column per LLM step, height ∝ input tokens; the gray band is what came from cache. {%- if traj.cache and traj.cache.convention == 'anthropic' %} Anthropic-style usage: cache writes are added back onto the step that paid for them.{%- endif %}
{% elif traj.token_series or traj.total_reasoning_tokens is not none %} {# one call: no growth to draw, so the call itself - what came from cache, what was new, what the model wrote #} {% set p = traj.token_series[0] if traj.token_series else none %}{% if job.runner %}{{ job.runner }} asks its model once for each of its {{ job.runner_items[1] if job.runner_items else 'items' }}, so the bar is that one call: its input, from cache or new, and what it wrote. An agent's run charts its calls one after another instead, as its context grows.{% else %}One step carried usage: the bar is that call; the growth strip needs two.{% endif %}
{% endif %} {% if traj.calls %}| Tool | Category | Calls | Failed | Time |
|---|---|---|---|---|
| {{ t.server }} | ||||
| {{ t.name[:80] }}{% if t.name|length > 80 %}…{% endif %} | {{ t.category }} | {{ t.calls }} | {{ t.failed or '—' }} | {{ t.ns|nsdur if t.ns is not none else '—' }} |
The gold patch is a correct answer, not the only one.
No trajectory recorded for this trial.
{% if bench_parts.says %}{{ bench_parts.says }}. {% endif %}{% if trial.score is not none %}This one scored {{ '%.3f'|format(trial.score) }}: the mean over its {{ bench_parts.parts|length }} {{ bench_parts.nouns }}, one without a score counting nothing.{% endif %}
| # | Asked | Answered | {% for k in bench_parts.shows %}{{ k|capitalize }} | {% endfor %}Score |
|---|---|---|---|---|
| {{ x.n }} | {{ x.text }} | {% if x.said is not none %}{{ x.said }}{% else %}no answer{% endif %} | {%- for k in bench_parts.shows %}{% if x.shows and x.shows[k] is not none %}{{ x.shows[k] }}{% endif %} | {% endfor -%}{% if x.score is not none %}{{ '%.3f'|format(x.score) }}{% else %}—{% endif %} |
Below, Nullius's own trajectory judge: experimental, asked on a click.
{{ bench_judge.judge or job.runner_judge }}{% if bench_judge.model %} · sent as {{ bench_judge.model }}{% endif %}
Said {{ (bench_judge.said or '—')[:200] }}: {{ 'resolved' if trial.outcome == 'resolved' else trial.outcome|replace('_', ' ') }}.
{{ bench_judge.asked }}{% if job.runner_judge %}{{ job.runner_judge }}. {% endif %}No record of its call for this one{% if trial.outcome not in ('resolved', 'failed_verification') %}: it never got to judge it{% endif %}.
{% endif %}Below, Nullius's own trajectory judge: experimental, asked on a click.
How clearly the task says what is wanted, judged from its text alone: once per dataset and task, shared by every trial of it. An unclear instruction caps every agent's score on this task, a finding about the benchmark, not about this agent. It changes no outcome.
A control run: its trials are a check on the tasks, so the instruction is scored from an agent's run of the same dataset.
{% endif %}{{ tt.instruction }}
{% else %}No instruction can be read from this trial's trajectory.
{% endif %}{{ trace.error }}
All {{ trace.totals.span_count }} spans are inferred — the exporter captured nothing and the producer fell back to reading the agent's own logs. Every duration below is approximate, and simply wrong wherever tool calls ran in parallel.
Rendering {{ viewing }} — a view saved from this trial, not the producer's capture. Back to trace.json
{% endif %} {{ ui.facet_chips(trace.facets, filt, chart_filter, view_url) }} {% if trace.hidden_spans %} {# Hidden one at a time rather than by label — these need their own controls, since no chip above represents them. #}{% if filt.active %} {# `shown` counts the filtered view whole — a zoom narrows the waterfall, not the filter, so this sentence must not shrink with it. #} {{ trace.shown }} span{{ '' if trace.shown == 1 else 's' }} shown, {{ trace.hidden_count }} left out. Clear × {% else %} Click a label to leave out every span carrying it, or the × on a waterfall row — alt-click its bar — to leave out one call. Either way what it called goes with it, and the breakdown is recomputed without it. The producer's own totals stay as reported. {% endif %}
{% if filt.active %}Writes a new harbor-trace-v1 file beside the trial's own, in agent/: the hidden spans gone, totals recomputed over what is left, and the producer's numbers kept under filtered.source_totals. Nothing is overwritten — least of all trace.json, which is a capture that cannot be taken again. {% if trace.zoomed %}The zoom is not written: it is a way of looking at the file, and the link carries it.{% endif %}
{% endif %}Categories, not a sequence. Reported by the trace, never summed from the bars below: spans nest.{% if trace.totals.unaccounted_ns %} Unaccounted is wall clock outside the instrumented spans.{% endif %}
{% else %}The trace reports no totals.
{% endif %} {% if filt.active %}Derived from the {{ trace.view.span_count }} remaining span{{ '' if trace.view.span_count == 1 else 's' }}. Each Δ is measured against this same arithmetic over the whole trace, so it is what the filter took out — read the absolute numbers against the strip above instead, which is the producer's own accounting and can differ from this one by a lot: time no span covers is “Unaccounted” here, where a producer may know it was a wait for a prompt and call it idle. Every instant is charged to exactly one category (waiting on a human beats real work, which beats the call it ran inside), because summing spans that nest or overlap attributes more than the run. So hiding a span moves its time into “Unaccounted” rather than out of the total: the run took just as long.
{% else %}Nothing is left to break down.
{% endif %} {% endif %} {% if trace.coverage_pct %} {% set uncovered = trace.totals.wall_ns - trace.instrumented_ns %}The waterfall covers {{ trace.instrumented_ns|nsdur }} of the {{ trace.totals.wall_ns|nsdur }} the trace {{ 'infers' if trace.zero_measured else 'measures' }} — {{ '%.1f'|format(trace.coverage_pct) }}%. The axis is the captured window, not the run.
{% endif %} {% if trace.has_spans %}Zoomed to {{ trace.zoomed.label }} {{ trace.zoomed.span_id[:12] }} {{ trace.rows|length }} of {{ trace.shown }} span{{ '' if trace.shown == 1 else 's' }} · starts {{ trace.zoom_offset_ns|nsoff }} into the capture · {{ trace.axis.span_ns|nsdur }} wide — offsets below count from its start Reset zoom ×
{% elif trace.zoom_lost %}No span {{ trace.zoom_lost[:24] }} in this view — nothing to zoom to, so this is the whole trace. It may be hidden by the filter, or belong to another trace file. Clear ×
{% endif %}{{ trace.truncated }} further span{{ '' if trace.truncated == 1 else 's' }} not shown — the trace exceeds the render cap.
{% endif %}A group only brackets what is inside it — claude_code.tool holds the wait for permission and the execution, and a request whose whole response was one block holds that block — so it is charged no time of its own and the row below it is the same measurement. Hiding a group takes its contents with it; hiding the contents leaves the bracket.
{% endif %}Every span is filtered out — nothing left to draw.
{% endif %}Select a span to load its recorded detail.
This trial has no trace sidecar. Its recorded trajectory, when available, is in Conversation. Missing span clocks are not reconstructed here.
The trace file contains no recorded spans to display.
Read the recorded source files. Content is redacted and paged; source files remain intact.
| Record | Availability | Size | |
|---|---|---|---|
| {{ artifact.label }} | {{ 'Recorded' if artifact.exists else 'Not recorded' }} | {{ ((artifact.size / 1024)|round(1)) ~ ' KB' if artifact.exists else '—' }} | {% if artifact.exists %}Read →{% endif %} |