Generated banner for Agent Observability Replay Trace
Generated logo for Agent Observability Replay Trace

Agent Observability Replay Trace

DatadogOfficialPublished by Datadog

  • Skill
  • Workflow
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

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,
— the description by Datadog

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/datadog-labs/agent-observability-replay-trace/download
  2. Unzip 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 (an ml_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-llmo MCP (used when present) or the pup CLI (fallback, and the easier install if you have neither) (step 0).

  • Credentials: DD_API_KEY + DD_SITE + provider key(s). Not DD_APP_KEY — plain trace, not an Experiment (that's 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 <ml_app>-local isolation (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_entrypoint if present; else infer from the root span (name/kind) + code and confirm with the user.

  • Input: metadata.replay_input if 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. Go span.Annotate(llmobs.WithAnnotatedMetadata(...)); see details.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; Python ml_app= on a decorator or in LLMObs.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 -local wins.

  • 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 -local ml_app). Python: copy scripts/replay_runner_template.py and fill ENTRYPOINTS. Other languages: write to the contract — don't assume the Python API carries over (Go APIs + export-mode gotchas in details.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 -local override. 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_id tag under <ml_app>-local (pup: --query "replay_run_id:<id>", plain key: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 real ml_app is a different app (the --ml-app filter gets ignored). Read ml_app off every returned span and assert it ends in -local before 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.

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_url verbatim — 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>-local or it opens empty — and the trace_url is an org-switch wrapper (…/switch_to_user/<id>?next=<encoded /llm/traces …>&flow=org_switch), so inject ml_app=<ml_app>-local into the decoded next query and re-encode; do NOT append to the outer URL (mechanics in details.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

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 .env skeleton, enable LLMObs under <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 .env with 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