Agent Trajectory Interchange Format (ATIF)

View as Markdown

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:

1version = 1
2
3[[components]]
4kind = "observability"
5enabled = true
6
7[components.config]
8version = 4
9
10[components.config.atif]
11enabled = true
12agent_name = "Planner"
13agent_version = "1.0.0"
14model_name = "unknown"
15output_directory = "logs"
16filename_template = "trajectory-{session_id}.json"

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 trajectory_id, the historical {session_id} filename expansion, and the remote session header remain trajectory-unique artifact identities. A rooted propagation context uses its root as the serialized ATIF session_id.

Fields

The following table describes the top-level ATIF settings:

FieldDefaultNotes
enabledfalseMust be true to write trajectories.
agent_nameNeMo RelayAgent metadata written into the trajectory.
agent_versionNeMo Relay crate versionAgent version metadata.
model_nameunknownDefault model metadata when no call-level model is present.
tool_definitionsOmittedOptional ATIF tool metadata.
extraOmittedOptional ATIF agent metadata.
output_directoryCurrent working directoryDirectory containing trajectory files and the recovery copy when every configured remote destination fails.
filename_templatenemo-relay-atif-{session_id}.jsonMust contain the historical {session_id} placeholder, which expands to the trajectory scope UUID. Can contain {metadata.<path>} placeholders for metadata-based routing.
storageOmittedOptional list of remote storage destinations. When non-empty, trajectories are uploaded to every configured backend. If all fail for one trajectory, Relay writes a local recovery copy. Refer to Remote Storage.

Run-Scoped Session IDs

Capture the default propagation context when the current Relay root carries a run identifier such as an upstream UUID request_id:

1[components.config.atif]
2enabled = true
3output_directory = "logs"
4filename_template = "trajectory-{session_id}.json"

For a top-level Agent scope, Relay emits the propagation root as the ATIF session_id and keeps the Agent scope UUID as trajectory_id. The filename, remote storage key, and remote session header continue to use the Agent scope UUID so multiple trajectories in one run cannot overwrite each other. Use rootless propagation when a receiver must start a new local session.

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:

1[components.config.atif]
2enabled = true
3output_directory = "logs"
4filename_template = "{metadata.atif_prefix:-unassigned}/trajectory-{session_id}.json"

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 and causes activation.close() 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:

1x-nemo-relay-session-metadata: {"atif_prefix":"tenant-a/session-123"}

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:

1[components.config.atif]
2enabled = true
3filename_template = "trajectory-{session_id}.json"
4
5[[components.config.atif.storage]]
6type = "s3"
7bucket = "nemo-relay-traces"
8key_prefix = "openshell/"

HTTP Endpoint Storage

Configure an HTTP destination as follows:

1[components.config.atif]
2enabled = true
3filename_template = "trajectory-{session_id}.json"
4
5[[components.config.atif.storage]]
6type = "http"
7endpoint = "https://observability.example.com/atif"
8timeout_millis = 3000
9
10[components.config.atif.storage.headers]
11x-team = "agent-platform"
12
13[components.config.atif.storage.header_env]
14authorization = "NEMO_RELAY_ATIF_HTTP_AUTH"

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:

FieldDefaultNotes
endpointrequiredDestination http:// or https:// URL.
headers{}Static request headers.
header_env{}Header names mapped to environment variable names containing secret values.
timeout_millis3000Per-request timeout in milliseconds. Must be positive.

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:

1[components.config.atif]
2enabled = true
3filename_template = "trajectory-{session_id}.json"
4
5[[components.config.atif.storage]]
6type = "s3"
7bucket = "nemo-relay-traces"
8key_prefix = "openshell/"
9
10[[components.config.atif.storage]]
11type = "http"
12endpoint = "https://observability.example.com/atif"

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:

FieldDefaultNotes
bucketrequiredDestination bucket name.
key_prefix""Optional prefix applied to every object. A trailing / is added if missing.
access_key_idAWS_ACCESS_KEY_IDInline static access key ID.
regionAWS_REGIONBucket region.
endpoint_urlAWS_ENDPOINT_URLEndpoint override for S3-compatible storage.
allow_httpAWS_ALLOW_HTTPSet to true when targeting an HTTP endpoint.

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:

FieldEnv Var FallbackNotes
secret_access_key_varAWS_SECRET_ACCESS_KEYName of the env var that holds the static secret key.
session_token_varAWS_SESSION_TOKENName of the env var that holds the optional STS session token.

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 is logged after subscriber delivery is flushed. 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 activation.close() runs, the close returns the delivery error after removing callbacks. 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 propagated root UUID when one is present, otherwise the top-level trajectory scope UUID. The ATIF trajectory_id remains the top-level trajectory 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.

When a successful tool result carries an opaque annotation, the corresponding ATIF observation result stores it at extra.tool_result_annotation. ATIF preserves the JSON value without interpreting application-defined keys.

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() resolves the same configuration layers as initialize() without loading plugin code or acquiring the activation lease. Use Python/Rust validate_exact(), Node.js validateExact(), or Go ValidateExact() when only the supplied static configuration should be checked. For complete layering rules, refer to Plugin Configuration Files.

1import asyncio
2
3from nemo_relay import plugin
4from nemo_relay.observability import AtifConfig, ComponentSpec, ObservabilityConfig
5
6config = plugin.PluginConfig(
7 components=[
8 ComponentSpec(
9 ObservabilityConfig(
10 atif=AtifConfig(
11 enabled=True,
12 agent_name="Planner",
13 agent_version="1.0.0",
14 model_name="unknown",
15 output_directory="logs",
16 filename_template="trajectory-{session_id}.json",
17 )
18 )
19 )
20 ]
21)
22
23report = plugin.validate(config)["config"]
24if any(diagnostic["level"] == "error" for diagnostic in report["diagnostics"]):
25 raise RuntimeError(report["diagnostics"])
26
27async def main():
28 async with plugin.activate(config):
29 # Run instrumented application work here.
30 pass
31
32asyncio.run(main())

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.

1from nemo_relay import AtifExporter
2
3exporter = AtifExporter("session-1", "agent", "1.0.0", model_name="demo-model")
4exporter.register("atif-exporter")
5
6# Run instrumented application work here.
7
8trajectory = exporter.export()
9exporter.deregister("atif-exporter")
10exporter.clear()

Common Configuration and Runtime Issues

  • filename_template does not contain {session_id}.
  • The output directory is not writable at runtime.
  • Tool definitions or extra metadata 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() or export_json().
  • The process terminates before manual export(), supported root-scope closure, or plugin teardown establishes the export boundary.
  • storage[i].type is unknown or storage[i].bucket is empty for some entry.
  • storage is non-empty in a build that was compiled without the atif-storage feature.