Agent Trajectory Observability Format (ATOF) Core Specification
Agent Trajectory Observability Format (ATOF) Core Specification
NVIDIA NeMo Relay reference implementation: crates/types/src/api/event.rs
Companion documents:
- Events — NeMo Relay event production and runtime semantics.
- ATOF Export — JSONL file and stream export configuration.
- ATOF to ATIF Examples — conversion reference, mapping table, and runnable examples.
1. Overview
ATOF (Agentic Trajectory Observability Format) is the wire format for agent runtime subscriber callbacks. Events represent the lifecycle of scopes — composable units of agent work — within the runtime. Subscribers receive events in real time as the runtime executes agent workflows.
Primary purpose: lossless replay for inspection and evaluation. An ATOF event stream MUST carry enough information to reconstruct what happened in an agent run — identity, call graph, LLM messages in/out, tool calls and results — so that humans and tools can debug, audit, and evaluate the run post-hoc.
Transport is JSON Lines: one JSON object per line. The kind field at the top of every event is the primary discriminator. ATOF v0.1 defines two event kinds:
"scope"— a scope lifecycle event (start or end, distinguished byscope_category)"mark"— a point-in-time checkpoint was recorded
A scope event carries a required scope_category field valued in "start" or "end". A start and end pair shares the same uuid (§5.3).
What kind of work an event represents — an LLM call, a tool invocation, an agent turn, a retriever lookup, a vendor extension — is carried by the category field. Kind-specific typed fields (model_name for llm, tool_call_id for tool, subtype for custom, future fields for other categories) are packaged into a single optional category_profile object. The category_profile is null for tier-1 opaque events and for categories with no kind-specific fields; tier-2 producers populate the keys appropriate to the category. Keeping the profile as a sub-object keeps the envelope flat and extensible — adding a retriever profile shape in the future does not bloat the top-level JSON.
category is REQUIRED on scope events and OPTIONAL on mark events. A mark event MAY carry a category to indicate that the checkpoint relates to a particular kind of work (e.g., an "llm" mark); when absent, the mark is a generic checkpoint.
Wire envelope example:
data is empty in this example. Producers can populate it with any application-defined JSON object.
1.1 Two Producer Enrichment Tiers
ATOF is designed for progressive enrichment at the producer’s discretion. A producer emits what it knows; absent fields are legal everywhere except where noted.
Tier 1 Compatibility
Tier 1 must always work. A consumer that doesn’t understand tier-2 enrichment MUST still preserve the event verbatim. Consumers SHOULD NOT reject events whose category they don’t recognize — unknown values are forward-compat extensions, not errors.
1.2 The Structured Fields at a Glance
Beyond the base envelope (kind, uuid, parent_uuid, timestamp, name, atof_version), ATOF events carry these structured fields:
scope_category— lifecycle phase of ascopeevent. Closedenum:"start"or"end".attributes— behavioral flag array. Vocabulary is shared across categories (see §2.1); per-flag applicability is documented with each flag. Carried byscopeevents only.category— semantic category of the work. Closedenum(see §4). Required onscope, optional onmark.category_profile— category-specific typed fields packaged as a sub-object. Keys vary bycategory—subtypeforcustom,model_nameforllm,tool_call_idfortool, additional keys reserved for future categories (see §4.4). Null for tier-1 opaque events and for categories with no kind-specific fields.data— application-defined payload. Opaque to ATOF. Onscopeevents, typically carries the scope’s input onscope_category: "start"and the scope’s output onscope_category: "end". Consumers MUST NOT dispatch ondatacontents.data_schema— optional identifier{name: string, version: string}describing the shape ofdata. Opaque to ATOF core; the producer declares it, and validation ofdataagainst the named schema is the consumer’s responsibility. The reference ATOF→ATIF converter provides two registries keyed on this identifier:nat.atof.schemasfor JSON Schema validators andnat.atof.extractorsfor payload parsers. Refer to Extending the converter for registration guidance.metadata— tracing and correlation envelope (trace_id,span_id, etc.).
2. Base Event Envelope
Every event carries the envelope fields below. The first six (kind, atof_version, uuid, parent_uuid, timestamp, name) are the structural identity of the event; data, data_schema, and metadata are common optional fields that MAY appear on any event. scope events add scope fields on top; mark events MAY carry category + category_profile (§3.2) and nothing else beyond this envelope.
Event kind discriminator.
ATOF protocol version in "MAJOR.MINOR" form, such as "0.1". Refer to §5.6.
Unique identifier for this event or span. The start and end events for a scope share a uuid.
UUID of the containing scope when this event was emitted. Null only for root scope events and mark events without parents.
Wall-clock time when the event was emitted. Refer to §5.1.
Human-readable label, such as "my_agent", "calculator__add", or "gpt-4.1".
Application-defined payload. ATOF treats this field as opaque.
Schema identifier {name: string, version: string} that describes the shape of data. ATOF Core treats the identifier as opaque; the consumer is responsible for validation.
Tracing and correlation envelope, such as {"trace_id": "...", "span_id": "..."}.
2.1 attributes — behavioral flag array
attributes is a cross-cutting field on scope events. mark does NOT carry attributes.
Canonical lowercase flag names, sorted and deduplicated. Emit an empty array ([]) when no flags are set.
Producers MUST emit attributes in lexicographic order with no duplicates. Consumers SHOULD treat the array as an unordered set and MUST preserve unknown flag names when re-emitting. Unknown flags SHOULD NOT be treated as errors.
Canonical flag vocabulary (shared across all categories; individual flag applicability noted):
Why defaults are “absence”: Each flag describes the exceptional case. Absence means the default applies — serial (not parallel), pinned (not relocatable), stateless (not stateful), single-payload (not streaming), local (not remote).
Flag extensibility. Implementations MAY emit additional flag names for vendor extensions; non-canonical flags SHOULD be namespaced with a dotted prefix — for example, "nvidia.speculative". Consumers MUST preserve unknown flag strings and MUST NOT reject events carrying them.
3. Event Kinds
3.1 scope event
Emitted at scope lifecycle transitions. A single scope span produces two scope events sharing the same uuid: one with scope_category: "start" when the scope is pushed onto the active scope stack, and one with scope_category: "end" when the scope is popped.
Scope event discriminator.
Lifecycle phase.
ATOF protocol version. Refer to §2.
Identifier shared by the start and end events for the same scope span.
Parent identifier from §2. Null on the root scope and identical on both events in a pair.
Timestamp from §2. The end timestamp is always strictly later than the start timestamp. Refer to §5.3.
Human-readable label from §2. Identical on both events in a pair.
Behavioral flags from §2.1. Identical on both events in a pair.
Semantic category from §4. Identical on both events in a pair.
Category-specific typed fields from §4.4. On an end event, model_name MAY identify the model that was actually used when it differs from the requested model, such as after provider routing.
Application-defined payload from §2. Typically contains the scope input on a start event and the scope output on an end event, but producers MAY populate it on either phase.
Schema identifier from §2.
Tracing and correlation metadata from §2.
3.2 mark event
Emitted as a point-in-time checkpoint. Unpaired (no start and end semantics). A mark MAY carry category + category_profile to indicate the kind of work the checkpoint relates to; when both are absent, the mark is a generic named timestamp.
Mark event discriminator.
ATOF protocol version from §2.
Unique identifier from §2.
Parent identifier from §2.
Event timestamp from §2.
Checkpoint label, such as "workflow_start" or "retry_attempt_2".
Semantic category from §4. Null or absent means that the mark is a generic checkpoint.
Category-specific typed fields from §4.4. REQUIRED when category == "custom" and must contain category_profile.subtype.
Optional checkpoint payload.
Schema identifier {name: string, version: string} that describes the shape of data. ATOF Core treats the identifier as opaque; the consumer is responsible for validation.
Tracing and correlation metadata from §2.
mark does NOT carry scope_category or attributes.
4. category Vocabulary
category classifies the kind of work an event represents. The canonical vocabulary is a closed set of lowercase strings:
category is REQUIRED on scope events. On mark events it is OPTIONAL — producers MAY omit it to emit a generic checkpoint, or populate it to tag the mark with the kind of work it relates to.
4.1 "unknown" is the tier-1 escape hatch
On scope events, producers that have a payload but no classification (the tier-1 pass-through case from §1.1) emit category: "unknown". This is ALWAYS valid. Consumers SHOULD NOT reject events with category: "unknown".
On mark events, the tier-1 equivalent is simply omitting category (since it is optional). Producers MAY still emit category: "unknown" explicitly to signal “I know about the category field but cannot classify this mark.”
4.2 category_profile.subtype when category == "custom"
When category == "custom", the event MUST carry category_profile.subtype: string naming the vendor category. The subtype string SHOULD follow a dotted-namespace convention to avoid collisions — for example:
"nvidia.speculative_decode""langchain.memory_retrieval""internal.audit_gate"
This rule applies to both scope and mark events.
When category != "custom", category_profile.subtype SHOULD be absent. Consumers SHOULD preserve the category_profile object verbatim on re-emission.
4.3 Extensibility
The category enum is closed but "custom" + category_profile.subtype provides unbounded vendor expressiveness. ATOF reserves the right to promote frequently-used subtype values into first-class category vocabulary entries in future versions (backward-compat MINOR bump).
4.4 The category_profile Object
Packages category-specific typed fields. It can be null for tier-1 opaque events and for categories with no defined profile keys in this version.
LLM model identifier when category == "llm". It can be null when the model is unknown.
LLM-provider correlation ID when category == "tool". It can be null when the tool was not invoked through an LLM tool-use flow.
Vendor category name. REQUIRED when category == "custom" per §4.2.
For category == "unknown", category_profile is null. For agent, function, retriever, embedder, reranker, guardrail, and evaluator, no profile keys are defined in v0.1; producers MAY emit null or {}. Future MINOR versions MAY define keys for these categories.
Unknown category_profile keys MUST be preserved verbatim by consumers. Adding new keys to an existing profile shape is a backward-compatible MINOR bump per §5.6.
5. Event Stream Semantics
5.1 Timestamp Format and Ordering
Accepted forms. Every event’s timestamp carries one of two interchangeable forms:
- RFC 3339 string (e.g.,
"2026-01-01T00:00:00.123456Z") — human-readable, interoperable with general-purpose date libraries, default choice for debug and log-tailing contexts. MUST end withZor an explicit UTC offset. - Integer epoch microseconds UTC (e.g.,
1767225600123456) — fast to parse (~15× faster than RFC 3339), ~50% smaller on the wire, safe in JSON numbers through year 2255. Chosen for high-throughput streams and columnar-storage pipelines.
Emitters choose per event. A single stream MAY contain events in both forms.
Why microseconds and not nanoseconds. JSON numbers are IEEE 754 doubles with 53 bits of integer precision (~9 × 10¹⁵). Nanoseconds since epoch for 2026 is ~1.76 × 10¹⁸ — exceeds safe integer range. Microseconds fits safely and remains precise enough for agent-scope event correlation.
Normalize Timestamps Before Sorting
Events are emitted in wall-clock order. Delivery from subscriber callbacks MAY arrive out-of-order for concurrent operations. Consumers MUST sort by timestamp before processing. When sorting a mixed-format stream, consumers MUST normalize both forms to a common representation (typically integer microseconds) before comparison — lexicographic string vs integer comparison is undefined.
ATIF compatibility. ATIF requires timestamps as ISO 8601 strings. RFC 3339 is a strict subset of ISO 8601, so the ATOF → ATIF converter forwards the RFC 3339 string form unchanged as a zero-cost pass-through; only the integer microsecond form is serialized to an RFC 3339 string before emitting ATIF.
5.2 Scope Nesting and parent_uuid
The runtime maintains a scope stack per async task. The parent_uuid of any event is the UUID of the scope that was on top of the stack when the handle was created. Following parent_uuid links upward reconstructs the full call graph.
The root scope has parent_uuid = null. The root scope’s events (both scope_category: "start" and scope_category: "end") are the only scope events in a well-formed stream that may carry a null parent_uuid (once the root scope is established). mark events MAY carry parent_uuid = null when emitted outside any scope.
5.3 Start/End Pairing
Every scope event with scope_category: "start" is paired with exactly one scope event with scope_category: "end" sharing the same uuid. The end event is always emitted strictly after the start event (strict: ts_micros(end) > ts_micros(start)).
mark events have no paired event — they are single-shot.
If the runtime dies before emitting a paired end event, the already-emitted start event remains in the stream, but no paired end event appears. The pairing guarantee is contingent on orderly shutdown. Consumers that detect an unpaired start event after the stream ends MAY synthesize an end event for downstream processing; such synthetic events are out of scope for ATOF Core.
5.4 UUID Uniqueness
Each scope span receives a unique UUID at creation time. The uuid is stable across the start and end events for the same scope. In the Rust reference implementation, UUID is v7 (time-ordered).
5.5 ID Relationships
Two distinct identifier namespaces appear in an ATOF stream:
uuid/parent_uuid— runtime identifiers attached to every event. Form the scope graph.category_profile.tool_call_id(onscopeormarkevents whencategory == "tool") — an LLM-provider identifier that bridges an LLM’s tool-call response with the resulting tool execution. Null when the tool was not invoked via an LLM tool-use flow.
5.6 ATOF Version and Negotiation
Every event carries a required atof_version field, formatted "MAJOR.MINOR" — e.g., "0.1". This section defines when producers bump the version and how consumers dispatch on it.
Reading rules. Consumers SHOULD accept any 0.Y event as ATOF-v0-family. Major-version bumps (1.0, 2.0) MAY introduce breaking changes; consumers that want forward compatibility MUST dispatch on the major version and fail fast on unknown majors.
Mixed-version streams. A single stream MAY contain events at different minor versions (0.1 and 0.2). Consumers MUST NOT reject a stream because it contains newer minor versions than expected; unknown fields are preserved per §2.
When to bump.
- Bump MINOR when adding new optional fields, new
category_profilekeys, new flag vocabulary, newcategoryvalues, or newattributesflags. Backward-compatible. - Bump MAJOR when renaming or removing required fields, changing
kindorscope_categorydiscriminator values, or altering pairing semantics. Breaking.
6. What ATOF Is Not
- Not ATIF. ATIF is a higher-level trajectory format with computed ancestry, merged observations, sequenced
step_ids, and turn-based structure. ATOF events are the raw observations ATIF is built from. Refer to the ATOF-to-ATIF conversion reference. - Not a metrics format. Token counts, latency budgets, cost attribution — those live in
datapayloads or in downstream aggregation. ATOF does not normalize or roll up metrics. - Not a trace format. ATOF is compatible with distributed tracing (subscribers can export to OpenTelemetry via
metadata.trace_id/metadata.span_id) but is not itself an OTLP-equivalent wire format. - Not a replay executor. An ATOF stream lets you reconstruct what happened. It does not provide the mechanism to re-run it — that’s a separate layer built on top.
7. Reference Implementations
- Python (consumer + test-producer):
packages/nvidia_nat_atif/src/nat/atof/in NVIDIA/NeMo-Agent-Toolkit. Pydantic models per event kind withmodel_config = ConfigDict(extra="allow")for lossless pass-through. - Producer runtimes: Agent runtimes emitting ATOF MAY use more granular internal types (e.g., separate
LlmStartEvent/ToolStartEventstructs in typed languages) for type-safe construction, but MUST serialize to ATOF’s two-kind wire format on emission. - Language bindings: Where a producer runtime exposes bindings to additional languages, those bindings SHOULD re-export the runtime’s event types via language-idiomatic wrappers while preserving the wire format on serialization.
Refer to the ATOF-to-ATIF conversion reference for the normative conversion mapping.
8. Roadmap / Under Consideration
The following capabilities have been deliberately deferred from v0.1. They may be added in a future version if concrete use cases demonstrate value.
- Terminal status on scope end. A
statusfield onscopeevents withscope_category: "end"— valued in"ok"/"error"/"cancelled"— to carry the scope’s terminal outcome on the wire. Consumers currently infer outcome (when needed) fromdatacontents defined by the producer. - Structured error payload. An
errorfield pairing withstatus == "error", carrying{message, type, traceback}for structured error reporting. - Cascading cancellation semantics. Normative guidance for how parent and child cancellation flows through the scope stack — contingent on
statusbeing adopted.
Producers and consumers experimenting with these fields ahead of standardization SHOULD namespace them (e.g., under data with a vendor-prefixed data_schema name) so that a future promotion into ATOF core remains backward-compatible.
Last updated: July 28, 2026, alongside the migration of ATOF v0.1 to NVIDIA NeMo Relay.