Agent Observability Replay Trace
DatadogOfficialPublished by Datadog
- Skill
- Workflow
- Software development
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a fast iteration loop for developers to troubleshoot and improve agent performance. By fetching a specific Agent Observability trace, the agent allows you to modify your local code and re-run the trace locally to observe the impact of your changes. It automates the process of replaying the trace, comparing the new output against the original baseline, and presenting a concise diff. This workflow is designed for Python-based agents instrumented with ddtrace/LLM Obs, ensuring that local testing remains isolated from production data by using a dedicated local ml_app environment.
What it does
- Fetches a specific production trace using the datadog-llmo MCP or pup CLI.
- Identifies the entrypoint and input for the trace to enable local execution.
- Scaffolds a local runner to execute the agent logic with the original trace input.
- Configures the local environment to ensure traces are tagged with a -local suffix for isolation.
- Performs a side-by-side diff between the original production output and the new local replay.
- Supports an iterative loop where you can modify code and re-replay until the desired output is achieved.
When to use it
- When you need to fix an agent's output that was incorrect in a production trace.
- When you want to verify that a code change produces the expected improvement without deploying.
- When you need to perform regression testing on a specific trace to ensure stability.
- When you need to isolate and debug a specific branch of a complex, fan-out agent execution.
Advantages
- Rapid feedback loop by running code locally instead of waiting for deployment.
- Prevents production pollution by enforcing strict -local ml_app isolation.
- Provides clear, concise diffs to visualize the impact of code changes.
- Reduces manual effort in setting up test harnesses for complex agent logic.
Usage examples
- “/agent-observability-replay-trace trace_id_12345”
- The agent fetches the specified trace, sets up a local replay environment, and displays the original output for comparison.
- “/agent-observability-replay-trace trace_id_12345 update the prompt to be more concise”
- The agent fetches the trace, applies the requested prompt change to your local code, re-runs the trace, and shows a diff of the output.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/datadog-labs/agent-observability-replay-trace/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip agent-observability-replay-trace.zip -d .agents/skills
Documentation by Datadog
Replay a trace against local code
A fast iteration loop on a single production trace: take a trace whose output a developer didn't like, optionally change the code, re-run it against their LOCAL code, and show a concise diff of old vs new output — repeating until they're happy. Assumes nothing about the project's layout.
Invoked from the developer's coding agent: /agent-observability-replay-trace <trace-id> [<changes to test>].
With no modification, do the replay + diff only (a reproduce/regression check), then offer to enter the loop.
This file is the workflow spine — terse on purpose. The depth lives in references/details.md (trace
backend + pup flags, the runner contract, export mode, polling, the trace-link scoping fix) and
references/local-setup.md (making a deployed-only app locally runnable). Read details.md before you touch
pup or generate the runner.
Writing code — keep comments minimal to none. Everything you generate or edit (the annotation, the
runner's ENTRYPOINTS entries, a local harness, iteration edits) should match the surrounding code and
carry no unnecessary comments — don't narrate what the code plainly does; add a comment only for a
genuinely non-obvious why.
Intent tagging: On every datadog-llmo MCP tool call, prefix telemetry.intent with skill:agent-observability-replay-trace[<inv_id>] — (a short per-run id, generated once and reused for every call) followed by a description of why the tool is being called. On the first MCP tool call only, use skill:agent-observability-replay-trace:start[<inv_id>] — instead (note the :start suffix). Example first call: skill:agent-observability-replay-trace:start[3a9f1c2b] — fetch the original trace's baseline output. pup-CLI calls carry no telemetry.intent, so this applies only on the MCP path.
Interaction model — selector gates, never a hard stop
This is a live loop. At every decision point present the choices as an AskUserQuestion selector (the
plan-mode-style menu), not a plain question that ends your turn. Two gates: (a) after you propose code
changes, before replaying; (b) after each diff. The selector's free-text option lets the user type detail
(what to refine) inline — act on it directly, don't ask a follow-up. Keep re-presenting after every replay
until they pick "stop here".
Scope — check first
Traced with
ddtrace/ LLM Obs (anml_app+ a discoverable entrypoint). Python is first-class; other languages work but you write the runner to the contract in their SDK/build tooling.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
datadog-llmoMCP (used when present) or thepupCLI (fallback, and the easier install if you have neither) (step 0).Credentials:
DD_API_KEY+DD_SITE+ provider key(s). NotDD_APP_KEY— plain trace, not an Experiment (that'sagent-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
<ml_app>-localisolation (steps 4/6/7) is load-bearing, not tidy. Warn before the first replay.
Workflow
0. Ensure a trace-access backend
Pick, in order: (1) the MCP if mcp__datadog-llmo-mcp__* tools are present — the default (slightly
richer for reads: structured tree + content_info); (2) else pup if installed and pup auth targets
the app's org; (3) else the user has neither → guide the pup install (it's easier to set up than the
MCP, so recommend pup here):
brew tap datadog-labs/pack && brew install datadog-labs/pack/pup
pup auth login(MCP alternative: claude mcp add --scope user --transport http "datadog-llmo-mcp" "https://mcp.datadoghq.com/v1/mcp?toolsets=llmobs"; see https://docs.datadoghq.com/bits_ai/mcp_server/setup/.) Don't proceed without a backend.
The backend↔operation mapping and pup's exact flags/gotchas are in details.md — read that section before
using pup. Two pup musts: (1) results come back at data.spans[] or top-level spans[]
(varies by version/--no-agent) — parse whichever is present, or you get zero hits on an ingested
trace (a silent false negative, step 7); (2) check token expiry (pup auth status), not just that auth
exists — expiry mid-loop looks like "trace not found."
1. Parse the command
<trace-id> + optional free-text modification (everything after the id); none → diff-only mode. Determine
the ml_app from the project (LLMObs.enable(ml_app=…) / DD_LLMOBS_ML_APP) or the trace; confirm if
ambiguous.
2. Fetch the trace + locate the baseline
Fetch via the backend; note total_duration_ms (drives step 7), the trace_url, and
metadata.replay_input/replay_entrypoint if present. Locate the baseline field — it's not always the
root output: the value the developer dislikes may be a tool-call input or an intermediate output several
levels deep, and the app may post-process it before the span records it. Pick the field the code change can
actually move, or the delta drowns in noise.
2.5. Check for fan-out
If the root span fans out into repeated sibling subtrees (a batch/map over N parallel sub-runs), the change under test is usually visible in a single branch — replaying the whole root costs ~N× spend and time for no extra signal. Offer to replay one representative branch; log what you skipped. Pick deliberately: the cheapest branch that reached the terminal / side-effecting tool (most branches are no-ops that prove nothing), and reconstruct its input from the child span's input, not the root's. Full root only if the change is inherently cross-branch.
3. Resolve the entrypoint + input
Entrypoint:
metadata.replay_entrypointif present; else infer from the root span (name/kind) + code and confirm with the user.Input:
metadata.replay_inputif present; else derive a suggested input (prefer the code signature — the rendered prompt is lossy) and have the user confirm/edit.
3.5. Ensure a local run path (find the innermost callable seam)
Ask "what is the innermost callable seam for this root span, and can I call it directly with JSON?" — not
"is the app runnable?". Already directly callable → skip, continue. Buried under a handler/service
(deployed-only, no local __main__, live-infra coupling) → follow references/local-setup.md (detect →
propose → approve → build). The common middle case — a deployed service whose core logic is already a plain
callable (ports-and-adapters) — just extract/call that seam; full local-setup is overkill.
4. 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
replay_input(those are the ones you most want to replay):LLMObs.annotate(span=span, metadata={"replay_entrypoint": "<stable id>", "replay_input": <extractor>})No
replay_output— the original trace is the baseline. (Non-Python annotate APIs differ — e.g. Gospan.Annotate(llmobs.WithAnnotatedMetadata(...)); seedetails.md.)Isolation pre-flight (before writing the runner): grep the entrypoint's call path for per-span/ per-call ml_app overrides (Go
llmobs.WithMLApp; Pythonml_app=on a decorator or inLLMObs.annotate). Those beat the init-level-local, 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.b) The runner — satisfies the language-independent runner contract in
details.md(load env → derive<ml_app>-local→ dispatch one entrypoint on JSON → flush on every exit path incl. errors → refuse to start unless ml_app ends in-local→ print the-localml_app). Python: copyscripts/replay_runner_template.pyand fillENTRYPOINTS. Other languages: write to the contract — don't assume the Python API carries over (Go APIs + export-mode gotchas indetails.md), 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 the-localoverride. Infer + confirm the run command; follow the host repo's build-file conventions (Bazel/Gazelle →cmd/<name>/, run Gazelle, build before replay).
5. (If a change was requested) edit, then gate
Make the code changes, show the developer the diff of your changes, then an AskUserQuestion selector:
Replay now / Adjust the changes first / Cancel. Only replay on "Replay now".
6. Replay
Before the first replay: warn (re-running is real — model spend + real writes), and sanitize the
environment. The coding agent's own env (ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL set by Claude
Code, and other provider keys) can make the app's SDK bypass its configured model gateway — a fidelity
gap invisible in the diff. Unset ambient provider vars by default and report that you did (don't
just ask); grep the app for its own ambient-key guards. Also verify the credential's org matches the
trace's org — a mismatch ships the replay somewhere you can't query (looks like ingest lag).
On confirmation, record t0 and run — source the project's env file, never inline secrets (the marker
tag is fine on the command; DD_API_KEY=<value> inline is blocked by the permission classifier and leaks to
history/transcript — use the wrapper/env-file):
DD_TAGS=replay_run_id:<unique-id> <run cmd or wrapper> --entrypoint <id> --input-file <path>The runner emits under <ml_app>-local (idempotent, so replays never pollute production) and prints that
name — poll for the new trace under it.
7. 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
replay_run_idtag under<ml_app>-local(pup:--query "replay_run_id:<id>", plainkey:value). Before ever reporting "not found," re-query with no tag filter (just<ml_app>-local+ 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.Verify isolation on each hit — a tag match is NOT proof.
--query/tag matching can return a span whose realml_appis a different app (the--ml-appfilter gets ignored). Readml_appoff every returned span and assert it ends in-localbefore 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.
8. Diff (with links to both traces)
Concise summary of how the new output differs from the old — meaningful differences only. Note live-world drift; and because any nondeterministic agent varies run-to-run, default to two replays (diff-only mode too, not just model-facing edits) and use replay-to-replay comparison — if the two local runs differ from each other about as much as from production, the delta is sampling variance, not your change. If the replay disables a side-effecting integration (dry-run), that integration's subtree is absent — exclude it from both sides before comparing span counts, or the structural diff is junk. Lead the diff with both trace links:
Old:
trace_urlverbatim — but under fan-out (you replayed one branch) link the branch span, not the whole-root url.New (replay): must carry
ml_app=<ml_app>-localor it opens empty — and thetrace_urlis an org-switch wrapper (…/switch_to_user/<id>?next=<encoded /llm/traces …>&flow=org_switch), so injectml_app=<ml_app>-localinto the decodednextquery and re-encode; do NOT append to the outer URL (mechanics indetails.md). Browser-unverifiable from here — confirm once it opens non-empty.
9. Gate — iterate, or stop on a broken harness
Harness-failure gate (before the diff): if a replay reveals the harness is wrong — trace landed under
the wrong ml_app, no trace after the step-7 sanity checks, missing flush, or auth/org misrouted — do NOT
proceed to a diff on bad data. Stop and present a selector to fix the harness (re-scope ml_app / add flush
/ fix env) and re-replay.
Otherwise, after the diff, an AskUserQuestion selector: Looks good — stop here (finish; leave the edits
in the working tree) / Make more changes (free-text inline → back to step 5). Re-present after every
replay; end only on "stop here".
Reference
references/details.md— 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/local-setup.md— making a deployed-only app locally runnable (step 3.5). Read when that gap shows.scripts/replay_runner_template.py— the Python runner to copy + fill.
Files
- ATTRIBUTION.mdAdded by AutoGineer920 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md14.6 KiB
- references/details.md21.2 KiB
- references/local-setup.md5.6 KiB
- scripts/replay_runner_template.pyScript5.0 KiB
references/local-setup.md
Setting up a local testing flow (step 3.5)
Read this only when the app can't be invoked locally with a JSON input — a deployed-only service, an
HTTP/gRPC handler entrypoint, no local __main__/CLI, deps not installed, or logic coupled to live infra.
Replay re-runs the entrypoint locally, so without a local run path there's nothing to replay. This procedure
makes the app locally runnable, then hands back to the normal instrument + runner steps.
Shape: detect → propose (one approval) → build automatically. You do as much as possible on your own, but two things are mandatory handbacks to the user; see below.
1. Detect the gap
The real question isn't the binary "is the app runnable?" — it's what is the innermost callable seam corresponding to the trace's root span, and can it be called directly with a JSON input? A deployed-only HTTP/gRPC service frequently still exposes a plain callable underneath its handler (the ports-and-adapters / hexagonal case); when it does, extract/call that seam — full local-setup would be overkill. Only run this procedure when no seam is directly callable.
Signals the seam isn't directly callable as-is:
The entrypoint is a request handler (FastAPI/Flask/gRPC/Lambda) with no plain callable beneath it.
No
if __name__ == "__main__"/ CLI, or no way to import + call the core logic directly.Config/secrets read from a deployed environment (env injected by the platform, a secrets manager).
Imports of cloud/service clients constructed at service startup (DB, queues, downstream services).
No local virtualenv / dependencies not installed.
If the seam is already an importable function you can call with JSON, skip this whole procedure and return to step 4. If the logic is a plain callable buried inside a handler, extracting that seam is a small edit (see §2) — still lighter than treating the whole app as non-runnable.
2. Analyze + propose (get ONE approval before touching anything)
First, look for an existing dry-run affordance before proposing any stubs. Many apps already ship a
dry-run / no-op / sandbox mode or a nil-adapter branch built for exactly this situation. Using the app's own
affordance is safer and higher fidelity than a mock — it exercises the real code path right up to the
side effect, and it's code the owners already trust. Check for it (flags, env vars, a --dry-run, a
nil/fake adapter) and prefer it. Only fall back to a stub — a fake/no-op stand-in for a dependency
that either isn't reachable locally or whose real call would re-trigger a side-effecting write — where no
such affordance exists; the per-dependency stub-vs-real decision is §4's second handback.
Read the code around the entrypoint and produce a concrete, reviewable plan covering:
The local entry point you'll scaffold — a thin module that invokes the core agent logic with a JSON input (extracting a callable seam if the logic is buried inside a handler).
Any existing dry-run / no-op / sandbox mode you found, and where you'll use it instead of a stub.
Deps + venv to create/install, and the run command.
Env/config to load locally (e.g. a
.env), and tracing enabled under<ml_app>-local(LLMObs.enable(ml_app="<ml_app>-local")) — local test traces must not pollute the production ml_app.Per external dependency, your proposed handling: stub / no-op, point at a local or test instance, or use real read-only creds — one line each.
Any structural edits to the app (e.g. extracting the core function out of a handler), called out explicitly.
Present this as a single plan and get the user's approval (accept / adjust / "just scaffold, I'll do X"). This is their one control point — don't build before it.
3. Build (automatic after approval)
Do everything you can without further prompting:
Scaffold the local entry point that calls the core logic with the JSON input.
Extract the callable seam if needed (apply the structural edits you flagged).
Create the venv, install deps, write the local
.envskeleton, enableLLMObsunder<ml_app>-local.Wire external deps per the approved plan (stubs / local instances).
Then hand off to step 4 (instrument + runner) as normal — the runner's entrypoint becomes this local entry point.
4. Two mandatory handbacks
These you cannot (and must not silently) do for the user:
Secrets. You can't fabricate API keys, DB credentials, or service tokens. Scaffold the
.envwith the required keys empty and ask the user to fill them; never invent or guess values.Stub-vs-real per external dependency. Never silently auto-mock — a wrong mock makes the replay's output diverge for reasons unrelated to the code change, so the diff becomes misleading. Surface each external dependency and let the user choose stub / local / real (prefer the app's own dry-run / no-op affordance from §2 over a hand-written stub — it's higher fidelity). Only then implement it.
Everything outside these two is automatic.
5. Safety
Default external writes to stubbed / no-op unless the user explicitly opts into real dependencies — replaying against real infra re-triggers real writes (DB/email/billing/queues) on every iteration of the loop.
Warn before structural refactors. Extracting a seam edits the app's real code; show the change and keep it minimal and non-destructive.
Don't make it run "at any cost" — hardcoding config, disabling auth, or skipping validation just to get a trace out produces a local flow that runs but doesn't represent production. Flag such shortcuts instead of taking them silently.
Requirements and compatibility
Written for: Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Names Claude Code tools, Needs an MCP server, Needs an API key, Needs a command-line tool, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Datadog
- Publisher
- Datadog (Official)
- Source
- datadog-labs/agent-skills · agent-observability/agent-observability-replay-trace · commit 8e1306a
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Datadog.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download agent-observability-replay-trace.zip