Setting the file. One moment.
Replay Runner Template · Agent Observability Replay Trace · datadog-labs/agent-skills · Skills Docs
ContentsBack to the top of the page scripts/replay_runner_template.py
scripts/ replay_runner_template.py
Python · 94 lines · 5 KB
14 # (e.g. OPENAI_API_KEY). NOT DD_APP_KEY — this replays a plain trace, not an Experiment.
15 import argparse
16 import asyncio
17 import json
18 import os
19 import sys
20 from dotenv import load_dotenv
21 # override=True makes the project's .env authoritative: a developer shell commonly exports ambient DD_*
22 # vars (e.g. for the datadog-llmo MCP, often a different org) that would otherwise win over .env and send
23 # the trace to the wrong org. MUST precede the ddtrace import. (It won't clobber the caller's DD_TAGS
24 # unless .env also sets DD_TAGS.)
25 load_dotenv( override = True )
26
27 from ddtrace.llmobs import LLMObs
28
29 # Import each instrumented entrypoint:
30 from {{ MODULE }} import {{ ENTRYPOINT_FN }} # , {{ANOTHER_ENTRYPOINT_FN}}, ...
31
32 ML_APP = os.environ.get( "DD_LLMOBS_ML_APP" , " {{ ML_APP }} " )
33 # Local replays emit under "<ml_app>-local" (idempotent) so they never pollute the production ml_app.
34 if not ML_APP .endswith( "-local" ):
35 ML_APP = ML_APP + "-local"
36 # Interlock: refuse to emit under a non-isolated ml_app. NOTE this only guards the init-level setting — if
37 # the app sets ml_app per span/call it overrides this (see the pre-flight in references/details.md).
38 if not ML_APP .endswith( "-local" ):
39 raise SystemExit ( f "[replay] refusing to run: ml_app { ML_APP !r} is not isolated (must end in -local)" )
40
41 # Export mode: agentless is the right default for a LOCAL replay (ships LLM Obs spans straight to Datadog,
42 # no Agent needed). If this app is instead wired to a local Agent sidecar, set DD_LLMOBS_AGENTLESS_ENABLED=0.
43 # Benign gotcha: with agentless on, the APM tracer may still dial localhost:8126 and log
44 # "ERROR: lost N traces ... connection refused". That is HARMLESS — the LLM Obs spans ship independently and
45 # arrive fine — so don't mistake it for a failed replay.
46 _agentless = os.environ.get( "DD_LLMOBS_AGENTLESS_ENABLED" , "1" ).lower() not in ( "0" , "false" , "no" )
47 LLMObs.enable( ml_app = ML_APP , agentless_enabled = _agentless)
48
49
50 # Dispatch table — ONE entry per execution type, keyed by the replay_entrypoint id the app annotates.
51 # Each entry: the function to call + whether it's async. The runner calls fn(**input_data); the trace it
52 # emits is what the skill diffs, so no return-value extraction is needed here.
53 ENTRYPOINTS = {
54 # "{{REPLAY_ENTRYPOINT_ID}}": {"fn": {{ENTRYPOINT_FN}}, "is_async": True},
55 }
56
57
58 def _run_entrypoint (spec, input_data):
59 fn, is_async = spec[ "fn" ], spec.get( "is_async" , False )
60 return asyncio.run(fn( ** input_data)) if is_async else fn( ** input_data)
61
62
63 def main ():
64 ap = argparse.ArgumentParser()
65 ap.add_argument( "--entrypoint" , required = True , help = "replay_entrypoint id (key in ENTRYPOINTS)" )
66 ap.add_argument( "--input-file" , required = True , help = "path to a JSON file of entrypoint kwargs" )
67 args = ap.parse_args()
68
69 if args.entrypoint not in ENTRYPOINTS :
70 print (json.dumps({ "error" : f "unknown entrypoint { args.entrypoint !r} ; known: { sorted ( ENTRYPOINTS ) } " }))
71 sys.exit( 2 )
72 with open (args.input_file) as f:
73 input_data = json.load(f)
74
75 # Run the entrypoint DIRECTLY — no wrapper span — so the replay trace is structurally identical to a
76 # normal run. The correlation marker rides along as a span tag via DD_TAGS (set by the caller).
77 # Flush in `finally` so a FAILED replay still emits its partial trace — otherwise the error path leaves
78 # nothing to diff, which is worse than a visible failure.
79 status, error = "done" , None
80 try :
81 _run_entrypoint( ENTRYPOINTS [args.entrypoint], input_data)
82 except Exception as exc: # noqa: BLE001 — normal failures → report + exit 1
83 status, error = "error" , repr (exc)
84 finally :
85 LLMObs.flush() # send the trace (even a partial one) before we exit
86 # Print in `finally` so the caller always gets ml_app to poll — even if the entrypoint raised
87 # SystemExit/KeyboardInterrupt (which skip `except Exception` but still run `finally`, then propagate).
88 print (json.dumps({ "status" : status, "entrypoint" : args.entrypoint, "ml_app" : ML_APP , "error" : error}))
89 if status == "error" :
90 sys.exit( 1 )
91
92
93 if __name__ == "__main__" :
94 main()