agent-observability-replay-trace
Use when a developer wants to iterate on ONE specific Agent Observability / LLM Obs trace whose output they didn't like — re-running that trace against their LOCAL code, seeing a concise diff of the old vs new output, and looping (change code → replay → diff) until satisfied. Invoked as /agent-observability-replay-trace <trace-id> [changes to test]. Signals: "replay this trace"; "iterate on a trace"; "this trace's output is wrong, fix it and re-run"; "re-run trace <id> with <change>"; pasting a trace id from the Agent Observability UI with a description of what to fix. It fetches the trace via the datadog-llmo MCP or the pup CLI, edits code, re-runs the app to emit a NEW trace, and diffs the two — no local server, no browser. For agents traced with ddtrace / LLM Obs (Python first-class), with JSON-serializable entry input. Do NOT use for: scored Experiments or the browser "Replay" button (that's agent-observability-replay-experiment), building an experiment from a dataset/CSV, writing evaluators, root-causing failed traces, or RUM/HTTP session replay.
NPX Install
npx skill4agent add datadog-labs/agent-skills agent-observability-replay-traceTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Replay a trace against local code
/agent-observability-replay-trace <trace-id> [<changes to test>]references/details.mdreferences/local-setup.mddetails.mdENTRYPOINTSdatadog-llmotelemetry.intentskill:agent-observability-replay-trace[<inv_id>] — skill:agent-observability-replay-trace:start[<inv_id>] — :startskill:agent-observability-replay-trace:start[3a9f1c2b] — fetch the original trace's baseline outputtelemetry.intentInteraction model — selector gates, never a hard stop
AskUserQuestionScope — check first
- Traced with / LLM Obs (an
ddtrace+ a discoverable entrypoint). Python is first-class; other languages work but you write the runner to the contract in their SDK/build tooling.ml_app - JSON-serializable entrypoint input, and a callable seam for the root span (see step 3.5 — not a binary "is it runnable?"; deployed-only apps often still expose a plain callable).
- A trace-access backend — the MCP (used when present) or the
datadog-llmoCLI (fallback, and the easier install if you have neither) (step 0).pup - Credentials: +
DD_API_KEY+ provider key(s). NotDD_SITE— plain trace, not an Experiment (that'sDD_APP_KEY).agent-observability-replay-experiment - Side effects, irreversible: replaying re-runs real code (model spend + real writes), and LLM Obs
traces cannot be deleted — a mis-scoped replay (wrong ml_app) permanently pollutes the production app's
dashboards/eval sets. That's why the isolation (steps 4/6/7) is load-bearing, not tidy. Warn before the first replay.
<ml_app>-local
Workflow
0. Ensure a trace-access backend
mcp__datadog-llmo-mcp__*content_infopuppup authbrew tap datadog-labs/pack && brew install datadog-labs/pack/pup
pup auth loginclaude mcp add --scope user --transport http "datadog-llmo-mcp" "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=llmobs"details.mddata.spans[]spans[]--no-agentpup auth status1. Parse the command
<trace-id>ml_appLLMObs.enable(ml_app=…)DD_LLMOBS_ML_APP2. Fetch the trace + locate the baseline
total_duration_mstrace_urlmetadata.replay_inputreplay_entrypoint2.5. Check for fan-out
3. Resolve the entrypoint + input
- Entrypoint: if present; else infer from the root span (name/kind) + code and confirm with the user.
metadata.replay_entrypoint - Input: if present; else derive a suggested input (prefer the code signature — the rendered prompt is lossy) and have the user confirm/edit.
metadata.replay_input
3.5. Ensure a local run path (find the innermost callable seam)
__main__references/local-setup.md4. Ensure the two persistent artifacts (one-time setup)
- a) In-entrypoint annotation on the app's real entrypoint, so all future traces (production too)
self-describe. Stamp it at span start, not the success/deferred-finish path — a failed run must still
carry (those are the ones you most want to replay):
replay_inputNopythonLLMObs.annotate(span=span, metadata={"replay_entrypoint": "<stable id>", "replay_input": <extractor>})— the original trace is the baseline. (Non-Python annotate APIs differ — e.g. Goreplay_output; seespan.Annotate(llmobs.WithAnnotatedMetadata(...)).)details.md - Isolation pre-flight (before writing the runner): grep the entrypoint's call path for per-span/
per-call ml_app overrides (Go ; Python
llmobs.WithMLAppon a decorator or inml_app=). Those beat the init-levelLLMObs.annotate, so the app's spans can still land in production — tracer-level config is not proof of isolation. If any exist, the app's ml_app must resolve from env so-localwins.-local - b) The runner — satisfies the language-independent runner contract in (load env → derive
details.md→ dispatch one entrypoint on JSON → flush on every exit path incl. errors → refuse to start unless ml_app ends in<ml_app>-local→ print the-localml_app). Python: copy-localand fillscripts/replay_runner_template.py. Other languages: write to the contract — don't assume the Python API carries over (Go APIs + export-mode gotchas inENTRYPOINTS), and where the language has no in-process dotenv add a run wrapper (artifact c) that sources the project env, unsets ambient provider vars, and exports thedetails.mdoverride. Infer + confirm the run command; follow the host repo's build-file conventions (Bazel/Gazelle →-local, run Gazelle, build before replay).cmd/<name>/
5. (If a change was requested) edit, then gate
AskUserQuestion6. Replay
ANTHROPIC_API_KEYANTHROPIC_BASE_URLt0DD_API_KEY=<value>DD_TAGS=replay_run_id:<unique-id> <run cmd or wrapper> --entrypoint <id> --input-file <path><ml_app>-local7. Wait for the new trace
- Runner subprocess timeout = .
max(120s, ~3 × total_duration_ms) - Ingest poll: after it returns, poll the backend every ~5s up to ~2 min for the tag under
replay_run_id(pup:<ml_app>-local, plain--query "replay_run_id:<id>"). Before ever reporting "not found," re-query with no tag filter (justkey:value+ window): if that returns spans, your filter/parse/scope is wrong — not ingestion. A false "no trace" reads as normal and invites a wasteful re-run.<ml_app>-local - Verify isolation on each hit — a tag match is NOT proof. /tag matching can return a span whose real
--queryis a different app (theml_appfilter gets ignored). Read--ml-appoff every returned span and assert it ends inml_appbefore reporting a clean replay — otherwise you report "clean replay under-local" while the trace is actually in production (which you can't undo). This false confidence is worse than the false negative. Don't hard-fail on timeout; offer to keep waiting.-local
8. Diff (with links to both traces)
- Old: verbatim — but under fan-out (you replayed one branch) link the branch span, not the whole-root url.
trace_url - New (replay): must carry or it opens empty — and the
ml_app=<ml_app>-localis an org-switch wrapper (trace_url), so inject…/switch_to_user/<id>?next=<encoded /llm/traces …>&flow=org_switchinto the decodedml_app=<ml_app>-localquery and re-encode; do NOT append to the outer URL (mechanics innext). Browser-unverifiable from here — confirm once it opens non-empty.details.md
9. Gate — iterate, or stop on a broken harness
AskUserQuestionReference
- — trace backend + pup exact flags, the runner contract (+ Go, export mode), polling + the false-negative sanity check, the trace-link scoping fix, limitations. Read before pup / the runner.
references/details.md - — making a deployed-only app locally runnable (step 3.5). Read when that gap shows.
references/local-setup.md - — the Python runner to copy + fill.
scripts/replay_runner_template.py