Dokümanlar menüsü
Replay tabanlı regresyon
Kayıtlı bir çalıştırmayı yeni bir modele, prompt'a ya da tool setine karşı yeniden koşun, sonra iki çalıştırmayı karar karar diff'leyin — ve belleğin enjekte ettiğini sıyırıp sebebin o olup olmadığını sınayın.
Ne işe yarar#
Bir trace size bir kere ne olduğunu söyler; modeli değiştirseydiniz ne olacağını söyleyemez. Onun için aynı girdiyi tekrar koşturmak gerekir. Journal çalıştırmanın girdisini dondurduğu için replayRun tam olarak bunu yapabilir: kayıtlı girdiyi okur ve yeni bir model, prompt, tool seti ya da guard altında sıfırdan koşar.
diffRuns ardından iki çalıştırmayı karar noktalarında karşılaştırır — her model adımı ve her tool çağrısı. Hizalama toolCallId ile yapılmaz (onları model ve SDK rastgele atar, iki bağımsız çalıştırma asla aynısını taşımaz); model adımı artı o adımın ürettiği tool çağrılarının içerik sırası ile yapılır. İki ayrı çalıştırmayı konumsal olarak karşılaştırılabilir kılan şey budur.
Sonuç, çalıştırmaların ilk ayrıştığı yeri verir (divergentAt) — böylece "yeni model farklı davranıyor" cümlesi "yeni model 2. adımda farklı bir tool seçti"ye dönüşür.
Kurulum / import#
import { replayRun, diffRuns, regressionReport } from '@gnldev/durable';
import type { RunDiff, DiffEntry, DecisionPoint } from '@gnldev/durable';Buradaki her şey @gnldev/durable içinde; ek paket gerekmez. Skorlama bilinçli olarak dışarıda: regressionReport sizin verdiğiniz bir scorer'ı kabul eder, hazır scorer'lar ise @gnldev/evals'te yaşar.
Adım adım#
Journal'da zaten duran bir çalıştırmadan başlayın. replayRun onun kayıtlı girdisini okur ve neyi geçersiz kıldıysanız onunla tekrar koşar — burada farklı bir model. Yeni bir runId altına yazar ve orijinal çalıştırmanın kayıtlarına hiç dokunmaz.
const { newRunId } = await replayRun({
journal,
runId: 'order-123', // the recorded run
model: anthropic('claude-opus-5'),
tools: { chargeCard },
});
// the input is read from the journal — you do not pass the prompt againŞimdi ikisini diff'leyin. RunDiff.steps her karar noktası için bir girdi taşır; durumu same, changed, missing ya da added olur. summary bunları sayar, divergentAt ise ilk farkın indeksidir.
const diff = await diffRuns(journal, 'order-123', newRunId);
diff.summary; // { same: 3, changed: 1, missing: 0, added: 0 }
diff.divergentAt; // 3 — the index of the first decision that differsBir model kararında ayrıntı her iki tarafın metnini ve tool çağrısı listelerini (ad + argsHash) taşır; bir tool kararında tool adını, argüman hash'lerini, durumu ve iki çıktıyı taşır. Yapısal uyuşmazlık (bir tarafta model adımı, diğerinde tool çağrısı) note alanında bildirilir.
const entry: DiffEntry = diff.steps[diff.divergentAt!];
// {
// step: 1,
// kind: 'tool',
// status: 'changed',
// detail: {
// toolName: 'chargeCard',
// argsHashA: '9f21…', argsHashB: 'c704…',
// statusA: 'succeeded', statusB: 'succeeded',
// outputA: { charged: 500 }, outputB: { charged: 5000 },
// },
// }regressionReport ince bir sarmalayıcıdır: iki çalıştırmayı diff'ler ve bir scorer verdiyseniz onu diff üzerinde koşar. Kendi skorlama mantığı yoktur — bağımlılık tek yönlüdür, @gnldev/evals'ten @gnldev/durable'a, tersi değil.
const report = await regressionReport(journal, 'order-123', newRunId, {
scorer: (d) => d.summary.changed / d.steps.length,
});
// { baseRunId, newRunId, diff, score }Karşı-olgu: sebep bellek miydi?#
Bir ajan, belleğin anımsattığı bir şeyi kullanarak cevap verdiğinde, "model bunu recall parçasından okumuş olmalı" bir çıkarımdır, ölçüm değil. stripMemoryContext bunu deneye çevirir: çalıştırmanın :memctx provenance kaydını okur, yalnız o turun kendi gelen mesajlarını tutar, belleğin onların önüne dizdiği her şeyi düşürür ve tekrar sorar. Sonra iki cevabı diff'lersiniz.
const withoutMemory = await replayRun({
journal,
runId: 'chat-42',
model,
stripMemoryContext: true, // keep only this turn's own messages
});
const diff = await diffRuns(journal, 'chat-42', withoutMemory.newRunId);
// same answer -> the recall was not what produced it
// different -> the injected context carried the answerİki dürüst sınır kodda yazılı, üstü örtülmüş değil. Çalıştırmanın kullanılabilir bir :memctx kaydı yoksa hata fırlatır — provenance var olmadan önceki bir çalıştırma dürüstçe sıyrılamaz. Ve dondurulmuş system metnine cerrahi müdahale yapılmaz: working memory oraya enjekte edildiyse orada kalır, ve kod bunu çağıranın açıkça belirtmesi gerektiğini söyler.
API#
replayRunKayıtlı bir çalıştırmanın girdisini (runKeys.input) okur ve YENİ bir runId altında sıfırdan koşar. forkRun değildir: hiçbir öneki kopyalamaz, hiçbir adımı replay etmez — model, tools, system, guard, approvals ve stopWhen geçersiz kılınabilir.
diffRunsİki çalıştırmayı karar noktalarında diff'ler ve RunDiff döner — steps[], summary ve divergentAt.
regressionReportdiffRuns etrafında bir rapor iskeleti: base ile new'i diff'ler ve isteğe bağlı scorer'ı sonuç üzerinde koşar.
buildDecisionSequenceJournal girdilerini, diff'in hizalandığı DecisionPoint dizisine çevirir. @gnldev/evals'teki trajectory scorer da bunu kullanır, böylece karar noktasının tek bir tanımı olur.
ReplayRunConfigjournal, runId, newRunId?, model, tools?, system?, guard?, approvals?, stopWhen?, replay?, stripMemoryContext?
RunDiff{ steps: DiffEntry[]; divergentAt?: number; summary: { same, changed, missing, added } }
DecisionPoint{ step, kind: 'model' | 'tool', toolCallId?, toolName?, value } — bir model adımı ya da bir tool çağrısı.
replayRun yalnız yeni runId altına yazar. Kaynak çalıştırmanın journal kayıtları okunur, değiştirilmez — yani bir regresyon kontrolü üretim geçmişine karşı, onu riske atmadan koşturulabilir.regressionReport üzerinden bir scorer bağlayıp toplam üzerinden karar verin.