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 = 3
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 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:

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 {session_id}. 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.

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 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:

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 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.

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)
24if any(diagnostic["level"] == "error" for diagnostic in report["diagnostics"]):
25 raise RuntimeError(report["diagnostics"])
26
27async def main():
28 await plugin.initialize(config)
29 try:
30 # Run instrumented application work here.
31 pass
32 finally:
33 await plugin.clear_async()
34
35asyncio.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.