Agent Trajectory Interchange Format (ATIF)
Agent Trajectory Interchange Format (ATIF)
Use the atif section when you want one Agent Trajectory Interchange Format
(ATIF) trajectory artifact per top-level Agent scope or supported coding-agent
turn scope.
The plugin-managed ATIF dispatcher creates a scope-local exporter for each
top-level Agent scope and each root-child custom scope marked
nemo_relay_scope_role = "turn". It writes the trajectory when that root scope
ends. Nested Agent scopes remain in the parent trajectory.
plugins.toml Example
Add the following ATIF exporter configuration to plugins.toml:
This configuration writes a trajectory file such as
logs/trajectory-<scope-uuid>.json for each top-level Agent scope or supported
coding-agent turn scope.
ATIF contains agent interaction steps, tool calls, and observations. Relay mark
events are point-in-time telemetry rather than trajectory steps, so the ATIF
exporter omits them from steps. Plugin-managed files retain raw events that
were associated with the trajectory under extra.observed_events.
Plugin-managed files also expose selected correlation fields under
extra.nemo_relay: session_id is the logical harness session ID,
session_instance_id is Relay’s root scope UUID, and user_id is present when
session metadata contains a top-level string value. The top-level ATIF
session_id, trajectory_id, {session_id} filename expansion, and remote
session header remain trajectory-unique artifact identities for compatibility.
Fields
The following table describes the top-level ATIF settings:
Metadata-Based Paths
Use {metadata.<path>} placeholders in filename_template to route
trajectories with top-level scope metadata. Dots select nested fields, and
templates can use multiple placeholders. For example:
With scope metadata {"atif_prefix":"tenant-a/session-123"}, Relay writes
logs/tenant-a/session-123/trajectory-<scope-uuid>.json. The template must
still contain {session_id}.
Use :- to provide a literal fallback when metadata can be absent, for example
{metadata.atif_prefix:-unassigned}. Relay uses the fallback when the selected
metadata field is absent or null. A present non-string value is rejected
instead of being routed through the fallback.
Each metadata placeholder must resolve to a string containing a non-empty,
relative path fragment. Slash-separated segments can contain ASCII letters,
digits, -, _, ., and ~. Relay rejects empty segments, . and ..
segments, absolute paths, backslashes, spaces, and other characters. If a
placeholder is missing without a fallback, is present but non-string, or
resolves to an unsafe value, Relay skips that trajectory. It records a runtime
diagnostic in plugin.report() and causes plugin teardown to fail. Literal
template text must also be a relative, traversal-free path. The rendered
filename applies to local, S3, and HTTP storage in the same way as a static
filename.
The CLI gateway parses x-nemo-relay-session-metadata as JSON and merges it
into the top-level scope metadata:
Remote Storage
Use storage when local trace files are not durable across sessions, such as in
sandboxed runtimes. When storage is non-empty, Relay uploads each completed
trajectory to every configured backend instead of writing a local file.
output_directory is ignored for successful remote writes, but Relay uses it
for a local recovery copy when every configured remote destination fails for a
trajectory.
Each storage entry is tagged with a type discriminator so additional
backends can be added without breaking existing configs. S3-compatible object
storage and HTTP endpoints are supported.
S3-Compatible Storage
Configure an S3-compatible destination as follows:
HTTP Endpoint Storage
Configure an HTTP destination as follows:
The HTTP backend sends one POST per completed trajectory. The request body is
the rendered ATIF JSON file with content-type: application/json. NeMo Relay
also sets x-nemo-relay-atif-filename and
x-nemo-relay-atif-session-id headers so receivers can keep the same object
identity that local files and S3 uploads use.
HTTP 2xx responses are treated as success. Any non-2xx response or
transport error records a runtime delivery diagnostic. Relay attempts every
configured destination again for each later trajectory, while other configured
destinations continue to receive writes. If every remote destination fails for
a trajectory, Relay writes a local recovery copy under output_directory.
The following table describes HTTP storage settings:
Multiple Destinations
Add another [[components.config.atif.storage]] table to send each trajectory
to every configured destination. For example, this configuration uses an
in-cluster MinIO target and a remote HTTP endpoint:
Connection Fields
By default, Relay reads credentials, region, and endpoint URL from standard AWS environment variables. You can also set non-secret connection fields directly in the configuration. This lets one file describe multiple S3-compatible destinations, such as an in-cluster MinIO target and a remote AWS target:
The following table describes S3-compatible connection settings:
Explicit fields take precedence; anything left unset falls back to the matching
AWS_* environment variable.
Secret Credential Fields
Secret values stay out of checked-in config files. Each secret field carries a
_var suffix and holds the name of an environment variable that contains the
secret value. The plugin validates the name during initialization:
The following table describes secret credential settings:
Relay uploads each trajectory under {key_prefix}{rendered_filename}. It
renders filename_template the same way it does for local files, so a
local→remote transition keeps object names stable. Relay adds a trailing / to
key_prefix when one is missing.
If an upload fails for a destination, the ATIF exporter records a runtime
delivery diagnostic and retries that destination for each later trajectory.
Other destinations continue to receive writes. When every remote destination
fails for one trajectory, Relay writes a local recovery copy under
output_directory; a successful remote destination does not create that copy.
The diagnostic becomes visible in plugin.report() after subscriber delivery
is flushed and is retained there after a failed teardown. Teardown also reports
the degraded delivery, even when the local recovery write succeeds. This error
reports delivery degradation, not a registration leak; callbacks have already
been removed. If the failure is still pending when initialization replaces the
configuration, that replacement returns the delivery error and leaves no
configuration active; a subsequent clear or initialization is safe. Fatal
dispatcher failures, such as trajectory serialization failures, are also
reported during teardown.
Expected Output
The exporter translates NeMo Relay lifecycle events into ATIF v1.7 trajectory
data. LLM start and end events become model steps, tool events become tool
calls and observations, and scope nesting contributes lineage metadata.
Nested agent scopes are embedded in the parent file as subagent_trajectories
and referenced from parent observation results with
subagent_trajectory_ref.trajectory_id. The reference points to the embedded
child trajectory by ID so consumers can validate the parent and child as one
single-file ATIF v1.7 artifact.
The plugin writes each trajectory when its top-level Agent scope or supported coding-agent turn scope closes. If the plugin is cleared while that root scope is still open, teardown first drains queued subscriber callbacks and then writes the partial trajectory. Terminating the process before root-scope closure or plugin teardown completes can leave the trajectory absent or incomplete.
To correlate ATIF with typed OpenTelemetry traces from the same run,
join on NeMo Relay UUIDs. The plugin-managed ATIF session_id is the
top-level trajectory root scope UUID. Each step’s extra.ancestry.function_id is the event UUID,
and extra.ancestry.parent_id is the parent event UUID. Trace spans expose the
same values as nemo_relay.uuid and nemo_relay.parent_uuid attributes.
When present, a step’s metrics can carry prompt_tokens, completion_tokens,
cached_tokens (cache read + write), and cost_usd (USD only); the trajectory
final_metrics sums the metrics present on its steps as total_*. Refer to
Token and Cost Field Semantics
for the full mapping, including how ATIF sources these values from the codec
annotation and the raw payload.
ATIF is a trajectory projection over NeMo Relay events. It should preserve the meaning of scope parentage, event UUIDs, codec annotations, and exporter-local lineage rules without becoming the source of truth for runtime ownership, middleware ordering, or provider payload decoding.
Plugin Configuration
Use plugin configuration when the application should let NeMo Relay own the ATIF dispatcher lifecycle. The following examples configure and activate the ATIF exporter through each supported language binding.
validate() checks only the supplied in-memory object. initialize() also
layers discovered plugins.toml configuration. For effective file-backed
validation, refer to Plugin Configuration Files
and run the gateway with the same configuration path that production uses.
Python
Node.js
Rust
Manual API
Use the manual AtifExporter API when you need explicit collection boundaries
or one exporter object per run. export() and export_json() first wait for
subscriber callbacks queued before the call, then take a snapshot of the
collected event history. A separate subscriber flush immediately before export
is unnecessary. The snapshot can also include events delivered after the
internal flush returns but before the snapshot is taken. Events delivered after
the snapshot are not included.
Python
Node.js
Rust
Common Configuration and Runtime Issues
filename_templatedoes not contain{session_id}.- The output directory is not writable at runtime.
- Tool definitions or
extrametadata are not JSON-compatible. - The application never opens a top-level Agent scope or a supported coding-agent turn scope, so no trajectory file is created.
- The application expects manual trajectory output without calling
export()orexport_json(). - The process terminates before manual
export(), supported root-scope closure, or plugin teardown establishes the export boundary. storage[i].typeis unknown orstorage[i].bucketis empty for some entry.storageis non-empty in a build that was compiled without theatif-storagefeature.