Rejouer une trace par rapport au code local
Une boucle d'itération rapide sur une unique trace de production : prenez une trace dont la sortie n'a pas plu à un développeur, modifiez optionnellement le code pour la corriger, rejouez cette trace par rapport à leur code LOCAL, et montrez un diff concis de l'ancienne vs la nouvelle sortie — en itérant jusqu'à satisfaction. Aucune hypothèse sur la disposition du projet.
Invoqué depuis l'agent de codage du développeur (il colle un CTA depuis l'UI Agent Observability) :
/agent-observability-replay-trace <trace-id> [<changes to test>].
La boucle (ce que vous construisez à chaque exécution)
- Récupérez la trace et lisez sa sortie (la baseline).
- Si une modification a été demandée, éditez le code local pour l'adresser — montrez les changements et obtenez un OK avant de rejouer.
- Rejouer : réexécutez le point d'entrée localement pour qu'il émette une nouvelle trace.
- Attendez la nouvelle trace, puis montrez un diff concis de l'ancienne vs la nouvelle sortie.
- Satisfait → terminé. Non satisfait → le développeur dit ce qui ne va toujours pas → retour à l'étape 2. Itérez.
Sans aucune modification (/agent-observability-replay-trace <trace-id>) : faites uniquement le replay + diff (une vérification de reproduction/régression), puis proposez d'entrer dans la boucle d'édition.
Modèle d'interaction — sélecteurs de contrôle, jamais d'arrêt brutal
C'est une boucle active. À chaque point de décision, présentez les choix comme un sélecteur interactif (l'outil AskUserQuestion — le même style de menu qu'en mode plan), pas une question simple qui termine votre tour.
Il y a deux contrôles : (a) après que vous proposiez des changements de code, avant de rejouer ; et (b) après chaque vue de diff. Continuez à présenter le sélecteur après chaque replay jusqu'à ce que l'utilisateur choisisse explicitement de terminer — n'arrêtez pas en cours de boucle. Terminez uniquement quand il choisit « Looks good — stop here ».
Le sélecteur offre toujours une option en texte libre, donc quand un choix a besoin de détails (ce à affiner, ce à ajuster), l'utilisateur tape directement dans le sélecteur — vous recevez sa description dans la même vue. Traitez ce texte libre comme l'instruction et agissez directement ; ne posez pas de question séparée de suivi.
Périmètre — vérifiez ceci d'abord
- Tracé avec
ddtrace/ LLM Obs, avec uneml_appet un point d'entrée découvrable. Python est de première classe ; les autres langages fonctionnent en principe (la boucle est agnostique du langage) mais vous devez apprendre les commandes de build/run du langage et générer le runner dedans. - Entrée du point d'entrée sérialisable en JSON. Si le point d'entrée nécessite une infrastructure live non sérialisable reconstruite à la rejeu (clients DB/API, un objet
deps/context), le runner ne peut pas la fabriquer — demandez à l'utilisateur comment, ou déclarez ce point d'entrée hors de portée. - Nécessite l'MCP
datadog-llmo(étape 0). - Identifiants :
DD_API_KEY+DD_SITE+ la/les clé(s) du fournisseur de l'agent. PasDD_APP_KEY— cela rejoue dans une trace simple, pas une Expérience. - Effets secondaires : rejouer réexécute le code réel (dépense réelle de modèle + toute écriture réelle que l'agent effectue). Voir étape 6 — avertir avant le premier replay.
Pourquoi trace uniquement (pas une Expérience)
C'est délibérément pas le chemin Expériences (c'est agent-observability-replay-experiment). Réexécuter l'app émène juste une nouvelle trace normale ; la comparaison est un diff LLM des sorties des deux traces. Cela garde le tout léger, supprime l'exigence DD_APP_KEY, et n'est pas limité au SDK Expériences de Python. Détails dans references/details.md — lisez-le avant de générer le runner.
Workflow
0. Assurez-vous que l'MCP datadog-llmo est disponible
La découverte + le diffing lisent les traces via cet MCP. Vérifiez la présence de mcp__datadog-llmo-mcp__* (p. ex. get_llmobs_trace, search_llmobs_spans). S'il est absent, arrêtez et guidez l'utilisateur à travers l'installation (https://docs.datadoghq.com/bits_ai/mcp_server/setup/) et reprenez une fois que les outils apparaissent.
1. Parsez la commande
<trace-id> (obligatoire) et une modification en texte libre optionnelle (tout après l'id). Pas de modification → mode reproduction/diff uniquement. Déterminez la ml_app depuis le projet (LLMObs.enable(ml_app=…) / DD_LLMOBS_ML_APP) ou la trace ; confirmez si ambigu.
2. Récupérez la trace
get_llmobs_trace (et contenu des spans au besoin). Lisez la sortie du span racine — c'est la baseline du diff — et son metadata.replay_input / metadata.replay_entrypoint s'il est présent.
3. Résolvez le point d'entrée + l'entrée
- Point d'entrée : si
metadata.replay_entrypointest présent, utilisez-le comme dispatch id. S'il est absent, déduisez le point d'entrée du span racine (nom/kind) + code et demandez à l'utilisateur de confirmer avant de procéder. - Entrée : si
metadata.replay_inputest présent, utilisez-le. S'il est absent, dérivez une entrée suggérée de la trace (meilleur effort — le prompt rendu est incomplet, donc préférez la signature du code) et faites confirmer ou éditer par l'utilisateur.
4. Assurez-vous des deux artefacts persistants (configuration ponctuelle, réutilisée à chaque itération)
- a) Annotation dans le point d'entrée — pour que les traces futures s'auto-décrivent. Si le point d'entrée n'annote pas déjà son span racine, ajoutez-le (meilleur effort, non destructif) :
LLMObs.annotate(span=span, metadata={ "replay_entrypoint": "<stable id for this type>", "replay_input": <input extractor>, # e.g. {"tickers": tickers} })(Pas de
replay_output— la trace originale est la baseline ; le diff lit les sorties des traces.) - b) Le runner — copiez
scripts/replay_runner_template.py→replay_runner.pyet remplissez sa table de dispatchENTRYPOINTS(une entrée par type, indexée parreplay_entrypoint→ sa fonction + sync/async). Étendez la table quand de nouveaux points d'entrée apparaissent ; gardez le fichier. Déduisez la commande de run (venv/interpréteur/build) du projet et confirmez-la avec l'utilisateur. Si le point d'entrée nécessite une infrastructure live non sérialisable, demandez comment la construire ou sautez-le.
5. (Si une modification a été demandée) éditez le code, puis contrôlez avec un sélecteur
Analysez la trace + la demande, faites les changements de code, montrez au développeur le diff de vos changements, puis présentez un sélecteur AskUserQuestion (pas une question simple) — p. ex. :
- Replay now — procédez à l'étape 6.
- Adjust the changes first — l'utilisateur dit ce à ajuster ; réditez et représentez ce contrôle.
- Cancel — arrêtez sans rejouer. Rejouez uniquement sur le choix « Replay now ».
6. Rejouer
Avant le premier replay, avertissez : réexécuter lance l'agent pour de vrai — les appels modèle coûtent des tokens et toute écriture externe (DB/email/facturation/queues) se reproduit. À la confirmation, enregistrez le temps de lancement t0, puis invoquez le runner avec l'id du point d'entrée + l'entrée (fichier JSON), en passant un marqueur de corrélation unique comme tag de span via l'environnement :
DD_TAGS=replay_run_id:<unique-id> <python> replay_runner.py --entrypoint <id> --input-file <path>
Le runner exécute le point d'entrée directement — aucun span wrapper — so la trace de rejeu ressemble à une exécution normale, et le marqueur s'ajoute comme tag aux spans émis.
7. Attendez la nouvelle trace
Deux attentes, basées sur la durée de la trace originale (total_duration_ms, lue à l'étape 2) :
- Exécution du runner : donnez au subprocess du runner un timeout de
max(120s, ~3 × total_duration_ms)— la rejeu exécute le même code, donc elle prend grosso modo la durée originale ; 3× attrape une exécution gelée/bloquée sans trébucher sur une normale. - Ingestion : une fois que le runner revient, dites à l'utilisateur « waiting for the new trace to appear in Datadog… » et sondez l'MCP tous les ~5s jusqu'à ~2 min :
search_llmobs_spanspour le tagreplay_run_id(depuis ≈t0). Si ce tag n'est pas interrogeable, basculez vers le span racine le plus récent pour cetteml_app+ point d'entrée créé aprèst0. Le lag d'ingestion est de secondes à ~2 min et n'évolue pas avec la durée. Ne faillez pas brutalement sur timeout : dites qu'il n'est pas apparu et proposez de continuer à attendre.
8. Résumez le diff (avec liens vers les deux traces)
Récupérez la nouvelle trace et donnez un résumé concis de comment la nouvelle sortie diffère de l'ancienne — juste les différences de sortie significatives, pas les arbres de spans complets. Notez que la dérive du monde vivant (temps, prix, résultats de recherche) peut différer même avec un code inchangé.
Chaque vue de diff doit commencer par des liens cliquables vers LES DEUX traces pour que le développeur puisse ouvrir l'une ou l'autre dans l'UI. Utilisez l'trace_url que l'MCP retourne pour chaque trace (depuis get_llmobs_trace) textuellement — ne construisez PAS l'URL à la main (la requête correcte est ?query=trace_id:<id>, pas @trace_id: ou la convention APM ?traceID=, donc la construire soi-même la rend fausse) :
- [Old trace](<old trace_url from get_llmobs_trace>)
- [New trace](<new trace_url from get_llmobs_trace>)
Le texte du lien est juste « Old trace » / « New trace ». Puis le résumé du diff.
9. Itérez — contrôle avec un sélecteur (jamais un arrêt brutal)
Après le diff, présentez un sélecteur AskUserQuestion avec deux options (l'outil offre aussi un « Other » en texte libre) :
- Looks good — stop here — terminez ; laissez les changements de code dans l'arbre de travail pour que l'utilisateur les examine.
- Make more changes — l'utilisateur décrit ce à changer directement dans le sélecteur (texte libre) ; utilisez cette description et allez à l'étape 5 (éditer → contrôler → rejouer → differ). En mode diff uniquement, c'est où le premier changement est fait. Représentez ce contrôle après chaque replay jusqu'à ce que l'utilisateur choisisse « stop here ». N'arrêtez pas votre tour entre les itérations.
Référence
scripts/replay_runner_template.py— le runner à copier + remplir. Lisez-le d'abord.references/details.md— le contrat annotation + runner, correlation-marker/polling, guidance concise-diff, et scope/limitations. Lisez avant de générer le runner.