GNL 0.17: erken aşamada ve hızlı ilerliyor — API'ler sürümden sürüme değişebilirBize katıl

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.

Mekanizmadan değil de yaşadığın sorundan mı geliyorsun? AI SDK tool'um retry sonrası iki kez çalıştı aynı konuyu sorun tarafından anlatıyor.

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.

idempotencyKey ile mantıksal bir kimliğe göre anahtarlanan araç, anahtarın ne kadar geçerli olduğunu da idempotencyWindow ile söylemelidir: 'run' — bu istekte bir kez, yeni bir istek yeniden yapar; 'cross-run' — tüm istekler için bir kez, purge edilene kadar; 'thread' — bu konuşmada bir kez. Pencere yoksa araç tanımlandığı (gnlTool) ya da sarıldığı (durableTool) yerde reddedilir ve hata aracın adını verir. 0.10'dan önce pencere varsayılan olarak 'run' idi; bu yüzden sonraki istekte yeniden istenen aynı işlem yeniden çalışıyordu.

bir anahtar ve penceresi
import { gnlTool } from '@gnldev/durable';
import { tool } from 'ai';
import { z } from 'zod';

const tools = {
  // Once for every request, until purged: asking again in a later request does not charge again.
  chargeOrder: gnlTool(
    tool({ description: 'Charge an order', inputSchema: z.object({ orderId: z.string() }), execute: chargeCard }),
    { sideEffect: true, idempotencyKey: (input) => input.orderId, idempotencyWindow: 'cross-run' },
  ),
  // Once in this request (the agent and every sub-agent under it); a new request sends it again.
  sendReceipt: gnlTool(
    tool({ description: 'Send a receipt', inputSchema: z.object({ orderId: z.string() }), execute: emailReceipt }),
    { sideEffect: true, idempotencyKey: (input) => input.orderId, idempotencyWindow: 'run' },
  ),
};

// Without idempotencyWindow, gnlTool throws a TypeError where the tool is declared:
// @gnldev/durable: gnlTool: the tool "Charge an order" has an idempotencyKey but no
// idempotencyWindow — say how far the key holds. ...

Run penceresi isteğe aittir: anahtarlı ya da idempotency: 'args' kullanan bir çağrı; ajan, her alt-ajan ve altındaki her seviye boyunca tekilleştirilir. Anahtarı modelin yazdığı metinden değil, kararlı kimliklerden (bir id, bir tarih) kurun.

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 aracı exactly-once semantiğiyle sarar; anahtar AI SDK'nın toolCallId'sidir. runDurable içinde otomatik uygulanır. idempotencyKey'i olup idempotencyWindow'u olmayan bir aracı, aracın adını vererek reddeder.

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.