Skip to content

Open Trace Files

A .trz file is a recording: the bytes the bench produced, frozen. The agent opens one and answers the same calls it answers for live data — query, at, window, latest, check, export — so an analysis you wrote against the live agent runs unchanged against the archive.

from zelos_sdk import connect

agent = connect()
trace = agent.trace("/data/bench-run-14.trz")
trace

Save a slice of live data

export() asks each producer to write its part of a time window into one file:

result = agent.export("/data/bench-run-14.trz", start="-10m", overwrite=True)
result

The result reports per producer, so one failing source cannot silently hollow out the archive: result.ok is True only when every producer succeeded, and result.results maps each producer to its outcome. result.format names the format the agent wrote. result.open() hands you the Trace without repeating the path.

signals= chooses producers, not signals. It takes the same glob grammar as everywhere else. Each producer that owns a matched signal writes its whole slice of the window, so the file holds more than the pattern names. Omit signals= and every producer with data in the window writes:

# Every signal from the producer that owns bus0/BMS_message/*, not only those signals
agent.export("/data/pack.trz", start="-10m", signals="bus0/BMS_message/*", overwrite=True)

A pattern that matches nothing raises SignalNotFound. To keep only some signals, query them and work with the frame instead.

Argument Means
path Destination .trz file, on the agent's host.
start, end The window, in the time grammar. start is required; end defaults to now.
signals Patterns that choose which producers write; None (default) means every producer with data in the window. paths= is a deprecated alias.
producers Producer addresses to consider; None (default) covers every connected producer.
overwrite Replace an existing file. Required on a cross-host target.
format "trz2" (sealed catalog plus Parquet), "legacy" (DuckDB), or None for the agent's default, which is "legacy" in current releases. Only a "trz2" file can be uploaded.
lookback Ignored, with a DeprecationWarning. Wildcards resolve against the export window.

The file is written on the agent's filesystem. On a cross-host target overwrite=False is refused outright — the agent has no way to atomically replace a file on another machine — so pass overwrite=True to opt in.

Time on a trace

This is the one thing that differs from live data, and it is the thing that makes archives comparable: the file is the clock. The wall clock never enters into a trace query.

Bound Means
"-60s" 60 seconds before the end of the recording
"+30s" 30 seconds after the start of the recording
"start" / "end" the file's own bounds
datetime(...), ISO 8601 that instant, taken literally
omitted the matching end of the file

So trace.query(paths, start="-60s") is "the last minute of the run" and trace.query(paths, start="start", end="+30s") is "the first thirty seconds" — both meaningful in a recording made last week, and both the same words you would write against a live agent.

A window entirely outside the file raises ValueError naming the file's extent; one that merely overhangs is clamped.

Query a trace

trace.signals()
trace.signals().match("bus0/BMS_message/cells.*")

frame = trace.query("bus0/BMS_message/cells.*", start="-60s")
frame

Everything a live frame does, a trace frame does: the join rule, ffill()/dropna(), unit-carrying series, plot(), describe(). Omit both bounds and you get the file's full extent — on a live agent start is required, because "all of time" is not a window.

trace.info() is the file's own metadata. It returns a TraceInfo with file_size_bytes, range and duration, segments, tables (each with name and row_count), and total_data_rows:

trace.info()
trace.time_range              # TimeRange: .start, .end, .duration, .start_ns, .end_ns
trace.segments()              # list[Segment]; read warnings arrive as RuntimeWarning
trace.segments_with_warnings()   # (segments, warnings) instead

Cursors, windows and checks

The same three calls, scoped to the file:

trace.at(["bus1/inverter_status.*"], cursor)
trace.window("bus1/inverter_status.mode", start="start", duration="30s")
trace.latest("bus0/BMS_message/status.pack_current")   # the file's last value

trace.check.that(frame["bus0/BMS_message/cells.cell_0"], ">", 3.0)

trace.at() and trace.latest() take the file's time grammar and default to its end. One literal path returns a LatestValue; a pattern or a list returns a Snapshot. On a live agent, agent.at() returns a Snapshot either way.

A check on a trace reads exactly as it does live and produces the same result and evidence — see Checks. The two duration temporals are the exception: on live data they wait for data that has not arrived, and on a recording there is nothing to wait for, so temporal="for_duration" and temporal="within_duration" read duration_s as a window anchored at the end of the recording — the last ten seconds of the file, not the next ten seconds of the clock.

Compare runs

traces() opens several files as one set. Each file is a run. Pass a list and the runs are named after the file stems; pass a mapping and you name them yourself:

runs = agent.traces({"baseline": "/data/run_a.trz", "candidate": "/data/run_b.trz"})
runs
runs.runs          # ['baseline', 'candidate']
runs.paths         # each run's path, in the same order
runs.time_range    # the union extent across every run
runs.max_duration  # the longest run, as a timedelta
runs.traces        # one TraceTiming per run: .path, .start, .duration

A set answers signals() (the union catalog; each signal's trace_path names its file), segments(), segments_with_warnings(), at(), latest() and window() the way a single trace does, across every run.

Offset bounds — "start", "+30s", "-60s", or none at all — align every run at t = 0, and the frame's axis becomes seconds from each run's own start. That is what makes "the first thirty seconds of the run" comparable across recordings made on different days:

frame = runs.query("bus0/BMS_message/cells.cell_0", start="start", end="+30s")

An absolute bound — a datetime or an ISO instant — selects the wall-clock axis instead, for the case where the runs really did happen at the same time.

A set frame has one column per (run, signal) pair. Each column names its run, as <run>::<path>, under the set's own run names — the ones you passed traces(), or the file stems when you passed a list. frame.time_origin is "run_start" on an aligned frame and "epoch" on a wall-clock one. A bare path returns one series per run:

frame.keys()
# ['baseline::bus0/BMS_message/cells.cell_0', 'candidate::bus0/BMS_message/cells.cell_0']

frame["baseline::bus0/BMS_message/cells.cell_0"]   # one run's column
frame["bus0/BMS_message/cells.cell_0"]
# SeriesByRun(2 runs: baseline len=30, candidate len=30)

frame.short_names().keys()
# ['baseline · cell_0', 'candidate · cell_0']

SeriesByRun is a read-only mapping keyed by run, so by_run["baseline"], by_run.items() and dict(by_run) all work. frame.series(path) returns the same mapping.

A check on a set returns one CheckResult per run, keyed by label:

runs.check.that(agent.signal("bus0/BMS_message/cells.cell_0"), ">", 3.0, last="30s")

Slice an archive into a smaller one

trace.export() writes a new file from a slice of this one, with the same bounds grammar. Omit both bounds to copy the whole file:

trace.export("/data/incident.trz", start="-2m", overwrite=True)
trace.export("/data/incident-trz2.trz", start="-2m", format="trz2", overwrite=True)

format=None writes the agent's default format, except that a TRZ2 input stays TRZ2. runs.export(path, start=..., end=...) writes the union of every run in a set to one file, with the same bounds rule as runs.query().

Closing

A trace holds a file open on the agent. Close it when you are done, or use the context manager and forget about it:

with agent.trace("/data/bench-run-14.trz") as trace:
    frame = trace.query("bus0/BMS_message/cells.*", start="start", end="+1m")

with agent.traces(["/data/run_a.trz", "/data/run_b.trz"]) as runs:
    ...

Closing is a hint, not a shutdown. The agent drops what it loaded for the file, and the handle reopens it on its next call. agent.close_traces() releases every trace this agent has open; agent.close_traces(["/data/run_a.trz"]) releases only those.

Upload and download cloud traces

The agent moves traces to and from Zelos Cloud. It must be signed in: run zelos login, or sign in from the Zelos app. Calls on a signed-out agent raise AgentError.

Open a cloud trace

agent.trace() and agent.traces() take a cloud trace's console URL, the link you copy from its page in the console:

with agent.trace("https://console2.zeloscloud.io/acme/traces/0198f0e1-…") as trace:
    frame = trace.query("bus0/BMS_message/cells.*", start="-60s")

The URL must have the shape https://<console>/<org>/traces/<uuid>, on any console host. The desktop app's deep link, zelos://open?org=<org>&trace=<uuid>, works too. Any other https:// or zelos:// address raises ValueError. trace.path reports the trace as its zelos:// deep link.

The agent reads the trace from the cloud, so nothing is downloaded first. In a set, local files and cloud traces mix, and a cloud run compares against a local one directly. Name the runs with a mapping, because a URL has no useful file stem:

runs = agent.traces({
    "bench": "/data/run_a.trz",
    "fleet": "https://console2.zeloscloud.io/acme/traces/0198f0e1-…",
})

Upload

agent.upload() sends a trace to your organization. The agent holds the login, seals the trace, and moves the bytes. Pass exactly one source:

# The agent's live session (needs an agent with a disk store)
upload = agent.upload(live=True, name="dyno run 14", tags=["dyno"])
print(upload.url)          # the trace's console page

# A file you exported in the TRZ2 format
agent.export("/data/run.trz", start="-10m", format="trz2", overwrite=True)
upload = agent.upload("/data/run.trz")
Argument Means
path A TRZ2 .trz file, resolved against this process's working directory. A "legacy" file raises NoData; export it again with format="trz2".
live True uploads the live session instead of a file.
producers With live=True, which producers to seal; None (default) is the agent and every remote it is connected to.
organization Organization slug; None (default) is your active organization.
name The trace's name in the console; defaults to trace_<UTC timestamp>.
tags Names of tags that already exist in the organization, matched case-insensitively. An unknown name raises ValueError before anything uploads. Upload never creates a tag.
wait True (default) blocks until the upload finishes. False returns the handle at once.
timeout Seconds to wait when wait=True; None (default) waits forever. When it runs out, the upload is canceled and TimeoutError raised.
poll_interval Seconds between status checks while waiting (default 0.5).

upload() returns an Upload with trace_id, organization and url. With wait=False, poll it, wait on it, or cancel it:

import time

upload = agent.upload(live=True, wait=False)
while not (status := upload.status()).done:
    print(status.phase, status.bytes_done, status.bytes_total)
    time.sleep(1.0)

upload.wait(timeout=600) blocks instead. Its TimeoutError leaves the upload running, because you still hold the handle; upload.cancel() asks the agent to stop it.

status() returns an UploadStatus. Its phase is "exporting", "sealing", "uploading", "finalizing", "ready" or "failed". bytes_total is 0 until the agent knows the size, done is True at "ready" or "failed", and error holds the reason for a failure. wait() raises UploadFailed when the upload fails. Ctrl+C during wait() cancels the upload.

Download

agent.download() writes a cloud trace to a local TRZ2 .trz file:

result = agent.download("https://console2.zeloscloud.io/acme/traces/0198f0e1-…", "/data/run.trz")
result.path, result.size_bytes

with agent.trace(result.path) as trace:
    ...

The agent writes the file, so on a remote agent it lands on that host. A relative path resolves against this process's working directory. Without a path, the file is named after the trace, in the working directory. overwrite=True replaces an existing file; without it, an existing file raises AgentError. The call blocks until the file is complete, with no time limit; Ctrl+C stops it. DownloadResult works anywhere a path does, such as open(result) or Path(result).

Errors

Error When
TraceNotFound no file at that path on the agent's host
SignalNotFound the path is not in this file's catalog
ValueError a window entirely outside the file's extent; a URL that is not a cloud trace; a bad upload() source or tag name
FileExistsError agent.export(overwrite=False) onto an existing file
AgentError trace.export(overwrite=False) or agent.download(overwrite=False) onto an existing file; a signed-out agent; a cloud trace the agent cannot open
NoData upload() of a legacy file, or live=True on an agent with a memory store
UploadFailed the agent reported that an upload failed
TimeoutError upload() or Upload.wait() ran past its timeout

What's next

  • Query live data

    The full read surface: globs, frames, series math, charts.

  • Checks

    Rules with evidence, on a file or on the live agent.

  • Notebooks

    The round-trip as a runnable document.