> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Agent Trajectory Interchange Format (ATIF)

> Configure ATIF trajectory export to local files, HTTP endpoints, or S3-compatible storage.

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

```toml
version = 1

[[components]]
kind = "observability"
enabled = true

[components.config]
version = 1

[components.config.atif]
enabled = true
agent_name = "Planner"
agent_version = "1.0.0"
model_name = "unknown"
output_directory = "logs"
filename_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.

## Fields

The following table describes the top-level ATIF settings:

| Field               | Default                             | Notes                                                                                                                                                                                             |
| ------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`           | `false`                             | Must be `true` to write trajectories.                                                                                                                                                             |
| `agent_name`        | `NeMo Relay`                        | Agent metadata written into the trajectory.                                                                                                                                                       |
| `agent_version`     | NeMo Relay crate version            | Agent version metadata.                                                                                                                                                                           |
| `model_name`        | `unknown`                           | Default model metadata when no call-level model is present.                                                                                                                                       |
| `tool_definitions`  | Omitted                             | Optional ATIF tool metadata.                                                                                                                                                                      |
| `extra`             | Omitted                             | Optional ATIF agent metadata.                                                                                                                                                                     |
| `output_directory`  | Current working directory           | Directory containing trajectory files. Ignored when `storage` is non-empty.                                                                                                                       |
| `filename_template` | `nemo-relay-atif-{session_id}.json` | Must contain `{session_id}`.                                                                                                                                                                      |
| `storage`           | Omitted                             | Optional list of remote storage destinations. When non-empty, trajectories are uploaded to every configured backend instead of being written locally. Refer to [Remote Storage](#remote-storage). |

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

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:

```toml
[components.config.atif]
enabled = true
filename_template = "trajectory-{session_id}.json"

[[components.config.atif.storage]]
type = "s3"
bucket = "nemo-relay-traces"
key_prefix = "openshell/"
```

### HTTP Endpoint Storage

Configure an HTTP destination as follows:

```toml
[components.config.atif]
enabled = true
filename_template = "trajectory-{session_id}.json"

[[components.config.atif.storage]]
type = "http"
endpoint = "https://observability.example.com/atif"
timeout_millis = 3000

[components.config.atif.storage.headers]
x-team = "agent-platform"

[components.config.atif.storage.header_env]
authorization = "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 the endpoint as unhealthy and skips it for later
trajectories. Other configured destinations continue to receive writes.

The following table describes HTTP storage settings:

| Field            | Default  | Notes                                                                       |
| ---------------- | -------- | --------------------------------------------------------------------------- |
| `endpoint`       | required | Destination `http://` or `https://` URL.                                    |
| `headers`        | `{}`     | Static request headers.                                                     |
| `header_env`     | `{}`     | Header names mapped to environment variable names containing secret values. |
| `timeout_millis` | `3000`   | Per-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:

```toml
[components.config.atif]
enabled = true
filename_template = "trajectory-{session_id}.json"

[[components.config.atif.storage]]
type = "s3"
bucket = "nemo-relay-traces"
key_prefix = "openshell/"

[[components.config.atif.storage]]
type = "http"
endpoint = "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:

| Field           | Default             | Notes                                                                        |
| --------------- | ------------------- | ---------------------------------------------------------------------------- |
| `bucket`        | required            | Destination bucket name.                                                     |
| `key_prefix`    | `""`                | Optional prefix applied to every object. A trailing `/` is added if missing. |
| `access_key_id` | `AWS_ACCESS_KEY_ID` | Inline static access key ID.                                                 |
| `region`        | `AWS_REGION`        | Bucket region.                                                               |
| `endpoint_url`  | `AWS_ENDPOINT_URL`  | Endpoint override for S3-compatible storage.                                 |
| `allow_http`    | `AWS_ALLOW_HTTP`    | Set 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:

| Field                   | Env Var Fallback        | Notes                                                          |
| ----------------------- | ----------------------- | -------------------------------------------------------------- |
| `secret_access_key_var` | `AWS_SECRET_ACCESS_KEY` | Name of the env var that holds the static secret key.          |
| `session_token_var`     | `AWS_SESSION_TOKEN`     | Name 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 that destination
as unhealthy and skips it for later trajectories. Other destinations continue
to receive writes. The exporter reports fatal dispatcher failures, such as
trajectory serialization failures, during plugin teardown. It isolates
per-destination sink failures to the failed sink.

## 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 flushes the partial trajectory.

To correlate ATIF with OpenTelemetry or OpenInference 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](/integrate-into-frameworks/provider-response-codecs#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](/configure-plugins/plugin-configuration-files)
and run the gateway with the same configuration path that production uses.

```python
import asyncio

from nemo_relay import plugin
from nemo_relay.observability import AtifConfig, ComponentSpec, ObservabilityConfig

config = plugin.PluginConfig(
    components=[
        ComponentSpec(
            ObservabilityConfig(
                atif=AtifConfig(
                    enabled=True,
                    agent_name="Planner",
                    agent_version="1.0.0",
                    model_name="unknown",
                    output_directory="logs",
                    filename_template="trajectory-{session_id}.json",
                )
            )
        )
    ]
)

report = plugin.validate(config)
if any(diagnostic["level"] == "error" for diagnostic in report["diagnostics"]):
    raise RuntimeError(report["diagnostics"])

async def main():
    await plugin.initialize(config)
    try:
        # Run instrumented application work here.
        pass
    finally:
        plugin.clear()

asyncio.run(main())
```

```js
const plugin = require("nemo-relay-node/plugin");
const observability = require("nemo-relay-node/observability");

void (async () => {
  await plugin.initialize({
  version: 1,
  components: [
    observability.ComponentSpec({
      version: 1,
      atif: observability.atifConfig({
        enabled: true,
        agent_name: "Planner",
        agent_version: "1.0.0",
        model_name: "unknown",
        output_directory: "logs",
        filename_template: "trajectory-{session_id}.json",
      }),
    }),
  ],
  });

  try {
    // Run instrumented application work here.
  } finally {
    plugin.clear();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use nemo_relay::observability::plugin_component::{
    AtifSectionConfig, ComponentSpec, ObservabilityConfig,
};
use nemo_relay::plugin::{
    clear_plugin_configuration, initialize_plugins, validate_plugin_config, PluginConfig,
};

let component = ComponentSpec::new(ObservabilityConfig {
    atif: Some(AtifSectionConfig {
        enabled: true,
        agent_name: "Planner".into(),
        agent_version: "1.0.0".into(),
        model_name: "unknown".into(),
        output_directory: Some("logs".into()),
        filename_template: "trajectory-{session_id}.json".into(),
        ..AtifSectionConfig::default()
    }),
    ..ObservabilityConfig::default()
});

let config = PluginConfig {
    version: 1,
    components: vec![component.into()],
    policy: Default::default(),
};

let report = validate_plugin_config(&config);
assert!(!report.has_errors());

let _active = initialize_plugins(config).await?;

// Run instrumented application work here.

clear_plugin_configuration()?;
Ok(())
}
```

## Manual API

Use the manual `AtifExporter` API when you need explicit collection boundaries
or one exporter object per run.

```python
from nemo_relay import AtifExporter

exporter = AtifExporter("session-1", "agent", "1.0.0", model_name="demo-model")
exporter.register("atif-exporter")

# Run instrumented application work here.

trajectory = exporter.export()
exporter.deregister("atif-exporter")
exporter.clear()
```

```js
const { AtifExporter } = require("nemo-relay-node");

const exporter = new AtifExporter("session-1", "agent", "1.0.0", "demo-model");
exporter.register("atif-exporter");

try {
  // Run instrumented application work here.

  const trajectoryJson = exporter.exportJson();
  console.log(trajectoryJson);
} finally {
  exporter.deregister("atif-exporter");
  exporter.clear();
}
```

```rust
use nemo_relay::api::subscriber::{deregister_subscriber, flush_subscribers, register_subscriber};
use nemo_relay::observability::atif::{AtifAgentInfo, AtifExporter};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let exporter = AtifExporter::new(
        "session-1".to_string(),
        AtifAgentInfo {
            name: "agent".to_string(),
            version: "1.0.0".to_string(),
            model_name: Some("demo-model".to_string()),
            tool_definitions: None,
            extra: None,
        },
    );
    register_subscriber("atif-exporter", exporter.subscriber())?;

    // Run instrumented application work here.

    flush_subscribers()?;
    let trajectory = exporter.export()?;
    let trajectory_json = serde_json::to_string_pretty(&trajectory)?;
    println!("{trajectory_json}");

    let _ = deregister_subscriber("atif-exporter")?;
    exporter.clear();
    Ok(())
}
```

## 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.
* `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.