Events
This page explains the lifecycle events emitted by scopes, tools, LLM calls, middleware, and subscribers.
What Events Represent
Events are the runtime record of what happened. NeMo Relay uses Agent
Trajectory Observability Format (ATOF) 0.1 as the canonical event format for
scopes, managed execution helpers, manual lifecycle APIs, subscribers, and
exporters.
Event Kinds
ATOF has two event kinds.
scope
Emitted when lifecycle work starts or ends. scope_category is start or
end, and category identifies the kind of work, such as agent, function,
tool, llm, retriever, embedder, reranker, guardrail, evaluator,
custom, or unknown.
mark
Emitted for named runtime checkpoints that are not full start/end lifecycle pairs.
The coding-agent CLI emits one session.start mark when a session lifecycle
begins. Its metadata includes the logical harness session_id, the Relay-owned
session_instance_id, and any configured session metadata such as user_id.
The mark does not copy the native startup-hook payload or force a turn scope to
open.
Shared Envelope
Every event carries a shared envelope:
kindatof_versionuuidparent_uuidtimestampnamedatadata_schemametadata
Scope events add scope_category, category, attributes, and optional
category_profile. Mark events can optionally carry category and
category_profile, though the current public mark helpers emit generic marks
without those optional category fields.
Runtime helpers assign timestamps automatically by default. Manual lifecycle helpers for scopes, marks, tool calls, and LLM calls also accept an explicit timestamp for integrations that are replaying events or mirroring an upstream framework clock. Use explicit timestamps only when the caller owns a reliable event time; otherwise prefer the runtime-generated timestamp.
Native Rust, Python, Node.js, and FFI event-producing APIs construct canonical events synchronously, enqueue subscriber delivery, and return without waiting for subscriber callbacks, exporter work, file writes, or tracing processors. Use the binding flush API when a test or shutdown path must wait for queued subscriber work.
Semantic Meaning
These sections describe how event shapes and relationships should be interpreted by consumers.
Start and End Pairing
Start and end scope events for the same lifecycle pair by UUID. That pairing lets subscribers compute durations, reconstruct call boundaries, and preserve nesting in downstream systems.
Input and Output Payloads
ATOF uses one data field. For scope events, data is the semantic input on
scope_category == "start" and the semantic output on
scope_category == "end".
LLM start annotations use freshness state owned by the nearest agent scope:
- Fresh history: An agent starts fresh when its scope opens and becomes
fresh again after a
compactionmark. The first subsequent LLM start retains the complete available request history and consumes the fresh state. - Stale history: After that first start, later LLM starts retain all system instructions, the latest user message, and every following assistant or tool message. If the history has no user message, the annotation retains system instructions and the final non-system message.
- Scope ownership: Nested agents track freshness independently. The implicit root is the freshness owner for work outside an explicit agent. Non-agent scopes inherit their nearest agent’s state instead of starting a new budget.
- Codec projection: When a request codec supplies an annotation, Relay applies the same history projection to the event’s provider-shaped input. If that event-only encode fails, Relay retains the complete event input and annotation. The request used for provider execution remains unchanged.
- Concurrency: Concurrent starts have no guaranteed emission order, but exactly one consumes a fresh state. Events are independent observations, not a replayable history stream.
Category Profiles
Category-specific fields live under category_profile. NeMo Relay uses
model_name for LLM events, tool_call_id for tool events, and subtype for
custom-category events. LLM codec annotations, when present, are serialized
under category_profile.annotated_request on LLM start events and
category_profile.annotated_response on LLM end events. Unknown profile fields
are preserved so newer producers can interoperate with older consumers.
Annotated Request and Response Data
LLM codecs can enrich LLM events with annotated request and response data. These
annotations are part of the canonical event JSON under category_profile when
they are present, so ATOF JSONL export and in-process subscriber JSON expose the
same payload shape. Request annotations use the owning agent’s freshness state.
Event Contract Boundary
The event is the canonical envelope and handoff point for subscribers and exporters. Consumers should prefer typed event fields, binding helpers, and annotated request or response data before falling back to raw payload re-reading.
Before that handoff, event sanitizers can replace data, category_profile,
and metadata. The runtime keeps identity, lifecycle, category, timestamp,
and parent fields immutable. Refer to
Event Sanitizers for registration levels and
ordering.
Events preserve runtime facts. They do not decide which scope owns a call, how replay or cost policy is applied, how redaction policy is configured, how streams are blocked, or how exporter-specific semantic projection works. Those decisions stay in the session, codec, plugin, guardrail, or exporter layer that owns them.
Automatic Skill-Load Marks
NeMo Relay automatically records an on-demand skill-load attempt when an
instrumented tool explicitly names a skill or requests a complete SKILL.md
read. The following payload excerpt shows the stable skill-load fields.
Standard fields from the shared envelope, such as
atof_version, uuid, parent_uuid, and timestamp, are omitted.
Automatic skill-load extraction is best effort. Relay emits skill.load only
through one of the following detection paths:
- An integration supplies a precomputed detection with a nonempty skill name and supported source before removing the original tool arguments.
- Relay sees a recognized first-class skill tool with a nonempty
skill,skill_name, ornameargument. - Relay sees the original arguments for a supported reader request that meets all of the reader criteria below.
For a reader request, all of the following conditions must be true:
- The tool name matches a supported reader, either directly or through one supported shell wrapper.
- A recognized argument field or parsed command contains a path or URI. Its
final path component equals
SKILL.mdunder an ASCII case-insensitive comparison. - The path’s immediate parent directory has a nonempty name, which Relay uses as the skill name.
- The request reads the entire file. Relay ignores requests that use partial or range controls, unsupported transformations, metadata-only operations, pipelines, redirections, command substitutions, or compound commands. Relay also ignores output-transforming and unreviewed reader options.
Relay checks the request before tool execution. The mark records an attempted load even if execution later fails. If Relay doesn’t emit a mark, that doesn’t prove that the skill wasn’t loaded.
Relay uses these skill_load_source values:
For structured reads, Relay recognizes only the following reviewed tool-name suffixes and argument fields:
- Tool suffixes:
read,read_file,read_text_file,read_multiple_files,file_read,read_resource,get_file_contents, andread_file_content. - Argument fields:
path,file_path,filepath,filename,file,paths,uri, andabsolute_path.
For shell reads, Relay recognizes the following direct shell tool names after normalizing case and nonalphanumeric separators:
- Tool names:
sh,bash,zsh,fish,shell,shell_command,exec,exec_command,execute,terminal,run_command,run_shell_command,shell_exec,powershell, andpwsh. - Argument fields:
commandandcmd.
Relay also recognizes a single wrapper layer inside a direct shell tool. Each wrapper must use one of the following forms:
- POSIX shells:
sh -c,bash -c,bash -lc,zsh -c, orzsh -lc. - Fish:
fish -corfish --command=.... - PowerShell:
powershell -Commandorpwsh -Command.
The sh, bash, zsh, and fish wrappers support cat, bat, and batcat.
PowerShell wrappers support Get-Content. Relay treats unknown tool names,
argument fields, wrapper forms, Windows type, and PowerShell aliases as
unsupported until an integration explicitly adds them to the registry.
The shell reader option registry is deliberately narrow:
catsupports no options or the exact content-preserving-uoption. Other options, including bundled short options, are unsupported.batandbatcatrequire one exact plain-output option:-por--plain. Relay rejects every additional option.Get-Contentsupports positional paths and the exact-Path,-LiteralPath,-Raw,-Encoding,-ReadCount,-AsByteStream, and-Forceoptions. PowerShell abbreviations and other options are unsupported.
For a reader request, the final path component must equal SKILL.md under an
ASCII case-insensitive comparison. Relay uses the immediate parent directory as
the skill name. Relay doesn’t emit a mark when a structured request has a
offset other than the integer 0, a limit, or a range control. It also
doesn’t emit a mark for shell commands that use pipelines, redirections,
compound commands, or range-limited readers, or help, version, listing, or
diagnostic operations. It also doesn’t emit a mark for output-transforming or
unreviewed reader options. An explicit structured offset of 0 still
represents a complete read. Relay omits the full path and shell command from
the mark.
The mark has these lifecycle semantics:
- Relay emits it immediately after the tool-start event and before execution, with the tool span as its parent.
- The mark records an eager load attempt and remains in the event stream if the tool later fails.
- Relay doesn’t emit the mark for a blocked call that never starts.
- Relay emits at most one mark for each skill in a tool call. A later tool call that attempts the same load emits another mark.
Claude Code Skill, Codex complete-reader shell calls, Hermes skill_view, and
tool calls made through the Rust, Python, Node.js, Go/FFI, LangChain, LangGraph,
and Deep Agents paths use this observed contract. The existing Deep Agents
DeepAgents Skills Configured mark remains a separate configuration summary;
it does not mean that a skill body was loaded.
Claude Code exposes slash-command expansion without saying whether the command
came from a skill or a legacy command. For a nonempty slash-command expansion,
Relay emits skill.load.inferred with data.skill_name,
metadata.skill_load_source = "prompt_expansion", and
metadata.inferred = true. Relay also includes metadata.command_source when
Claude Code provides it. Keep inferred and observed counts separate.
Exporters handle the mark as follows:
- ATOF preserves the canonical mark event.
- ATIF omits marks because it models trajectory steps rather than independent point-in-time events.
- OpenTelemetry and OpenInference apply their configured
mark_projectionto parented marks.
How Events Are Produced
Scope APIs emit scope start and end events and can also emit named mark
events. Managed tool and LLM helpers emit scope events with category set to
tool or llm. Conditional-execution rejections and explicit mark points let
the runtime record important state transitions even when there is no full nested
callback to wrap. Subscriber delivery is downstream from event construction and
does not block the emitting call on native bindings.
ATOF export writes this raw canonical event stream. ATIF, OpenTelemetry, and OpenInference are downstream projections over the same events.