> 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 full documentation content, see https://docs.nvidia.com/nemo/relay/llms-full.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)

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

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

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

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

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

* `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, see
[Events](/about-nemo-relay/concepts/events).

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.

```python
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"])

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

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

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();
}
```

```rust
use nemo_relay::observability::plugin_component::{
    AtofEndpointSectionConfig, AtofSectionConfig, ComponentSpec, ObservabilityConfig,
};
use nemo_relay::plugin::{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,
        }],
    }),
    ..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?;
```

## 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,
};

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()?;
```

## Common Validation Failures

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