> 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 Observability Format (ATOF)

> Configure raw ATOF event export to JSONL files and streaming endpoints.

Use the `atof` section when you want the raw Agent Trajectory Observability
Format (ATOF) `0.1` event stream written as JSONL or streamed to raw-event
collectors.

ATOF JSONL export is useful for local debugging, offline inspection, and
preserving the canonical event stream before it is translated into another
format. Streaming endpoints are useful when a collector wants the same raw
event shape in near real time.

## `plugins.toml` Example

Add the following ATOF exporter configuration to `plugins.toml`:

```toml
version = 1

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

[components.config]
version = 1

[components.config.atof]
enabled = true
output_directory = "logs"
filename = "events.jsonl"
mode = "overwrite"

[[components.config.atof.endpoints]]
url = "http://localhost:8080/events"
transport = "http_post"
timeout_millis = 3000
field_name_policy = "replace_dots"
```

This configuration registers the plugin-managed ATOF exporter and writes one
JSON object per lifecycle event to `logs/events.jsonl`. It also sends each raw
ATOF event to the configured endpoint.

## Fields

The following table describes the top-level ATOF settings:

| Field              | Default                                 | Notes                                                                                      |
| ------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------ |
| `enabled`          | `false`                                 | Must be `true` to write events.                                                            |
| `output_directory` | Current working directory               | Directory containing the JSONL file.                                                       |
| `filename`         | Timestamped `nemo-relay-events-*.jsonl` | Explicit output filename.                                                                  |
| `mode`             | `append`                                | `append` or `overwrite`.                                                                   |
| `endpoints`        | `[]`                                    | Optional streaming destinations. File output remains active when endpoints are configured. |

## Streaming Endpoints

Each endpoint receives the same raw ATOF JSON object that the file exporter
writes as one JSONL line. Endpoints are independent: a failed endpoint is
skipped or retried without blocking file output or other endpoints.

The following table describes each streaming endpoint:

| Field               | Default     | Notes                                                                                                      |
| ------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `url`               | Required    | Endpoint URL.                                                                                              |
| `transport`         | `http_post` | `http_post`, `websocket`, or `ndjson`.                                                                     |
| `headers`           | `{}`        | String-to-string headers for requests or handshakes.                                                       |
| `timeout_millis`    | `3000`      | Per-endpoint timeout. Must be greater than `0`.                                                            |
| `field_name_policy` | `preserve`  | Field-name handling before Relay sends endpoint events. Accepted values are `preserve` and `replace_dots`. |

`preserve` sends canonical ATOF field names unchanged. `replace_dots` replaces
dots in JSON object keys with underscores recursively. If replacement produces
a collision, Relay keeps both values and deterministically appends `_2`, `_3`,
and later suffixes as needed. Validation rejects any other
`endpoints[i].field_name_policy` value.

* `http_post` sends each event as one JSONL record in an HTTP `POST` request
  with `Content-Type: application/x-ndjson`. Any `2xx` response is treated as
  success.
* `websocket` opens one connection and sends each event as one JSON text
  message.
* `ndjson` opens one long-lived HTTP upload and writes each event as one
  newline-delimited JSON record.

`force_flush()` flushes file output and drains queued endpoint events without
closing streaming connections. `shutdown()` is terminal: it flushes pending
work, closes streaming connections, and makes later events no-op.

## Expected Output

Each emitted scope, tool, LLM, middleware, or mark event is written as one ATOF
JSON object per line. For event field semantics, refer to
[Events](/about-nemo-relay/concepts/events).

ATOF is the raw-event export path. It preserves the canonical event shape; it
does not add semantic extraction, replay policy, scope-owner resolution, or
exporter projection rules.

Register the plugin before instrumented work starts and clear it during
shutdown so file handles flush.

## Plugin Configuration

Use plugin configuration when the application should let NeMo Relay own the ATOF
exporter lifecycle. The following examples configure and activate the ATOF
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 (
    AtofConfig,
    AtofEndpointConfig,
    ComponentSpec,
    ObservabilityConfig,
)

config = plugin.PluginConfig(
    components=[
        ComponentSpec(
            ObservabilityConfig(
                atof=AtofConfig(
                    enabled=True,
                    output_directory="logs",
                    filename="events.jsonl",
                    mode="overwrite",
                    endpoints=[
                        AtofEndpointConfig(
                            url="http://localhost:8080/events",
                            transport="http_post",
                        )
                    ],
                )
            )
        )
    ]
)

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,
      atof: observability.atofConfig({
        enabled: true,
        output_directory: "logs",
        filename: "events.jsonl",
        mode: "overwrite",
        endpoints: [
          {
            url: "http://localhost:8080/events",
            transport: "http_post",
          },
        ],
      }),
    }),
  ],
  });

  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::{
    AtofEndpointSectionConfig, AtofSectionConfig, ComponentSpec, ObservabilityConfig,
};
use nemo_relay::plugin::{
    clear_plugin_configuration, initialize_plugins, validate_plugin_config, PluginConfig,
};

let component = ComponentSpec::new(ObservabilityConfig {
    atof: Some(AtofSectionConfig {
        enabled: true,
        output_directory: Some("logs".into()),
        filename: Some("events.jsonl".into()),
        mode: "overwrite".into(),
        endpoints: vec![AtofEndpointSectionConfig {
            url: "http://localhost:8080/events".into(),
            transport: "http_post".into(),
            headers: Default::default(),
            timeout_millis: 3000,
            field_name_policy: "replace_dots".into(),
        }],
    }),
    ..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 `AtofExporter` API when a test or script needs a custom
subscriber name or explicit registration window.

```python
from nemo_relay import AtofEndpointConfig, AtofExporter, AtofExporterConfig, AtofExporterMode

config = AtofExporterConfig()
config.output_directory = "logs"
config.filename = "events.jsonl"
config.mode = AtofExporterMode.Overwrite
config.endpoints = [
    AtofEndpointConfig("http://localhost:8080/events", transport="http_post")
]

exporter = AtofExporter(config)
exporter.register("atof-exporter")

# Run instrumented application work here.

exporter.force_flush()
exporter.deregister("atof-exporter")
exporter.shutdown()
```

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

const exporter = new AtofExporter({
  outputDirectory: "logs",
  filename: "events.jsonl",
  mode: "overwrite",
  endpoints: [{ url: "http://localhost:8080/events", transport: "http_post" }],
});
exporter.register("atof-exporter");

try {
  // Run instrumented application work here.

  exporter.forceFlush();
} finally {
  exporter.deregister("atof-exporter");
  exporter.shutdown();
}
```

```rust
use nemo_relay::observability::atof::{
    AtofEndpointConfig, AtofEndpointTransport, AtofExporter, AtofExporterConfig, AtofExporterMode,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = AtofExporterConfig::new()
        .with_output_directory("logs")
        .with_filename("events.jsonl")
        .with_mode(AtofExporterMode::Overwrite)
        .with_endpoint(AtofEndpointConfig::new(
            "http://localhost:8080/events",
            AtofEndpointTransport::HttpPost,
        ));
    let exporter = AtofExporter::new(config)?;
    exporter.register("atof-exporter")?;

    // Run instrumented application work here.

    exporter.force_flush()?;
    let _ = exporter.deregister("atof-exporter")?;
    exporter.shutdown()?;
    Ok(())
}
```

## Common Configuration and Runtime Issues

* `mode` is not `append` or `overwrite`.
* `endpoints[i].url` is empty, `endpoints[i].transport` is not supported, or
  `endpoints[i].timeout_millis` is `0`.
* The output directory is not writable at runtime.
* `nemo-relay doctor` cannot deliver its synthetic ATOF mark probe to a
  configured endpoint.
* ATOF is enabled in a target that cannot access the native filesystem.