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

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#

npm / pnpm
pnpm add @gnldev/durable ai

Journal 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
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):

exactly-once — çökme + resume
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.

aynı argümanlar, tek yürütme
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ı#

fnrunDurable

generateText'in durable karşılığı: model + tool'ları exactly-once/replay sarmalayıcılarıyla çalıştırır; journal+runId alır.

fndurableTool

Tek bir tool'u exactly-once sarar; anahtar AI SDK'nın toolCallId'si. runDurable içinde otomatik uygulanır.

fndurableTools

Bir ToolSet'in (Record<isim, tool>) tamamını durableTool ile sarar.

fnargsHash

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

fnclaim

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

classSideEffectRetryBlockedError

sideEffect:true / idempotent:false işaretli bir tool'un failed kaydı, approvals[toolCallId]=true olmadan otomatik retry edilmeye çalışılırsa fırlatılır.

classRetryLimitExceededError

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

Garanti
Aynı 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.
Dikkat
Yan-etkili (idempotent olmayan) bir tool'u 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.