GNL
Core · Ücretsiz@gnldev/durable

Ajanın yarı yolda çöktü. Kaldığı yerden devam ettir.

Yeniden başlatma, hiç bitmemiş ilk adımdan devam etmeli; modeli ve biten tool'ları baştan çalıştırmamalı. Bu sayfa generateText'in bunu neden yapamadığını, elle checkpoint'in neyi kaçırdığını ve bunu yapan değişikliği gösteriyor.

Belirti#

Bir destek ajanı tek bir mesajı üç adımda işliyor: müşteriyi buluyor, bir ticket açıyor, sonra müşteriye ticket numarasını mailliyor. Süreç ticket açıldıktan sonra, mail gitmeden önce ölüyor. İş yeniden çalıştığında ajan ilk adımdan başlıyor: müşteriyi yeniden arıyor ve model ticket açıp açmayacağına yeniden karar veriyor.

üç tool'lu düz bir AI SDK ajanı
import { generateText, tool, stepCountIs } from 'ai';
import { z } from 'zod';

const tools = {
  lookupCustomer: tool({
    description: 'Find the customer',
    inputSchema: z.object({ email: z.string() }),
    execute: async ({ email }) => crm.find(email),
  }),
  createTicket: tool({
    description: 'Open a support ticket',
    inputSchema: z.object({ customerId: z.string(), subject: z.string() }),
    execute: async (input) => desk.open(input),
  }),
  sendEmail: tool({
    description: 'Email the customer',
    inputSchema: z.object({ to: z.string(), body: z.string() }),
    execute: async (input) => mailer.send(input),
  }),
};

// A queue worker runs this for each incoming message.
await generateText({ model, tools, stopWhen: stepCountIs(10), prompt: job.message });
// The process dies after createTicket, before sendEmail. The retry starts from
// lookupCustomer, and the model decides again whether to open a ticket.

Neden oluyor#

generateText'in tur hakkında bildiği her şey (modelin cevapları, tool sonuçları, hangi adımda olduğu) bellekte durur. Süreç ölünce hepsi onunla gider. Sonraki deneme elinde sadece prompt olan yeni bir çağrıdır; model en baştan yeniden sorulur ve parası yeniden ödenir.

Ticket'a ne olacağı modele kalmıştır: ikinci bir ticket açabilir ya da hiçbir şey fark etmeyip maili atlayabilir. Her iki durumda da, olmuş olan yarı, olmamış yarıya görünmez.

Elle checkpoint#

Bilinen çözüm her adımdan sonra konuşmayı kaydetmek ve sonraki denemeyi kaydedilmiş mesajlardan başlatmak.

elle yazılmış bir checkpoint
const saved = await db.loadMessages(job.id); // [] on the first attempt

await generateText({
  model, tools, stopWhen: stepCountIs(10),
  messages: [{ role: 'user', content: job.message }, ...saved],
  onStepFinish: async (step) => {
    // runs AFTER the step's tools — a crash before this line loses the step
    await db.saveMessages(job.id, step.response.messages);
  },
});

Yolun büyük kısmını götürür, ama üç şeyi kaçırır:

  • Çöken adım. onStepFinish adımın tool'larından sonra çalışır. Süreç createTicket çalıştıktan sonra, adım kaydedilmeden önce ölürse kayıtlı mesajlarda ticket görünmez ve retry onu yeniden açar.
  • İki işçi, tek iş. İlk işçi hâlâ ayaktayken işi yeniden teslim eden bir kuyruk, aynı konuşmayı aynı anda devam ettiren iki ajan demektir ve ikisi de tool'ları çalıştırır.
  • Tool'lar hâlâ korumasız. Mesajları geri yüklemek, yan etkili bir tool'u iki kez çalıştırılabilir hale getirmez; her birinin yine kendi korumasına ihtiyacı var.

GNL ile#

İşe bir kimlik ve bir journal ver, generateText'i çağırdığın yerde runDurable'ı çağır. İlk deneme de her retry de birebir aynı kodu çalıştırır.

kuyruk işleyicisi, dayanıklı
import { runDurable } from '@gnldev/durable';
import { PostgresStorage } from '@gnldev/durable/postgres';

const journal = new PostgresStorage({ connectionString: process.env.DATABASE_URL! }).runs;

// The queue handler: the first attempt and every retry run this same call.
export async function handle(job: { id: string; message: string }) {
  return runDurable({
    runId: `support:${job.id}`,   // the id of THIS job — never a session id
    journal,
    model, tools, stopWhen: stepCountIs(10),
    prompt: job.message,
  });
}

Retry'da modelin kayıtlı cevapları yeniden istenmek yerine journal'dan oynatılır; biten adımlar hiçbir şeye mal olmaz. lookupCustomer ve createTicket kayıtlı sonuçlarını döndürür ve çalışmaz. Ajan hiç bitmemiş adıma, yani mail göndermeye ulaşır ve onu canlı çalıştırır.

Aynı işi alan iki işçi bir tool'u iki kez çalıştırmaz: her tool çağrısı çalışmadan önce journal'da atomik olarak sahiplenilir, gövdesi bir kez koşar. Çalıştırma kilidi varsa ikinci işçi doğrudan RunBusyError ile reddedilir.

Streaming de aynı şekilde çalışır: streamDurable aynı runId'yi ve journal'ı alır, aynı koşuyu devam ettirir.

Not
Bir id bir iştir, asla bir oturum değildir. runId'yi konuşmadan değil işten türet (support: ve işin id'si). Oturum id'si vermek, sonraki her mesajın ilkini tekrar oynatmasına yol açar.

GNL'in çözmediği yerler#

Tool çağrısının içindeki çökme. Süreç createTicket çalışırken öldüyse GNL ticket'ın var olup olmadığını bilemez. Tahmin yürütmez: runDurable SideEffectRetryBlockedError fırlatır ve kararı bir insan verir, ya da tool'un recover()'u ticket sistemine sorar.

Çökme noktasından sonra model canlı çalışır. Replay kaydedileni yeniden üretir; hiç bitmemiş ilk adımdan itibaren model yeniden çağrılır ve farklı seçebilir, o kısmı hiçbir şey denetlemez, çünkü karşılaştırılacak bir kayıt yoktur. Kayıtlı kısım için replay: 'strict', tool argümanları kayıtla artık uyuşmayan bir oynatılan adımı sessizce devam etmek yerine DivergenceError'a çevirir.

Onayladığı yazıyı kaybeden depolama. Devam etmek ancak journal'ın tuttuğundan olabilir. Failover'lı Postgres'te senkron replikasyon kullan.

Kaynaklar#

Bu sayfadaki her iddianın herkese açık depoda bir testi ya da çalıştırılabilir bir örneği var:

  • packages/durable/test/crash-window.test.ts — yan etki ile journal yazımı arasındaki çökme ve devam etmenin onunla ne yaptığı
  • packages/durable/test/multi-worker.test.ts — aynı koşuyu devam ettiren iki işçi, tool çağrısı başına tek çalıştırma
  • packages/durable/test/stream-crash-window.test.ts — aynı çökme, streaming sırasında
  • examples/incident-proofs — bağlantı koptuktan sonra checkpoint'ten yeniden gönderilen bir tool çağrısı, yeniden üretilmiş ve engellenmiş, API anahtarı gerekmez

Mekanizmanın tamamı: deterministik replay. Aynı sorunun tekrar yarısı: AI SDK tool'um retry sonrası iki kez çalıştı. github.com/Karaca7/gnldev