Dokümanlar menüsü
Exactly-once araçlar
Aynı runId ile yeniden çalıştırıldığında yan-etkili araçların (ödeme, e-posta) SADECE BİR KEZ yürütülmesini garanti eder; çökme/retry'de tekrar tahsil olmaz.
Ne işe yarar / ne zaman kullanılır#
Bir agent, bir tool çağırdıktan (ör. kredi kartı tahsilatı) hemen sonra çökerse veya süreç yeniden başlatılırsa, aynı runId ile runDurable tekrar çağrıldığında normalde model isteği baştan tekrarlanır. @gnldev/durable bu tekrar çalıştırmayı tool seviyesinde yakalar: her tool çağrısı, model tarafından üretilen toolCallId anahtarıyla journal'a kaydedilir. Replay'de aynı anahtar için succeeded bir kayıt bulunursa gerçek fonksiyon (execute) BİR DAHA ÇALIŞTIRILMAZ; kayıtlı çıktı doğrudan döner.
Kullanım senaryosu: ödeme tahsilatı, e-posta/SMS gönderimi, dış sisteme yazma gibi idempotent OLMAYAN (yan-etkili) tool'lara sahip her agent. Salt-okunur/idempotent tool'larda bu garanti zaten gerekmez ama zarar da vermez.
Kurulum / import#
pnpm add @gnldev/durable aiJournal olarak hızlı başlangıç için bellek-içi InMemoryJournal kullanılabilir; kalıcı depolama için alt-export'lardaki adaptörler tercih edilir (bkz. Storage adaptörleri):
import { runDurable, InMemoryJournal } from '@gnldev/durable';
// durable: import { SqliteStorage } from '@gnldev/durable/sqlite';
// durable: import { PostgresStorage } from '@gnldev/durable/postgres';Adım adım kullanım#
runDurable, Vercel AI SDK'nın generateText fonksiyonuna doğrudan yerine geçer (drop-in); ek olarak journal ve runId alır. Aşağıdaki örnekte bir tahsilat tool'u ilk çalıştırmada modelin ürettiği CRASH hatasıyla yarıda kesilir; aynı runId ile resume edildiğinde tahsilat SAYACI artmaz (charges hâlâ 1):
const journal = new InMemoryJournal();
const counter = { charges: 0 };
const tools = () => ({
chargeCard: tool({
description: 'charge',
inputSchema: z.object({ amount: z.number() }),
execute: async ({ amount }) => {
counter.charges++;
return { charged: amount };
},
}),
});
// 1) first run: the model calls the tool, then crashes on the next step
await expect(
runDurable({ runId: 'run-1', journal, model, tools: tools(), prompt: 'charge', stopWhen: stepCountIs(6) }),
).rejects.toThrow('CRASH');
expect(counter.charges).toBe(1);
// 2) resume with the same runId: chargeCard does NOT run again — the result comes from the journal
const res = await runDurable({ runId: 'run-1', journal, model, tools: tools(), prompt: 'charge', stopWhen: stepCountIs(6) });
expect(counter.charges).toBe(1); // exactly-once
expect(res.text).toContain('Charged');Bu garanti runDurable içinde otomatik çalışır: verdiğiniz tools, durableTools(tools, ctx) ile sarmalanır — her tool'un execute'i, çağrı öncesi journal'da succeeded/denied kaydı olup olmadığını kontrol eder.
Tool'u tek başına (agent döngüsü dışında) sarmalamak isterseniz durableTool doğrudan kullanılabilir; bu durumda DurableCtx (journal + runId içeren bağlam) elle sağlanır — runDurable bunu sizin için otomatik kurar.
Çağrı kimliğiyle değil, argümanlarla tekilleştirme#
Varsayılan olarak bir araç sonucu modelin toolCallId'sine göre anahtarlanır; yani aynı çağrının yeniden denenmesi aynı kaydı kullanır. idempotency: 'args' ise anahtarı argümanlara taşır: aynı şeyi isteyen iki farklı çağrı tek bir yürütmede birleşir. Model aynı tahsilatı taze bir çağrı kimliğiyle yeniden istediğinde istediğiniz şey budur — "yeniden deneme güvenli" ile "ikinci özdeş istek de güvenli" arasındaki fark.
Anahtar argsHash'ten gelir; sıradan bağımsız bir özet, yani { b: 2, a: 1 } ile { a: 1, b: 2 } aynı çağrıdır. Her değer bir tip etiketi taşır ve metne dönüşünce birbirine benzeyen değerlerin aynı anahtarı paylaşmasını bu engeller: Date, Map, Set, RegExp ve BigInt ayrı ayrı özetlenir. Etiketler olmadan çöküyorlardı ve ikinci çağrı birincinin sonucunu geri okuyordu — yani yanlış cevabı, tam da onu engellemek için var olan mekanizma üretiyordu.
import { durableTool } from '@gnldev/durable';
const chargeCard = durableTool({
name: 'chargeCard',
idempotency: 'args', // key on the arguments, not on the model's toolCallId
sideEffect: true,
execute: async ({ amount, orderId }) => paymentApi.charge(orderId, amount),
});
// Two calls, two toolCallIds, same arguments → ONE charge.
// { a: 1, b: 2 } and { b: 2, a: 1 } are the same key; a Date and a Map are not.Argüman almayan bir araç da bu modu kullanabilir: undefined, etiket ad alanındaki ayrılmış bir belirtece özetlenir; böylece "bu araç argüman almaz" bir çökme değil, ifade edilebilir bir anahtardır. Fonksiyon veya sembol argümanı hâlâ hata fırlatır, mesajda argsHash ve tip adıyla.
API referansı#
runDurablegenerateText'in durable karşılığı: model + tool'ları exactly-once/replay sarmalayıcılarıyla çalıştırır; journal+runId alır.
durableToolTek bir tool'u exactly-once sarar; anahtar AI SDK'nın toolCallId'si. runDurable içinde otomatik uygulanır.
durableToolsBir ToolSet'in (Record<isim, tool>) tamamını durableTool ile sarar.
argsHashAraç argümanlarının sıradan bağımsız, tip etiketli özeti — idempotency: 'args' için anahtar, ve tekrar oynatmada sapmayı saptamak için başarılı kayıtla karşılaştırılan değer.
claimAtomik insert-only journal yazımı (putIfAbsent varsa onu, yoksa get+put'a düşer) — eşzamanlı resume'ların aynı tool'u çift çalıştırmasını engeller.
SideEffectRetryBlockedErrorsideEffect:true / idempotent:false işaretli bir tool'un failed kaydı, approvals[toolCallId]=true olmadan otomatik retry edilmeye çalışılırsa fırlatılır.
RetryLimitExceededErrorBir tool'un failed kaydı maxRetries (varsayılan 3) sınırına ulaştığında fırlatılır; kayıt kalıcı failed kalır.
runId ile kaç kez resume edilirse edilsin, başarıyla tamamlanmış (succeeded) bir tool çağrısının gerçek execute'i bir daha koşmaz — anahtar AI SDK'nın model tarafından üretilen toolCallId'sidir; replay aynı çıktıyı üretmeli, bu yüzden aynı ID tekrar ortaya çıkar.sideEffect: true (veya idempotent: false) ile işaretlerseniz, çöken/failed bir çağrı için otomatik retry YAPILMAZ — approvals[toolCallId] = true ile açık onay vermeniz gerekir; aksi halde SideEffectRetryBlockedError fırlatılır. Bu, çift tahsilat gibi riskleri kapatan bilinçli bir korumadır.