GNL
Dokümanlar menüsü
Core · Ücretsiz@gnldev/durable

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

replayRun — aynı girdiyi yeni modelde koş
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.

diffRuns — nerede ayrıldılar?
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 differs

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

bir diff girdisinin şekli
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.

regressionReport — diff artı isteğe bağlı skor
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.

replayRun — bellek enjeksiyonu çıkarılmış hâlde
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#

fnreplayRun

Kayı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.

fndiffRuns

İki çalıştırmayı karar noktalarında diff'ler ve RunDiff döner — steps[], summary ve divergentAt.

fnregressionReport

diffRuns etrafında bir rapor iskeleti: base ile new'i diff'ler ve isteğe bağlı scorer'ı sonuç üzerinde koşar.

fnbuildDecisionSequence

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

typeReplayRunConfig

journal, runId, newRunId?, model, tools?, system?, guard?, approvals?, stopWhen?, replay?, stripMemoryContext?

typeRunDiff

{ steps: DiffEntry[]; divergentAt?: number; summary: { same, changed, missing, added } }

typeDecisionPoint

{ step, kind: 'model' | 'tool', toolCallId?, toolName?, value } — bir model adımı ya da bir tool çağrısı.

Orijinal çalıştırmaya dokunulmaz
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.
Tek çalıştırma kanıt değildir
Tek bir replay iki örneği karşılaştırır. Modeller çağrılar arasında deterministik olmadığı için bir karar noktasındaki fark regresyon değil gürültü olabilir. Replay'i tekrarlayın ya da regressionReport üzerinden bir scorer bağlayıp toplam üzerinden karar verin.