Installer, enregistrer, rejouer.
Tout ce qu’il faut pour faire tourner Trajekt à côté de vos agents, lui envoyer des traces, lire ce qu’ils ont fait et être prévenu quand une nouvelle version se dégrade.
Vue d’ensemble
Trajekt a deux parties :
- Le serveur (
apps/dashboard) : une app Next.js avec un Postgres. Il reçoit les traces surPOST /v1/traces, stocke une ligne par run et sert le dashboard. - La librairie (
@trajekt-ai/core) : lit une trace dans n’importe quel format supporté vers un modèle unique, calcule ses métriques et compare des groupes de runs. Le serveur l’utilise ; vous pouvez l’utiliser dans vos propres tests.
Votre agent n’a pas besoin d’un SDK Trajekt. Il envoie des spans OpenTelemetry standard, ou vous postez un fichier de trace.
Démarrage rapide
Il suffit de Docker et de Git.
git clone https://github.com/LeoLR/trajekt.git cd trajekt cp .env.example .env # renseigner DASHBOARD_PASSWORD et DASHBOARD_SESSION_SECRET docker compose up -d --build
docker compose exec dashboard pnpm create-project mon-projet
docker compose exec dashboard pnpm seed
Ouvrez http://localhost:3000 et connectez-vous avec DASHBOARD_PASSWORD. Avec les données de démo, vous verrez un agent, sentry-triage, dont la version 1.5.0 se met à boucler sur un appel d’outil, coûte plus cher et échoue plus souvent que la 1.4.0 — c’est à ça que ressemble une régression dans Trajekt.
Ensuite, envoyez vos propres traces.
Installation
Docker Compose
Le fichier compose lance deux conteneurs : Postgres 16 et le dashboard. Les migrations s’exécutent au démarrage du conteneur du dashboard. Le dashboard et le point d’ingestion partagent le port 3000.
docker compose up -d --build # démarrer docker compose logs -f dashboard # suivre les logs docker compose down # arrêter, garder les données docker compose down -v # arrêter et supprimer le volume de la base
.env, le mot de passe vaut trajekt et le secret de session une valeur de développement. Définissez les deux avant d’exposer le port au-delà de votre machine. Le fichier compose ne fournit ni TLS ni reverse proxy.Depuis les sources
Il faut Node.js 22, pnpm 10 et une base Postgres.
pnpm install pnpm --filter @trajekt-ai/core build # le dashboard importe le paquet compilé cd apps/dashboard cp .env.example .env # DATABASE_URL, DASHBOARD_PASSWORD, DASHBOARD_SESSION_SECRET pnpm migrate pnpm create-project mon-projet pnpm dev # http://localhost:3000
Configuration
| Variable | Requise | Rôle |
|---|---|---|
DATABASE_URL | oui | Chaîne de connexion Postgres. Définie par le fichier compose. |
DASHBOARD_PASSWORD | oui | Le mot de passe de la page de connexion. Il y a un seul compte. |
DASHBOARD_SESSION_SECRET | oui | Signe le cookie de session. 16 caractères minimum. |
CRON_SECRET | pour les alertes | Jeton Bearer de POST /api/alerts/check. Le point d’accès est désactivé tant qu’il est vide. |
SLACK_WEBHOOK_URL | non | Webhook entrant Slack. Les nouvelles régressions y sont postées. |
TRAJEKT_PUBLIC_URL | non | URL publique du dashboard, pour les liens des messages Slack. |
TRAJEKT_PRICING | non | Prix personnalisés en JSON, en USD par million de tokens. Voir l’estimation des coûts. |
TRAJEKT_REDACT | non | Ce qu’il faut masquer avant stockage : secrets (défaut), email, iban, card, phone, ip, séparés par des virgules, ou none. Voir Données sensibles. |
TRAJEKT_REDACT_PATTERNS | non | Vos propres motifs, en JSON : [{"name":"customer","pattern":"CUST-\\d{6}"}]. |
TRAJEKT_RETENTION_DAYS | non | Les runs plus anciens sont supprimés à chaque passage du cron. Vide : conservés indéfiniment. |
TRAJEKT_INGEST_RATE_PER_MIN | non | Requêtes d’ingestion par minute et par projet, par instance du serveur (600 par défaut). Au-delà : 429 avec Retry-After. |
TRAJEKT_MAX_BODY_MB | non | Taille maximale d’une requête, après gzip (20 par défaut). Au-delà : 413. |
TRAJEKT_STORE_RAW | non | false : ne pas conserver le payload d’origine de chaque step — environ deux fois moins de stockage, mais le panneau « raw payload » d’un step est vide. |
Envoyer des traces
Chaque requête vers POST /v1/traces doit porter la clé d’API du projet : Authorization: Bearer tk_live_…. Seul son hash est stocké : une clé perdue ne se récupère pas, il faut créer un nouveau projet.
OpenTelemetry
Pointez n’importe quel exporteur OTLP/HTTP vers l’URL racine de Trajekt ; l’exporteur ajoute lui-même /v1/traces. Les encodages JSON et protobuf fonctionnent tous les deux : les réglages par défaut des SDK Python, Java ou Go conviennent.
OTEL_SERVICE_NAME=sentry-triage OTEL_RESOURCE_ATTRIBUTES=service.version=3.2.0,trajekt.agent.version=1.5.0,trajekt.prompt.version=v8 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:3000 OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # ou http/json OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer tk_live_..."
Ou dans le code, avec le SDK Node :
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
new NodeSDK({
serviceName: 'sentry-triage',
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:3000/v1/traces',
headers: { Authorization: `Bearer ${process.env.TRAJEKT_API_KEY}` },
}),
}).start();Trajekt lit les spans qui suivent les conventions sémantiques GenAI d’OpenTelemetry : gen_ai.operation.name, gen_ai.request.model, gen_ai.usage.input_tokens / output_tokens, gen_ai.tool.name, gen_ai.tool.call.arguments / result, gen_ai.input.messages / output.messages. Il lit aussi les spans OpenInference et OpenLLMetry (voir plus bas). Un span est en échec si son statut est ERROR ou s’il enregistre une exception.
Un exporteur envoie une trace en plusieurs lots, les enfants d’abord. Trajekt fusionne tous les lots d’un même traceId en un seul run ; renvoyer un lot ne duplique pas les steps.
/v1/traces).Instrumenter votre code
Si votre agent est écrit à la main, @trajekt-ai/core fournit deux helpers qui créent les spans attendus par Trajekt, avec n’importe quel tracer OpenTelemetry. Les arguments des outils comptent : sans eux, les appels répétés ne peuvent pas être détectés.
import { trace } from '@opentelemetry/api';
import { traceRun, traceTool } from '@trajekt-ai/core';
const tracer = trace.getTracer('sentry-triage');
await traceRun(tracer, { agent: 'sentry-triage', agentVersion: '1.5.0', promptVersion: 'v8' }, async (run) => {
const issue = await traceTool(tracer, 'sentry.getIssue', { id }, () => sentry.getIssue(id));
const ticket = await traceTool(tracer, 'jira.createIssue', { project, summary }, () => jira.createIssue(project, summary));
run.score('correct', ticket.project === expectedProject); // score de qualité, optionnel
return ticket;
});Appels au modèle : enveloppez le client Anthropic une fois et chaque messages.create est enregistré — modèle, tokens y compris le cache de prompt (tarifé comme tel), raison d’arrêt, et les messages sauf avec { captureContent: false }. Autres SDK : traceLlm(tracer, { model, input }, () => call()) lit les réponses Anthropic et OpenAI.
import Anthropic from '@anthropic-ai/sdk';
import { traceAnthropic } from '@trajekt-ai/core';
const anthropic = traceAnthropic(new Anthropic(), tracer);
// le code de l'agent ne change pas :
const msg = await anthropic.messages.create({ model: 'claude-sonnet-5-5', max_tokens: 1024, system, messages, tools });traceRun accepte aussi appVersion, environment et scenario (pour les runs d’évaluation), et expose run.traceId pour attacher un score plus tard. Une exception qui sort du run le marque en échec ; une exception levée par un outil ne marque que ce step.
Vercel AI SDK
Avec un exporteur OpenTelemetry configuré comme ci-dessus, activez la télémétrie à chaque appel. functionId devient le nom de l’agent.
const result = await generateText({
model,
prompt,
tools,
experimental_telemetry: {
isEnabled: true,
functionId: 'trip-planner',
metadata: { appVersion: '3.2.0', agentVersion: '2.3.0', promptVersion: 'v8' },
},
});Sans OpenTelemetry, vous pouvez poster l’objet résultat lui-même (la valeur qui contient steps) sur /v1/traces. Il ne porte ni horodatage ni identifiant de trace : chaque envoi est un run distinct, et le nom de l’agent doit venir d’un en-tête.
LangChain, LlamaIndex, CrewAI…
Les frameworks Python sont généralement tracés avec OpenInference ou OpenLLMetry. Trajekt lit les deux : modèle, tokens, coût et messages des spans LLM ; nom, arguments et résultat des spans d’outils ; le nom de l’agent depuis le span AGENT ou le nom du workflow. Pointez leur exporteur OTLP vers Trajekt :
from openinference.instrumentation.langchain import LangChainInstrumentor
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="http://localhost:3000/v1/traces",
headers={"Authorization": "Bearer tk_live_..."},
)))
trace.set_tracer_provider(provider)
LangChainInstrumentor().instrument()Définissez les versions avec des attributs de ressource (OTEL_RESOURCE_ATTRIBUTES=service.version=3.2.0,trajekt.agent.version=1.5.0,trajekt.prompt.version=v8) pour pouvoir les comparer.
Exports Langfuse et fichiers de trace
Déjà sur Langfuse ? Importez ses traces, scores compris, sans toucher à votre agent. Lancez l’import à intervalle régulier : il n’ajoute que ce qui est nouveau et ne duplique jamais un run.
LANGFUSE_HOST=https://cloud.langfuse.com \ LANGFUSE_PUBLIC_KEY=pk-lf-... LANGFUSE_SECRET_KEY=sk-lf-... \ pnpm --filter dashboard import-langfuse --since 7d --name sentry-triage
Les versions viennent de la trace : release devient la version de l’app, version la version de l’agent, metadata.promptVersion la version du prompt. Une trace Langfuse seule — la réponse de GET /api/public/traces/:traceId — peut aussi être postée telle quelle, tout comme un fichier OTLP/JSON.
curl -X POST http://localhost:3000/v1/traces \ -H "Authorization: Bearer tk_live_..." \ -H "Content-Type: application/json" \ -H "X-Trajekt-Agent: support-bot" \ --data @trace.json
Fichiers d’exemple pour chaque format : OTLP, Langfuse, Vercel AI SDK.
Agent, versions et modèle
Chaque run porte cinq étiquettes. Le nom de l’agent regroupe les runs ; les quatre autres sont les dimensions sur lesquelles comparer :
- Version de l’app — la release de l’application qui embarque l’agent (votre service, votre app web).
- Version de l’agent — l’agent lui-même : son code, ses outils, son orchestration. Par défaut, c’est la version de l’app si vous ne la définissez pas.
- Version du prompt — la version du prompt système ou du template de prompt.
- Modèle — lu dans les appels LLM du run (le plus utilisé), rien à définir.
Pour chaque champ, la première valeur trouvée l’emporte.
| Champ | OpenTelemetry (span, puis ressource) | Langfuse | En-tête de repli |
|---|---|---|---|
| Agent | trajekt.agent.name, gen_ai.agent.name, ai.telemetry.functionId, service.name | metadata.agentName, nom de la trace | X-Trajekt-Agent |
| Version de l’app | trajekt.app.version, app.version, ai.telemetry.metadata.appVersion, service.version | metadata.appVersion, release | X-Trajekt-App-Version |
| Version de l’agent | trajekt.agent.version, gen_ai.agent.version, ai.telemetry.metadata.agentVersion — sinon la version de l’app | metadata.agentVersion, version, release | X-Trajekt-Agent-Version |
| Version du prompt | trajekt.prompt.version, gen_ai.prompt.version, ai.telemetry.metadata.promptVersion | metadata.promptVersion, promptVersion de la génération | X-Trajekt-Prompt-Version |
| Environnement | trajekt.environment, deployment.environment.name | environment | X-Trajekt-Environment |
| Scores | trajekt.score.<nom> (un nombre, plus c’est haut mieux c’est) — voir Scores de qualité | scores de la trace (numériques et booléens) | POST /v1/scores |
| Scénario | trajekt.scenario, ai.telemetry.metadata.scenario — marque un run d’évaluation, voir Suites d’évaluation | metadata.scenario | X-Trajekt-Scenario |
Les en-têtes ne servent que si la trace ne porte aucune valeur. Un run sans nom d’agent est classé sous unknown.
service.version suffit. Définissez trajekt.agent.version et trajekt.prompt.version dès qu’ils évoluent séparément — sinon un changement de prompt ne se distingue pas d’une release de l’app.Scores de qualité
Coût, durée et échecs disent comment un agent a tourné, pas s’il a fait la bonne chose. Une version moins chère qui crée les tickets dans le mauvais projet a l’air meilleure sur toutes les autres métriques. Les scores comblent ce manque : un nombre par run et par nom, plus c’est haut, mieux c’est, idéalement entre 0 et 1 — correct ou non, part des attentes remplies, note d’un juge LLM, pouce levé.
- Dans la trace : un attribut
trajekt.score.correct = 1sur n’importe quel span, ourun.score('correct', true)avectraceRun. Les scores Langfuse arrivent avec la trace. - Après coup, quand vous savez : le retour de l’utilisateur, la merge request acceptée, une revue. Envoyez-le avec l’identifiant de trace que votre app connaît déjà. Un score envoyé ainsi n’est jamais écrasé par la trace.
curl -X POST https://trajekt.example.com/v1/scores \
-H "Authorization: Bearer tk_live_..." -H "Content-Type: application/json" \
-d '{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "name": "correct", "value": 0, "comment": "mauvais projet Jira" }'Il accepte aussi une liste ({ "scores": [ … ] }, jusqu’à 500) et un runId Trajekt au lieu d’un traceId. Dans les tests, scoreTrajectory(run, expectations) transforme des vérifications en score (la part des attentes remplies).
Les scores sont comparés comme les autres métriques, mais c’est une baisse qui est une régression : en production (au moins −0,05, significative, 20 runs notés de chaque côté — alertes comprises), et dans les suites d’évaluation, où ils passent en premier et où un scénario dont le score baisse de 0,5 ou plus fait échouer la suite. Ils s’affichent sur chaque run, dans la liste des runs quand ils sont sous 1, et par version sur la page de l’agent.
Utiliser le dashboard
| Page | Pour |
|---|---|
/ Agents | Voir quels agents ont régressé. Une carte par agent : runs, taux d’échec, durée médiane et coût moyen sur 24 h, tendance sur 14 jours, et les régressions trouvées. |
/agents/:name | Répondre à « est-ce que ça se dégrade, et depuis quand ? ». Comparer les versions métrique par métrique (voir plus bas), les dernières 24 h face aux 7 jours précédents, tendances quotidiennes avec le premier run de chaque version marqué, chemins d’outils par version, runs récents. |
/runs | Filtrer les runs par agent, version d’app, version d’agent, version de prompt, modèle, statut, ou « appels d’outils répétés uniquement ». |
/runs/:id | Rejouer un run sur une timeline. Les appels répétés (REPEAT #5), les relances (ATTEMPT 2) et les échecs sont signalés ; cliquez sur un step pour voir ses arguments, son résultat, son prompt ou sa sortie. |
/runs/:id/compare | Aligner deux runs étape par étape. Par défaut, le run est comparé au dernier run réussi de la version précédente ; ajoutez ?with=<id du run> pour en choisir un autre. |
/agents/:name/evals | Comparer deux versions sur votre suite d’évaluation, scénario par scénario : PASS ou FAIL, ce qui s’est dégradé, et un diff par scénario. Voir Suites d’évaluation. |
/explore | Ouvrir un fichier de trace dans le navigateur sans l’envoyer au serveur. |
Comparer des versions
Sur la page d’un agent, Compare by choisit la dimension — version d’app, version d’agent, version de prompt ou modèle — et la comparaison suit :
- Latest vs previous : la valeur la plus récente face à la précédente.
- Pick two : n’importe quels A et B, par exemple le prompt
v7face auv8, ou l’app3.1.0face à la3.2.0. Pour les modèles, A est par défaut le plus utilisé : un modèle moins cher à l’essai apparaît en B.
L’URL garde la sélection (/agents/sentry-triage?dim=prompt&compare=ab&a=v7&b=v8) : une comparaison se partage par un lien. Le tableau des versions, les chemins d’outils et les filtres de runs suivent la même dimension.
from et to — ou comparez sur la dimension agent.Comment les métriques d’un run sont calculées :
- Appel d’outil répété : le même outil avec les mêmes arguments (ordre des clés ignoré) qu’un appel précédent réussi, sans appel modifiant l’état entre les deux. Relire un fichier après l’avoir modifié, ou relancer les tests après un correctif, n’est pas une boucle ; faire deux fois de suite la même écriture, si. Un outil est considéré comme modifiant l’état d’après son nom (
write,edit,create,update,post,run,bash… mais pasget,read,search,list…) ;setMutatingTools()de@trajekt-ai/coreremplace cette règle. Les appels sans arguments capturés ne sont jamais comptés. - Relance : un appel au même outil ou au même modèle juste après l’échec de cet appel. Une relance n’est pas comptée comme une répétition.
- Statut : avec un seul span racine, c’est la racine qui décide — les erreurs rattrapées dans le run ne le font pas échouer. Sinon, c’est le dernier step qui n’est pas un span système.
- Coût : le coût fourni par la source quand il existe (Langfuse), sinon estimé à partir des tokens et affiché avec
≈. - Chemin d’outils : les outils dans l’ordre ; les relances sont retirées et les répétitions consécutives deviennent
×n, par exemplesentry.getIssue → gitlab.searchCode ×3 → jira.createIssue.
Alertes de régression
Deux comparaisons tournent pour chaque agent ayant eu des runs dans les dernières 24 h :
- les dernières 24 h face aux 7 jours précédents ;
- les dernières versions d’agent, de prompt et d’app face à la précédente, chacune tant qu’elle a moins de 7 jours — un changement de prompt seul peut déclencher une alerte.
Huit métriques sont comparées — runs en échec, appels d’outils en échec, durée moyenne, coût moyen, steps par run, tokens par run, appels d’outils répétés, part des runs sur un chemin d’outils jamais vu — plus chaque score de qualité présent des deux côtés. Une métrique n’est signalée que si les trois conditions sont réunies :
- l’écart est statistiquement significatif (test unilatéral, p < 0,01), et le reste après correction pour le nombre de métriques testées en même temps (Holm) — sinon une métrique sur dix crierait au loup par hasard ;
- il est assez grand pour compter : +15 % sur une moyenne, +3 points sur un taux, −0,05 sur un score ;
- il y a au moins 20 runs de chaque côté.
Lancez la vérification à intervalle régulier. Elle couvre tous les projets ; chaque nouvelle régression est enregistrée et postée sur Slack une fois par agent et par métrique toutes les 24 h. Le même appel purge les anciens runs quand TRAJEKT_RETENTION_DAYS est défini.
0 * * * * curl -fsS -X POST https://trajekt.example.com/api/alerts/check -H "Authorization: Bearer $CRON_SECRET"
docker compose exec dashboard pnpm check-regressions
Projets et exploitation
Un projet regroupe des runs et a ses propres clés d’API : un par équipe, produit ou environnement. Créez et ouvrez les projets depuis la page /projects du dashboard — les clés ne sont affichées qu’une fois, à la création — ou en ligne de commande avec pnpm --filter dashboard create-project <nom>. Il y a toujours un seul mot de passe de connexion pour toute l’instance.
Données sensibles
Les prompts, les arguments et les résultats d’outils contiennent souvent des données clients. Trajekt les masque avant tout stockage — messages, arguments, résultats, erreurs et payload d’origine. Par défaut, il masque les secrets : clés d’API et jetons (Anthropic, OpenAI, GitHub, GitLab, Slack, AWS, Stripe, Google, Trajekt), en-têtes Bearer, JWT, et la valeur des clés comme password, token, api_key ou authorization. Ajoutez ce dont votre domaine a besoin :
TRAJEKT_REDACT=secrets,email,iban,card,phone
TRAJEKT_REDACT_PATTERNS=[{"name":"customer","pattern":"CUST-\\d{6}"},{"name":"contract","pattern":"CTR-[A-Z0-9]{8}"}]Une correspondance devient une marque — [email], [iban], [card] (seulement les numéros qui passent le contrôle de Luhn), [customer] — toujours la même pour un même type de valeur : les métriques et la détection des appels répétés fonctionnent toujours. Si la configuration est invalide, l’ingestion refuse la requête (503) plutôt que de stocker des données que vous pensiez masquées. Dans votre code, createRedactor et redactTrajectory de @trajekt-ai/core appliquent les mêmes règles ; avec traceAnthropic, { captureContent: false } garde les prompts totalement hors des traces.
- Rétention :
TRAJEKT_RETENTION_DAYS=90supprime les runs plus anciens (et leurs scores) à chaque passage du cron ; les alertes sont gardées au moins 90 jours. - Stockage : un run stocke chaque step avec son payload d’origine.
TRAJEKT_STORE_RAW=falseretire le payload et divise la taille par deux environ ; Postgres compresse déjà le reste. - Limites d’ingestion : 600 requêtes par minute et par projet (
TRAJEKT_INGEST_RATE_PER_MIN), 20 Mo par requête après gzip (TRAJEKT_MAX_BODY_MB). Les exporteurs OpenTelemetry réessaient sur429et respectentRetry-After. La limite est tenue en mémoire, par instance du serveur : un garde-fou, pas un quota.
Interroger vos agents (MCP)
Trajekt est un serveur MCP. Connectez-y Claude, Cursor ou votre propre agent et posez vos questions en langage naturel : « sentry-triage s’est-il dégradé depuis le prompt v8 ? », « pourquoi ce run a-t-il échoué ? », « le modèle moins cher est-il assez bon ? ».
Les outils renvoient des réponses déjà calculées par Trajekt — verdicts statistiques, résumés de runs, différences — sous forme de texte court et de JSON structuré. Votre agent n’a jamais à lire des milliers de spans : ses réponses restent courtes et fondées sur les chiffres. Tous les outils sont en lecture seule.
claude mcp add --transport http trajekt http://localhost:3000/api/mcp \ --header "Authorization: Bearer tk_read_..."
{
"mcpServers": {
"trajekt": {
"type": "http",
"url": "http://localhost:3000/api/mcp",
"headers": { "Authorization": "Bearer tk_read_..." }
}
}
}Essayez sans rien installer : la démo en ligne expose son point d’accès MCP sans clé sur https://trajekt-demo.vercel.app/api/mcp.
Clés de lecture
Les clés ont une portée. Une clé d’ingestion (tk_live_…) ne peut qu’envoyer des traces ; une clé de lecture (tk_read_…) ne peut que lire — le serveur MCP et toutes les routes GET /api/*. Donnez une clé de lecture aux agents, jamais votre clé d’ingestion.
docker compose exec dashboard pnpm create-read-key claude-desktop
Outils
| Outil | Répond à |
|---|---|
list_agents | Quels agents existent, leur activité sur 24 h, et lesquels ont régressé. Le point de départ. |
list_versions | Versions d’app, versions d’agent, versions de prompt ou modèles d’un agent, avec leurs métriques. |
compare | B est-il pire que A ? Deux valeurs de n’importe quelle dimension, la dernière face à la précédente, ou les dernières 24 h face aux 7 jours précédents — un verdict par métrique. |
compare_eval_suite | Pour les runs d’évaluation (marqués d’un scénario) : PASS ou FAIL entre deux versions, les verdicts sur la suite, et les scénarios dégradés avec ce qui a changé. |
get_regressions | Les régressions détectées maintenant, et les alertes déjà envoyées. |
find_runs | Runs récents par agent, statut, versions, modèle, scénario, production ou évaluation, ou appels d’outils répétés. |
explain_run | Ce qu’un run a fait : chemin d’outils, et les steps en échec, relancés ou répétés, avec leurs arguments. |
diff_runs | Deux runs alignés étape par étape, et la configuration qui a changé entre eux. |
Il n’y a volontairement pas d’outil de requête libre : les traces contiennent des données clients, et un agent qui écrit son propre SQL est à la fois fragile et risqué. Un résumé en texte brut de cette doc, destiné aux agents, est disponible sur /llms.txt (en anglais).
@trajekt-ai/core
@trajekt-ai/core n’est pas encore publié sur npm — la release est prête et sera annoncée ici. D’ici là, compilez-le depuis le dépôt et installez-le par chemin : pnpm --filter @trajekt-ai/core build, puis npm i ../trajekt/packages/core depuis votre projet.| Fonction | Renvoie |
|---|---|
normalize(trace) | Une Trajectory à partir d’une trace Langfuse, de spans OTLP/JSON, d’un résultat AI SDK ou d’une trajectoire Trajekt. Le format est détecté d’après la forme du JSON. |
normalizeAll(payload) | Une Trajectory par traceId, pour les lots OTLP qui mélangent plusieurs traces. |
computeMetrics(run) | status, durationMs, stepCount, toolCallCount, errorCount, inputTokens, outputTokens, costUsd, costEstimated, redundantToolCalls, retryCount, pathSignature… |
annotateSteps(run) | Par step : error, attempt, repeatOf. |
diffTrajectories(a, b) | Lignes alignées étape par étape, marquées = ≠ + −, avec les totaux. |
compareWindows(baseline, current) | Un verdict par métrique : regression, improvement, stable ou insufficient. |
decodeOtlpProtobuf(bytes) | Une requête OTLP protobuf → sa forme OTLP/JSON, prête pour normalize. Sans dépendance. |
traceRun, traceTool, traceLlm, traceAnthropic(client, tracer) | Les spans que lit Trajekt, avec n’importe quel tracer OpenTelemetry ; l’enveloppe du client Anthropic enregistre chaque appel au modèle. Voir Instrumenter votre code. |
scoreTrajectory(run, expectations) | La part des attentes remplies, à envoyer comme score de qualité. |
createRedactor(options), redactTrajectory(run, options) | Masque emails, IBAN, cartes, téléphones, secrets et vos propres motifs dans n’importe quelle valeur ou run. Voir Données sensibles. |
setMutatingTools(rule) | Quels outils modifient l’état (une regex, une liste de noms ou une fonction), pour la détection des appels répétés. |
assertTrajectory(run, expectations) | Lève une erreur lisible qui liste chaque attente non remplie (outils appelés, ordre, arguments, budgets). checkTrajectory renvoie la liste à la place. |
compareScenarios(a, b) | Comparaison de suites d’évaluation, scénario par scénario : result (pass / fail), reasons, verdicts sur la suite et changements par scénario. scenarioRunOf(run) transforme une trajectoire en entrée. |
mergeTrajectories(a, b) | Un run reconstitué à partir de deux lots partiels d’une même trace. |
setPricing(prices) | Remplace la table de prix utilisée pour estimer les coûts. |
Estimation des coûts. Les spans portent rarement un coût : il est estimé à partir des tokens avec une table de prix publics indicatifs pour les modèles courants d’Anthropic (dont Claude 5.x), OpenAI, Google et Mistral. Les lectures et écritures du cache de prompt sont tarifées à part quand les spans les rapportent. Le préfixe de modèle le plus long l’emporte ; un modèle inconnu n’a pas d’estimation plutôt qu’une estimation fausse. Les prix changent : remplacez-les.
import { setPricing } from '@trajekt-ai/core';
setPricing({ 'my-fine-tune': { input: 1.2, output: 4.8 } }); // USD par million de tokensTRAJEKT_PRICING={"my-fine-tune":{"input":1.2,"output":4.8}}Tester votre agent
La production vous dit qu’un agent s’est dégradé après coup. Pendant le développement, rejouez un jeu fixe de scénarios — outils et données mockés — à chaque changement, et vérifiez que rien n’a régressé avant la mise en production.
Vérifier un run
Vérifiez ce que l’agent a fait, pas seulement sa réponse finale. Fonctionne avec node:test, vitest ou jest ; l’erreur liste chaque attente non remplie, avec le chemin d’outils du run.
import { test } from 'node:test';
import { readFileSync } from 'node:fs';
import { normalize, assertTrajectory } from '@trajekt-ai/core';
test('npe-checkout : ouvre un ticket PAY, sans boucle, dans le budget', () => {
const run = normalize(JSON.parse(readFileSync('runs/npe-checkout.json', 'utf8')));
assertTrajectory(run, {
status: 'ok',
order: ['sentry.getIssue', 'gitlab.readFile', 'jira.createIssue'],
calledWith: [{ tool: 'jira.createIssue', args: { project: 'PAY' } }],
notCalls: ['gitlab.createMergeRequest'],
maxRepeatedToolCalls: 0,
maxSteps: 12,
maxCostUsd: 0.08,
});
});Également disponibles : calls, maxCallsPerTool, maxToolCalls, maxFailedToolCalls, maxRetries, maxDurationMs, maxTokens. calledWith compare les arguments partiellement (seulement les clés fournies) et lit les arguments sérialisés en JSON.
Suites d’évaluation
Un seul run ne suffit pas pour juger un changement : le modèle n’est pas déterministe. Jouez chaque scénario quelques fois, sur chaque version, et envoyez les runs à Trajekt avec un attribut trajekt.scenario :
import { trace } from '@opentelemetry/api';
for (const scenario of scenarios) { // par ex. 10 à 30 issues Sentry mockées
for (let i = 0; i < 3; i++) { // quelques répétitions par scénario
await trace.getTracer('eval').startActiveSpan('invoke_agent sentry-triage', async (span) => {
span.setAttributes({
'gen_ai.agent.name': 'sentry-triage',
'trajekt.scenario': scenario.name,
'trajekt.agent.version': process.env.GIT_SHA ?? 'dev',
'trajekt.prompt.version': PROMPT_VERSION,
});
await runAgent(scenario.input, { tools: mockedTools(scenario) });
span.end();
});
}
}Les runs avec un scénario sont des runs d’évaluation : ils sont exclus des métriques de production, des tendances et des alertes, et comparés sur /agents/:name/evals. Chaque scénario est comparé à lui-même entre les deux versions, ce qui est bien plus sensible que de comparer des moyennes :
- Sur toute la suite : un test apparié (Wilcoxon signé, unilatéral, p < 0,05, corrigé par Holm sur l’ensemble des métriques) sur chaque métrique et chaque score de qualité. Durée, coût, steps et tokens sont comparés en ratios : un gros scénario ne pèse pas plus qu’un petit. Une métrique est signalée si l’écart typique est aussi assez grand : +15 %, +5 points sur un taux, −0,05 sur un score. Il faut au moins 8 scénarios.
- Scénario par scénario : pas de statistiques avec trois runs, des seuils simples à la place — un scénario qui se met à échouer (par ex. 0/3 → 2/3), un score en baisse de 0,25 ou plus, +50 % de durée, de coût, de steps ou de tokens, un appel d’outil répété qui ne l’était pas, un chemin d’outils jamais vu dans la référence.
- Résultat :
failsi une métrique régresse sur la suite, si un scénario se met à échouer, ou si le score d’un scénario baisse de 0,5 ou plus. Un nouveau chemin d’outils est affiché mais ne fait jamais échouer la suite.
Chaque scénario renvoie vers un diff étape par étape des deux versions. Comparez sur n’importe quelle dimension : version d’agent, de prompt, d’app ou modèle.
En CI
GitHub Actions. Enregistrez la suite sur main sous le SHA du commit, rejouez-la à chaque pull request, et laissez l’action comparer les deux. Elle écrit le résultat dans le résumé du job, poste un commentaire sur la pull request (mis à jour à chaque push) avec les scénarios dégradés et un lien vers leur diff, et fait échouer le job en cas de régression.
name: Agent evals
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
evals:
runs-on: ubuntu-latest
env:
AGENT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@v4
# Votre suite : outils mockés, quelques runs par scénario, chacun tracé avec
# trajekt.scenario et trajekt.agent.version=$AGENT_SHA.
- run: npm ci && npm run eval
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.TRAJEKT_URL }}
OTEL_EXPORTER_OTLP_PROTOCOL: http/json
OTEL_EXPORTER_OTLP_HEADERS: Authorization=Bearer ${{ secrets.TRAJEKT_INGEST_KEY }}
- if: github.event_name == 'pull_request'
uses: LeoLR/Trajekt/actions/eval-suite@main
with:
url: ${{ vars.TRAJEKT_URL }}
read-key: ${{ secrets.TRAJEKT_READ_KEY }}
agent: sentry-triage
baseline: ${{ github.event.pull_request.base.sha }}
candidate: ${{ env.AGENT_SHA }}Entrées : dimension (agent par défaut ; prompt, app ou model pour comparer sur une autre étiquette), fail-on-regression et comment (tous deux à true). Sorties : result et report-url. Le commit de référence doit avoir été évalué — c’est le rôle du déclencheur push sur main. Toutes les options sont dans le README de l’action.
Toute autre CI appelle le même point d’accès :
curl -s -H "Authorization: Bearer $TRAJEKT_READ_KEY" \
"$TRAJEKT_URL/api/agents/sentry-triage/evals?dimension=agent&a=$BASE_SHA&b=$GIT_SHA" \
| jq -e '.comparison.result == "pass"' || { echo "la suite d'évaluation a régressé"; exit 1; }Sans serveur, comparez deux exécutions enregistrées de la suite dans le code :
import { normalize, scenarioRunOf, compareScenarios } from '@trajekt-ai/core';
const baseline = loadRuns('eval/baseline/').map((t) => scenarioRunOf(normalize(t)));
const current = loadRuns('eval/current/').map((t) => scenarioRunOf(normalize(t)));
const r = compareScenarios(baseline, current);
if (r.result === 'fail') {
console.error(r.reasons.join('\n'));
process.exit(1);
}Une version exécutable de tout cela — un agent jouet, des scénarios mockés, des tests de comportement et la comparaison — se trouve dans examples/eval-suite. Un agent peut poser la même question par MCP avec compare_eval_suite : « ma branche a-t-elle dégradé sentry-triage ? ».
Tester Trajekt
pnpm --filter @trajekt-ai/core test
Les tests du dashboard exécutent le vrai SQL sur une base jetable, créée et supprimée par chaque fichier de test. Il faut un Postgres qui accepte l’utilisateur postgres sans mot de passe :
docker run -d --name trajekt-test -p 54329:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:16-alpine pnpm --filter dashboard test
TEST_PG_HOST, TEST_PG_PORT et TEST_PG_USER changent la cible (par défaut : 127.0.0.1, 54329, postgres). pnpm --filter dashboard preview rend chaque page du dashboard avec des données simulées en fichiers HTML statiques, sans Next.js.
Les tests de bout en bout lancent l’app compilée (next start) dans un vrai navigateur avec Playwright : connexion, chaque page, création de projet, ingestion en JSON et en protobuf, scores, masquage et MCP. Ils sont dans e2e/ ; le workflow de CI montre comment démarrer l’app avant d’y lancer npm test.
Référence de l’API
Les routes de lecture acceptent la session du dashboard ou une clé de lecture : Authorization: Bearer tk_read_….
| Point d’accès | Auth | Rôle |
|---|---|---|
POST /v1/traces | clé d’API | Ingérer de l’OTLP (JSON ou protobuf), une trace Langfuse, un résultat AI SDK ou une trajectoire. Renvoie les runs créés ou complétés. |
POST /v1/scores | clé d’API | Attacher des scores de qualité à des runs, par traceId ou runId. |
GET /api/agents | lecture | Un résumé par agent. |
GET /api/agents/:name/timeseries | lecture | from, to, bucket=hour|day, version. |
GET /api/agents/:name/versions | lecture | Métriques par valeur de dimension=app|agent|prompt|model (agent par défaut). |
GET /api/agents/:name/compare | lecture | mode=window, mode=version&dimension=…, ou mode=ab&dimension=…&a=…&b=…, avec from et to en option. |
GET /api/agents/:name/evals | lecture | Comparaison de suite d’évaluation : dimension, a, b. comparison.result vaut pass ou fail. |
GET /api/runs | lecture | agent, appVersion, version, promptVersion, model, scenario, kind=live|eval, status, loops=1, from, to, limit, cursor. |
GET /api/runs/:id | lecture | Métriques du run et trajectoire complète. |
GET /api/alerts | lecture | Alertes envoyées, les plus récentes d’abord. |
POST /api/mcp | clé de lecture | Serveur MCP (Streamable HTTP, réponses JSON). |
POST /api/alerts/check | CRON_SECRET | Lancer la détection de régressions sur tous les projets et purger les anciens runs (aussi en GET, pour Vercel Cron). |
GET / POST /api/projects | session | Lister les projets, ou en créer un (renvoie ses clés d’ingestion et de lecture, une seule fois). |
Les erreurs ont toujours la forme { "error": { "code", "message" } }. Le contrat complet est dans docs/api-contract.md (en anglais).
Dépannage
- L’exporteur n’arrive pas à se connecter, ou utilise le port 4317
- C’est du gRPC, que Trajekt n’accepte pas. Utilisez OTLP sur HTTP :
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf(ouhttp/json) et l’URL du dashboard. 429 RATE_LIMITEDou413 PAYLOAD_TOO_LARGE- Trop de requêtes par minute pour ce projet, ou un lot trop gros. Les exporteurs réessaient sur 429 ; pour 413, réduisez la taille des lots (
OTEL_BSP_MAX_EXPORT_BATCH_SIZE) ou augmentezTRAJEKT_MAX_BODY_MB. 422 UNKNOWN_FORMAT- Le JSON est valide mais ne correspond à aucune des formes supportées. Le message liste les clés de premier niveau trouvées.
401 UNAUTHORIZED- L’en-tête
Authorizationmanque, ou la clé n’appartient à aucun projet. Les clés commencent partk_live_. - Les runs apparaissent sous
unknown - Aucun nom d’agent n’a été trouvé. Définissez
OTEL_SERVICE_NAME(la valeur par défaut du SDK,unknown_service, est ignorée) ou envoyezX-Trajekt-Agent. - Le coût affiche
— - Le modèle n’est pas dans la table de prix, ou les spans ne portent pas de nombre de tokens. Ajoutez le modèle avec
TRAJEKT_PRICING. - Chaque métrique indique « not enough data »
- Une comparaison en production demande au moins 20 runs de chaque côté, une suite d’évaluation au moins 8 scénarios.
- Une métrique a beaucoup bougé mais reste « stable »
- L’écart n’est pas significatif une fois corrigé pour le nombre de métriques testées, ou il est sous l’effet minimal. Survolez le verdict pour voir pourquoi.
- La page de connexion indique que la connexion est désactivée
- Définissez
DASHBOARD_PASSWORDetDASHBOARD_SESSION_SECRET(16 caractères ou plus) et redémarrez.
Il manque quelque chose ? Ouvrez une issue.