GNL
Core · Ücretsiz@gnldev/durable

Ajanın bir tool'u çalıştırmadan önce insana sor. Süreç ölünce cevabı kaybetme.

Sormak kolay kısım. Zor olan, soruyla cevap arasındaki saatler ve evet'ten hemen sonraki an.

Belirti#

Bir ajan bülten hazırlıyor ve 40.000 kişiye e-posta gönderen sendCampaign'i çağırıyor. Bunun bir insan evet demeden olmasını kimse istemez, o yüzden tool önce soruyor. Pazarlama sorumlusu ertesi sabah onaylıyor. Tool çalışıyor, ve konuşma kaydedilmeden hemen önce worker yeniden deploy ediliyor. Bir sonraki çağrıda ajan kayıtlı geçmişi okuyor, onaylanmış ama sonucu olmayan bir çağrı görüyor ve kampanyayı yeniden gönderiyor.

yalnızca AI SDK ile onay
import { generateText, tool } from 'ai';
import { z } from 'zod';

const sendCampaign = tool({
  description: 'Email a campaign to its whole audience',
  inputSchema: z.object({ campaignId: z.string() }),
  needsApproval: true, // the tool does not run until a person says yes
  execute: async ({ campaignId }) => mailer.sendCampaign(campaignId),
});

// Next morning: the marketing lead approved. Append the answer and call again.
messages.push({ role: 'tool', content: [{ type: 'tool-approval-response', approvalId, approved: true }] });
const result = await generateText({ model, tools: { sendCampaign }, messages }); // the campaign goes out
await db.saveMessages(messages.concat(result.response.messages));
// A redeploy before this save → the stored history still says "approved, not run"
// → the next call sends the campaign to 40,000 people again.

Neden olur#

AI SDK'nın needsApproval'ı doğru başlangıç noktası: tool çalışmaz, sonuçta bir tool-approval-request gelir. Bunu bir insana gösterirsin, onun tool-approval-response'unu mesajlara eklersin ve generateText'i yeniden çağırırsın; tool o zaman çalışır.

Aradaki her şey senin işin. Soru da cevap da yalnızca mesaj dizisinde var, onu sen saklarsın. Tool ikinci çağrının içinde çalışır, yeni mesajları da o döndükten sonra kaydedersin. Süreç bu iki an arasında ölürse kayıtlı geçmiş hâlâ onaylandı, henüz çalışmadı der ve bir sonraki çağrı onu yeniden çalıştırır.

Elle düzeltmek#

Bu boşluğu kapatınca kod, her riskli tool için yazdığın küçük bir durum makinesine dönüşür:

  • Kararın kaydı. Bekleyen onaylar için çağrıya göre anahtarlanmış, yeniden başlatmadan sağ çıkan ve eski bir isteği yenisinden ayırabilen bir tablo.
  • Tool'un çalıştığının kaydı. Etkiyle aynı adımda yazılmalı, yoksa tek bir evet iki kez kullanılabilir — düz retry'daki pencerenin aynısı.
  • Bir gelen kutusu. Birçok run'a dağılmış bekleyen onayların, bir insanın her çağrının argümanlarıyla ne yapacağını gördüğü tek bir yeri olmalı.

GNL ile#

Kapıyı tool'un üzerinde tanımla ve runDurable'ı çağır. sendCampaign'e gelen yeni bir çağrı çalışmaz; run durur ve result.interrupts hangi çağrının beklediğini argümanları ve senin yazdığın gerekçeyle söyler.

aynı ajan, dayanıklı
import { runDurable, gnlTool } from '@gnldev/durable';
import { PostgresStorage } from '@gnldev/durable/postgres';

const tools = {
  // the same tool without needsApproval — confirm takes its place
  sendCampaign: gnlTool(sendCampaign, {
    sideEffect: true,
    confirm: { reason: (args: { campaignId: string }) => `send campaign ${args.campaignId} to its audience?` },
  }),
};
const journal = new PostgresStorage({ connectionString: process.env.DATABASE_URL }).runs;
const runId = `campaign:${campaignId}`; // the id of THIS job — never a session id

const first = await runDurable({ runId, journal, model, tools, prompt });
// first.interrupts → [{ toolCallId, toolName: 'sendCampaign', args, reason }] — show it to a person

// Hours later, in any process, with the toolCallId the person answered:
await runDurable({ runId, journal, model, tools, prompt,
  approvals: { [toolCallId]: true } }); // false = deny

Cevap saatler sonra, başka bir süreçten gelebilir: aynı runId ve approvals içindeki kararla runDurable'ı yeniden çağır. Biten adımlar journal'dan okunur, onaylanan çağrı bir kez çalışır ve ajan devam eder.

Karar senin mesaj dizine değil journal'a yazılır. Süreç evet'ten sonra ve tool çalışmadan önce ölürse, bir sonraki çağrı kayıtlı kararı yeniden söylenmesine gerek kalmadan uygular. Tool'un sonucu kaydedildikten sonra hiçbir çağrı onu bir daha çalıştırmaz.

Askıdaki bütün run'ların onayları tek bir yerde toplanır: Studio'nun gelen kutusu tool'u, durdurduğu run'ı ve çalışacağı argümanların tam halini gösterir.

Not
Hayır da bir cevap. approvals içinde çağrı için false verirsen ret kaydedilir; tool hiç çalışmaz ve model izin verilmediğini okur, böylece kullanıcıya söyleyebilir.

GNL'in çözmediği durumlar#

Çağrının içindeki çökme. Süreç sendCampaign çalışırken ölürse e-postaların çıkıp çıkmadığını kimse bilmez. GNL tahmin etmez: devam etme SideEffectRetryBlockedError ile durur, ya da tool'unun recover()'ı e-posta sağlayıcısına sorar.

Kim onaylayabilir. GNL kararı kaydeder; bu kişinin o kararı verme yetkisi olup olmadığı senin uygulamanın kontrolüdür.

Bir evet, bir çağrı. Onay tek bir tool çağrısına aittir. Model kampanyayı yeni bir çağrı olarak yeniden planlarsa yeniden sorar. Varsayılan olarak kaydedilmiş bir evet çökmeden sağ çıkar; her denemenin yeniden sorması gerekiyorsa limits.approvalScope: 'attempt' kullan.

Kaynaklar#

Bu sayfadaki her iddianın herkese açık repoda bir testi var:

  • packages/durable/test/approval-persistence.test.ts — onaylandı, tool çalışmadan çöktü, onay verilmeden devam edildi: tool bir kez çalışır; ret de kalıcıdır
  • packages/durable/test/suspend-cross-process.test.ts — bir süreç askıya alır, başka biri bekleyen çağrıyı okuyup onaylar, tool bir kez çalışır
  • packages/durable/test/faz3-confirm-thread.test.ts — confirm kapısı: ilk çağrı askıya alınır, onay tam bir kez çalıştırır, ret etkiyi hiç tetiklemez
  • packages/durable/test/approval-scope.test.ts — journal'a yazılmış onay ile tek bir denemede harcanan onay

Mekanizmanın tamamı: human-in-the-loop onayları. github.com/gnlhq/gnldev