Deep Researcher Agent uses NeMo Relay as its observability runtime.
Relay gives Deep Researcher Agent users four complementary views:
- Developer logs show agent, LLM, and tool activity in the terminal.
- ATOF JSONL provides a durable, machine-readable event stream for debugging and post-processing.
- OpenTelemetry (OTEL) exports the same scope tree to backends such as Phoenix.
- Pricing enrichment attaches model cost data when a configured catalog matches the observed model.
Logging, ATOF export, full observability payloads, and redaction are enabled by default. OTEL export is opt-in so a default Deep Researcher Agent installation does not attempt to contact an observability server. Pricing is enabled with no catalog sources; token usage is still recorded, but monetary cost is not estimated until you configure prices.
NeMo Relay and the LangChain, LangGraph, and Deep Agents integrations are installed with Deep Researcher Agent:
./scripts/setup.shFor an existing development checkout, synchronize the environment:
uv syncVerify the installed version:
uv run python -c 'from importlib.metadata import version; print(version("nemo-relay"))'Deep Researcher Agent supports the Relay version range pinned in pyproject.toml.
Relay is configured under the top-level Deep Researcher Agent workflow:
workflow:
_type: chat_deepresearcher_agent
relay:
logging: true
observability:
enable_full_payloads: true
atof:
enabled: true
output_directory: ./relay
filename: deep-researcher-relay.atof.jsonl
mode: append
opentelemetry:
enabled: false
redaction:
enabled: trueMost users can omit this block and use the defaults. Add only the settings that you intend to change.
| Setting | Default | Purpose |
|---|---|---|
logging |
true |
Register Deep Researcher Agent's Relay console subscriber. |
observability.enable_full_payloads |
true |
Preserve supported inputs, outputs, metadata, and annotated usage for sanitization and export. |
observability.atof.enabled |
true |
Write Relay events to ATOF JSONL. |
observability.atof.mode |
append |
Preserve events across turns and async jobs. Use overwrite only for a single isolated run. |
observability.opentelemetry.enabled |
false |
Export Relay scopes over OTEL when explicitly enabled. |
redaction.enabled |
true |
Sanitize supported sensitive values before logs and exporters receive them. |
pricing.sources |
[] |
Model pricing catalogs used to enrich Relay usage. |
Relay configuration is strict. Unknown fields, invalid endpoint URLs, and an invalid source shape fail workflow validation instead of being ignored.
Set output_directory and filename independently:
workflow:
relay:
observability:
atof:
enabled: true
output_directory: ./observability/traces
filename: deep-researcher-development.atof.jsonl
mode: appendRelative directories are resolved from the working directory where Deep Researcher Agent is started. Use an absolute directory for containers, services, or async workers when their working directories might differ:
output_directory: /var/lib/deep-researcher/relayThe resulting file is
/var/lib/deep-researcher/relay/deep-researcher-development.atof.jsonl. Ensure every worker can write
to the directory. Keep mode: append when multiple user turns or async jobs
share a file; choose a unique filename instead of overwrite when you need
per-run isolation.
By default, Deep Researcher Agent appends events to:
relay/deep-researcher-relay.atof.jsonl
Each line is one JSON event. Use jq to inspect it:
# Follow new events while Deep Researcher Agent runs.
tail -f relay/deep-researcher-relay.atof.jsonl | jq -c .
# Count scope starts by category.
jq -s '
[.[] | select(.kind == "scope" and .scope_category == "start")]
| group_by(.category)
| map({category: .[0].category, count: length})
' relay/deep-researcher-relay.atof.jsonl
# Show LLM usage recorded on completed LLM scopes.
jq -c '
select(.category == "llm" and .scope_category == "end")
| {
model: .category_profile.annotated_response.model,
usage: .category_profile.annotated_response.usage,
status: .metadata["otel.status_code"]
}
' relay/deep-researcher-relay.atof.jsonl
# Find scopes that do not have exactly one start and one end.
jq -s '
[.[] | select(.kind == "scope")]
| sort_by(.uuid)
| group_by(.uuid)
| map({
uuid: .[0].uuid,
name: .[0].name,
starts: map(select(.scope_category == "start")) | length,
ends: map(select(.scope_category == "end")) | length
})
| map(select(.starts != 1 or .ends != 1))
' relay/deep-researcher-relay.atof.jsonlAn empty final result from the balance check means every recorded scope closed
exactly once. ATOF mode: append is important for web and async-job testing:
multiple worker processes can initialize exporters, and overwrite can replace
events written by an earlier job.
Deep Researcher Agent registers a process-wide Relay subscriber when workflow.relay.logging is
enabled. It reads the sanitized Relay lifecycle stream and renders developer logs:
[Chain Start] shallow_research_agent
[AGENT] model-name
[Tool Calls] 1 tool(s) requested
→ web_search_tool
[Tokens] prompt=1882, completion=42, model=model-name
[Tool Start] web_search_tool
[Tool Result] chars=8472 ref=sha256:...
[Chain End] shallow_research_agent
The subscriber does not instrument the workflow itself. Relay's maintained framework integrations and Deep Researcher Agent's semantic agent/tool scopes produce events; the subscriber only formats those events. This keeps console logging aligned with ATOF and OTEL rather than maintaining a second callback-based trace.
Raw prompts, responses, tool arguments, and tool results are not printed.
Instead, the subscriber logs sizes and stable content references after Relay
redaction. Set the normal console log level under general.telemetry.logging.
Phoenix provides a local UI for inspecting the Relay hierarchy, latency, model inputs and outputs, tool calls, token usage, and errors.
Start Phoenix:
uvx --from arize-phoenix phoenix servePhoenix is available at http://localhost:6006. OTEL is commented out in the default Deep Researcher Agent configs. Uncomment or add this Relay block:
workflow:
relay:
observability:
opentelemetry:
enabled: true
endpoints:
- type: openinference
endpoint: http://localhost:6006/v1/traces
service_name: deep-researcher-relay
resource_attributes:
openinference.project.name: deep-researcher-relay
deployment.environment: developmentThe openinference projection gives Phoenix semantic LLM, agent, and tool span
attributes and the corresponding UI icons. openinference.project.name
selects the Phoenix project. Use a distinct project name for each Deep Researcher Agent
environment that you want to compare independently.
Relay also supports full and gen_ai OTEL projections. Use full when the
destination needs the richest Relay-native attributes, and gen_ai when the
destination expects OpenTelemetry GenAI semantic conventions. Phoenix users
should normally use openinference.
If a trace does not appear in the expected Phoenix project:
- Check that Phoenix is listening at the configured endpoint.
- Confirm
opentelemetry.enabled: trueand inspect the Deep Researcher Agent log for export errors. - Look in Phoenix's
defaultproject and any project configured globally on the machine. - Inspect
~/.config/nemo-relay/plugins.toml. NeMo Relay automatically discovers user-level plugin configuration. If Relay was already configured for another application or coding agent, that exporter can send the Deep Researcher Agent trace to its globally configured Phoenix project instead of the project you are currently viewing. - Compare the Phoenix trace with the local ATOF file. If ATOF contains the scopes, instrumentation worked and the remaining issue is OTEL destination, project selection, export, or batching.
Keep personal Relay configuration when it is needed by other applications. Use an Deep Researcher Agent-specific project in the workflow configuration and account for all discovered exporters when validating where telemetry is sent.
Deep Researcher Agent creates one root trace for each user turn. Multiple turns in the same
conversation have different trace IDs and share the Phoenix session.id, so
the session view groups them without merging their execution trees.
An async deep-research job runs in a separate Relay trace because it executes outside the request task, often in another Dask worker process. Job metadata links the background trace to the submitted job and originating request.
A typical deep-research trace is structured as follows:
<workflow>
└── chat_deepresearcher_agent
├── intent_classifier
│ └── LLM
├── clarifier_agent
│ ├── LLM
│ └── tool
└── deep_research_agent
├── planner-agent
│ └── LLM
├── researcher-agent
│ ├── LLM
│ └── tool
└── writer-agent
└── LLM
Use the tree in this order:
- Start at the root and check its terminal status and duration.
- Find the slowest agent, LLM, or tool child.
- Inspect LLM spans for model, token usage, response status, and sanitized input/output attributes.
- Inspect tool spans for tool name, duration, sanitized arguments/results, and errors.
- For parallel researchers, compare sibling spans rather than adding their wall-clock durations.
- For a failed async job, search by
deep-researcher.job.idand confirm the root scope has one start, one end, and anERRORterminal status.
Internal graph-routing nodes are represented as decision metadata/events where possible rather than noisy agent spans. Framework-generated names can still appear when the underlying integration exposes a real execution boundary.
Relay redaction runs before the Deep Researcher Agent logging subscriber, ATOF sink, and OTEL
exporter. The default detectors cover common credentials and personal data.
Deep Researcher Agent can also request privacy-mode sanitization for supported data and
category_profile payloads through request privacy context.
Redaction reduces accidental disclosure; it is not a substitute for auditing the destination's access controls, retention, and data policy. Validate every configured exported attribute with synthetic sensitive values before enabling full payloads in a production environment.
Default Deep Researcher Agent configs omit the Relay pricing block. The resulting empty source list records token usage without claiming a monetary cost. Pricing depends on the provider, deployment, contract, region, cache policy, and date.
Use the dedicated example when you want model cost enrichment:
nat serve \
--config_file configs/nemo_relay/config_web_default_with_pricing.yml \
--port 8000That config loads configs/nemo_relay/relay_pricing_catalog.json:
pricing:
enabled: true
sources:
- type: file
path: configs/nemo_relay/relay_pricing_catalog.jsonRelay matches the observed provider/model name against the catalog and adds cost information to the annotated LLM usage. Review and date every rate before using it operationally. A zero-dollar hosted API rate does not mean that a self-hosted deployment has no infrastructure cost.
Relay's pricing catalog covers model usage. Deep Researcher Agent's tokenomics report also supports per-call prices for external tools such as web search. After a run, generate the report with:
PYTHONPATH=src python -m deep_researcher_agent.tokenomics.report \
--trace relay/deep-researcher-relay.atof.jsonl \
--config frontends/benchmarks/deepresearch_bench/configs/config_tokenomics_pricing.ymlThe report uses Relay-attributed model cost when present and the report pricing configuration as a fallback. It also calculates configured tool API charges and writes a self-contained HTML report. See Profiling and Cost Analysis for report fields, phase attribution, and pricing maintenance.
Validate an edited workflow before starting Deep Researcher Agent:
uv run python .agents/skills/deep-researcher-configure-workflow/scripts/validate_config.py \
configs/config_web_default_llamaindex.ymlThen start Deep Researcher Agent, run one shallow turn and one deep-research job, and verify all three views that you enabled:
- console logs show agent, LLM, and tool lifecycle activity;
- ATOF contains balanced scopes and annotated LLM usage;
- Phoenix shows the expected project, per-turn traces, shared session grouping, and an independent async-job trace.