DOCS

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 sur POST /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.

1 · lancer Trajekt
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
2 · créer un projet (affiche la clé d’API une seule fois)
docker compose exec dashboard pnpm create-project mon-projet
3 · optionnel : deux semaines de données de démo
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.

bash
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
Sans fichier .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.

bash
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

VariableRequiseRôle
DATABASE_URLouiChaîne de connexion Postgres. Définie par le fichier compose.
DASHBOARD_PASSWORDouiLe mot de passe de la page de connexion. Il y a un seul compte.
DASHBOARD_SESSION_SECRETouiSigne le cookie de session. 16 caractères minimum.
CRON_SECRETpour les alertesJeton Bearer de POST /api/alerts/check. Le point d’accès est désactivé tant qu’il est vide.
SLACK_WEBHOOK_URLnonWebhook entrant Slack. Les nouvelles régressions y sont postées.
TRAJEKT_PUBLIC_URLnonURL publique du dashboard, pour les liens des messages Slack.
TRAJEKT_PRICINGnonPrix personnalisés en JSON, en USD par million de tokens. Voir l’estimation des coûts.
TRAJEKT_REDACTnonCe 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_PATTERNSnonVos propres motifs, en JSON : [{"name":"customer","pattern":"CUST-\\d{6}"}].
TRAJEKT_RETENTION_DAYSnonLes runs plus anciens sont supprimés à chaque passage du cron. Vide : conservés indéfiniment.
TRAJEKT_INGEST_RATE_PER_MINnonRequê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_MBnonTaille maximale d’une requête, après gzip (20 par défaut). Au-delà : 413.
TRAJEKT_STORE_RAWnonfalse : 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.

.env de votre agent
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 :

instrumentation.ts
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.

Gzip est accepté. gRPC ne l’est pas : utilisez OTLP sur HTTP (port 3000, chemin /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.

agent.ts
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.

ts
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.

ts
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 :

LangChain + OpenInference
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.

importer les 7 derniers jours d’un agent
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.

bash
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.

ChampOpenTelemetry (span, puis ressource)LangfuseEn-tête de repli
Agenttrajekt.agent.name, gen_ai.agent.name, ai.telemetry.functionId, service.namemetadata.agentName, nom de la traceX-Trajekt-Agent
Version de l’apptrajekt.app.version, app.version, ai.telemetry.metadata.appVersion, service.versionmetadata.appVersion, releaseX-Trajekt-App-Version
Version de l’agenttrajekt.agent.version, gen_ai.agent.version, ai.telemetry.metadata.agentVersion — sinon la version de l’appmetadata.agentVersion, version, releaseX-Trajekt-Agent-Version
Version du prompttrajekt.prompt.version, gen_ai.prompt.version, ai.telemetry.metadata.promptVersionmetadata.promptVersion, promptVersion de la générationX-Trajekt-Prompt-Version
Environnementtrajekt.environment, deployment.environment.nameenvironmentX-Trajekt-Environment
Scorestrajekt.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énariotrajekt.scenario, ai.telemetry.metadata.scenario — marque un run d’évaluation, voir Suites d’évaluationmetadata.scenarioX-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.

Si votre app et votre agent sont livrés ensemble, 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 = 1 sur n’importe quel span, ou run.score('correct', true) avec traceRun. 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.
POST /v1/scores — clé d’ingestion
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

PagePour
/ AgentsVoir 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/:nameRé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.
/runsFiltrer 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/:idRejouer 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/compareAligner 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/evalsComparer 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.
/exploreOuvrir 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 v7 face au v8, ou l’app 3.1.0 face à la 3.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.

Comparer deux versions d’app compare aussi tout ce qui a été livré avec. Si une nouvelle version d’agent est sortie sur la même période, réduisez la plage de temps — l’API accepte 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 pas get, read, search, list…) ; setMutatingTools() de @trajekt-ai/core remplace 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 exemple sentry.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.

crontab — toutes les heures
0 * * * * curl -fsS -X POST https://trajekt.example.com/api/alerts/check -H "Authorization: Bearer $CRON_SECRET"
ou dans le conteneur
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 :

.env
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=90 supprime 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=false retire 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 sur 429 et respectent Retry-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 Code
claude mcp add --transport http trajekt http://localhost:3000/api/mcp \
  --header "Authorization: Bearer tk_read_..."
n’importe quel client MCP (Streamable HTTP)
{
  "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.

bash
docker compose exec dashboard pnpm create-read-key claude-desktop

Outils

OutilRépond à
list_agentsQuels agents existent, leur activité sur 24 h, et lesquels ont régressé. Le point de départ.
list_versionsVersions d’app, versions d’agent, versions de prompt ou modèles d’un agent, avec leurs métriques.
compareB 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_suitePour 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_regressionsLes régressions détectées maintenant, et les alertes déjà envoyées.
find_runsRuns récents par agent, statut, versions, modèle, scénario, production ou évaluation, ou appels d’outils répétés.
explain_runCe qu’un run a fait : chemin d’outils, et les steps en échec, relancés ou répétés, avec leurs arguments.
diff_runsDeux 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.
FonctionRenvoie
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.

ts
import { setPricing } from '@trajekt-ai/core';
setPricing({ 'my-fine-tune': { input: 1.2, output: 4.8 } }); // USD par million de tokens
serveur : la même chose par l’environnement
TRAJEKT_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.

triage.test.ts
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 :

eval/run-suite.ts
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 : fail si 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.

.github/workflows/agent-evals.yml
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 :

sur une instance Trajekt (clé de lecture)
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 :

eval/compare.ts
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

tests de la librairie
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 :

bash
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èsAuthRôle
POST /v1/tracesclé d’APIIngé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/scoresclé d’APIAttacher des scores de qualité à des runs, par traceId ou runId.
GET /api/agentslectureUn résumé par agent.
GET /api/agents/:name/timeserieslecturefrom, to, bucket=hour|day, version.
GET /api/agents/:name/versionslectureMétriques par valeur de dimension=app|agent|prompt|model (agent par défaut).
GET /api/agents/:name/comparelecturemode=window, mode=version&dimension=…, ou mode=ab&dimension=…&a=…&b=…, avec from et to en option.
GET /api/agents/:name/evalslectureComparaison de suite d’évaluation : dimension, a, b. comparison.result vaut pass ou fail.
GET /api/runslectureagent, appVersion, version, promptVersion, model, scenario, kind=live|eval, status, loops=1, from, to, limit, cursor.
GET /api/runs/:idlectureMétriques du run et trajectoire complète.
GET /api/alertslectureAlertes envoyées, les plus récentes d’abord.
POST /api/mcpclé de lectureServeur MCP (Streamable HTTP, réponses JSON).
POST /api/alerts/checkCRON_SECRETLancer la détection de régressions sur tous les projets et purger les anciens runs (aussi en GET, pour Vercel Cron).
GET / POST /api/projectssessionLister 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 (ou http/json) et l’URL du dashboard.
429 RATE_LIMITED ou 413 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 augmentez TRAJEKT_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 Authorization manque, ou la clé n’appartient à aucun projet. Les clés commencent par tk_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 envoyez X-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_PASSWORD et DASHBOARD_SESSION_SECRET (16 caractères ou plus) et redémarrez.

Il manque quelque chose ? Ouvrez une issue.