Dayanıklı bir yapay zekâ ajanı kurun
Tek proje, boş bir klasörden gerçekten dağıtacağınız bir şeye: runbook'larınızı okuyan, production'a dokunmadan önce insana soran ve kaç kez yeniden denenirse denensin bir servisi iki kez yeniden başlatamayan bir on-call ajanı. Gerçek model, gerçek veritabanı, bölüm başına bir paket — senaryo hak ettikçe.
Bölümler
GNL nedir?
GNL, yalnızca metin üretmek yerine bir şeyler yapmasına izin verilen yapay zekâ ajanları için açık kaynak bir TypeScript çatısı — bir servisi yeniden başlatmak, e-posta göndermek, bir kaydı taşımak gibi.
Doğrudan Vercel AI SDK'nın üstünde durur: araçlarınız sıradan AI SDK araçları, modeliniz sıradan bir AI SDK modeli; yani elinizdeki hiçbir şey yeniden yazılmıyor. GNL'nin eklediği şey bir günlük — bir çalıştırmanın attığı her adımın kalıcı kaydı; "bu ajan genelde düzgün davranır"ı adlandırabildiğiniz, test edebildiğiniz ve bilerek kırabildiğiniz garantilere çeviren şey bu.
Dayanıklı yürütme nedir?
Normal bir ajan çalıştırması tamamen bellekte yaşar. Süreç yarı yolda ölürse — bir dağıtım, bir OOM, kopan bir bağlantı — yaptığı her şey gider, ve alışılmış çözüm onu baştan çalıştırmaktır.
Ajan yalnızca metin ürettiği sürece bunun sakıncası yok. Ortadaki bir adım gerçekten bir şey yaptığı anda sakıncası başlar: yeniden çalıştırmak bir düşünceyi tekrarlamaz, bir eylemi tekrarlar.
Dayanıklı yürütme, her adımın tamamlandığı anda yazılması demek. Yeniden çalıştırma o kaydı okur ve durduğu yerden devam eder; böylece zaten olmuş iş ikinci kez yapılmaz.
Neden özellikle bir yapay zekâ ajanı
Çünkü neyin çağrılacağına model karar verir ve modeller deterministik değildir. Aynı istek, yepyeni bir çağrı kimliği altında aynı araç çağrısını iki kez üretebilir — belgelenmiş bir AI SDK davranışı, egzotik bir uç durum değil. Yeniden denemeler, kuyruk yeniden dağıtımları ve operatörün iki kez tıklaması da aynı yere düşer.
Dayanıklı yürütme genelde bir ödemeyle anlatılır: kartı iki kez çekme. Takip etmesi kolay, ve biraz sahtekârca — çift çekim geri alınabilir. İade edersiniz ve özür dilersiniz.
Biz geri alınamayan bir şey kuracağız. Ajanımız bir production servisini yeniden başlatabiliyor; ve bir olay sırasında bir servisi iki kez yeniden başlatmak, çağrıldığınız kesintinin üstüne ikinci bir kesinti demek, geri verecek hiçbir şey olmadan.
1Projeyi oluşturun
GNL bir iskeletleyiciyle geliyor ve yazmadan önce soruyor. Tek komut eksiksiz, çalışan bir proje veriyor — gnl.config.ts, iki ajan, yan etkili bir araç ve o aracın iki kez çalışamadığını kanıtlayan bir test. API anahtarı gerekmiyor: başlangıç modeli deterministik bir taklit, yani aşağıdaki her şey kendi makinenizde oluyor.
npm create gnl@latest oncall-agent
cd oncall-agent
pnpm installDört soru, ve neden yalnız dört
Kapıda Let me choose derseniz dört şey sorulur. Bunlar tercih değil: her biri sonradan değiştirmesi pahalı kararlar — bu yüzden şimdi soruluyorlar ve bu yüzden başka hiçbir şey sorulmuyor. Özellikler ihtiyaç duyduğunuz gün ekleniyor (gnl add …); atlamış bir proje hiçbir şey kaybetmiyor. --yes ya da terminal olmaması önerilen cevapları alır ve asla beklemez.
gnl init — If the same work arrives twice, what should happen?
◉ Ask me — someone is there to decide (tekrar bir soruya dönüşür)
◯ Refuse it — this runs unattended (soracak kimse yok, reddedilir)
◯ Refuse it, and never let two copies race (+ kilit, parmak izi, mezar taşı)
gnl init — Who does each run belong to? (koşumların sahibi var mı)
gnl init — Where should the record of every run be kept? (dosya mı, Postgres mi)
gnl init — How will people reach this? (gnl dev · kendi sunucusu · mevcut sunucu)Ne alıyorsunuz
İlk dosyadan itibaren her kavram için bir klasör — böylece “ikinci ajanı nereye yazayım?” sorusu hiç doğmuyor. Klasörler ihtiyaç doğdukça beliriyor (workflows/, models/, jobs/); hiçbiri boş oluşturulmuyor.
oncall-agent/
gnl.config.ts ajanlar, araçlar ve korumaların bağlandığı tek yer
src/agents/assistant.ts sohbet ajanı (10 satır)
src/agents/charge-demo.ts modeli aşağıdaki aracı çağırıyor
src/tools/charge-order.ts GERÇEK yan etki: idempotency: 'args'
test/proof.test.ts 3 tekrar eden çağrı → tek tahsilatBaşlatın. gnl dev size tek portta bir REST API, bir OpenAPI dokümanı ve Studio denetleyicisini veriyor — kendinize ait hiçbir sunucu dosyası olmadan:
Bir değil iki ajan kayıtlı: assistant yankıyla cevap veriyor, charge-demo ise para hareket ettiren bir aracı çağırıyor. İkincisi, bir sonraki bölümün size güvenmenizi isteyen bir iddia olmaması için var.
$ pnpm dev gnl dev → REST http://localhost:3000 (auth: open) OpenAPI http://localhost:3000/openapi.json Studio http://localhost:3000/studio (Playground) $ curl -XPOST localhost:3000/agents/assistant/run \ -H 'content-type: application/json' \ -d '{"runId":"r1","prompt":"hello"}'{"ok":true,"runId":"r1","text":"echo: hello","interrupts":[]}İki dakika sonra: bir tekrarın reddedilişini izleyin
Tüm iddia bu ve tek satır kod yazmadan koşturulabiliyor. Projeyle gelen test, belgelenmiş bir AI SDK desenini yeniden üretiyor — model aynı araç çağrısını tek turda üç kez, her seferinde farklı bir id ile yapıyor — ve tahsilatın bir kez olduğunu doğruluyor.
$ pnpm test
✓ one order, 3 duplicate tool-calls in one turn → charged exactly once
✓ crash-resume replays instead of re-charging
$ curl -si localhost:3000/agents/charge-demo/run \
-H 'content-type: application/json' \
-d '{"workKey":"order-1","resourceId":"me","prompt":"charge order 1"}' | grep x-gnl
x-gnl-idempotency-status: new
$ # birebir aynı komut, ikinci kez
x-gnl-idempotency-status: replay ← model çağrılmadı; kayıt cevapladı
$ # aynı workKey, farklı içerik
409 run_input_mismatch ← bir workKey bir iştir2Journal — her koşumun kaydı
Journal çerçevenin kendisi: yeniden denenen bir koşumun tekrar etmek yerine replay etmesini sağlayan, askıya alınmış bir koşumu insan beklerken tutan ve bir tekrarın karşılaştırıldığı şey. Bunu kapıda zaten cevapladınız — In a file here gnl.config.ts'e bir SQLite satırı yazdı ve hiçbir şey kurulması gerekmedi. Postgres için üçüncü soruyu farklı cevaplayın (ya da bayrağı geçin):
npm create gnl@latest oncall-agent --store pg// gnl.config.ts — `gnl init` yazdı, elle değil.
import { PostgresStorage } from '@gnldev/durable/postgres';
export default {
// Postgres, because you said the journal lives in one. `DATABASE_URL` is read at startup and
// is NOT defaulted — a journal that silently falls back to a local file is a journal you
// discover is empty in production.
storage: new PostgresStorage({ connectionString: process.env.DATABASE_URL! }),
agents: { assistant, 'charge-demo': { ...chargeDemo, tools: { chargeOrder } } },
preset: 'assistant',
};Tek storage nesnesi, beş port — runs, memory, vectors, work, cache — aynı veritabanında ve aynı işlem sınırında. Her adaptör aynı arayüzü uyguluyor, yani yukarıdaki satır dizüstündeki bir dosya ile bir küme arasındaki tek fark.
Göç adımı yok: şema ilk kullanımda oluşuyor. Sonradan yeniden yazım da yok — dosyadan Postgres'e geçmek bu tek satır. Sorunun doksanıncı günde keşfedilmek yerine ilk gün sorulmasının sebebi tam olarak bu.
$ docker run -d --name pg -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=agent -p 5432:5432 postgres:16-alpine$ export DATABASE_URL=postgres://postgres:pw@localhost:5432/agent $ # after the first run, the schema is there:$ psql $DATABASE_URL -c '\dt' public | gnl_cache | table public | gnl_counters | table public | gnl_messages | table public | gnl_run_journal | table public | gnl_runs | table public | gnl_threads | table public | gnl_vectors | table3Gerçek model — tek komut
Başlangıç projesi bilinçli olarak yankıyla cevap veriyor: anahtar yok, hesap yok, çalıştığını görmeden hiçbir yere kaydolmuyorsunuz. Gerçek bir sağlayıcıya geçmek tek komut — dosyayı yazıyor, bağımlılığı ekliyor ve değiştireceğiniz tek import satırını söylüyor:
npx gnl add model nvidia # ya da: openai · anthropic · openai-compatible
pnpm install// src/models/nvidia.ts — `gnl add model nvidia` yazdı.
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
const nim = createOpenAICompatible({
name: 'nvidia',
baseURL: 'https://integrate.api.nvidia.com/v1',
apiKey: process.env.NVIDIA_API_KEY ?? '',
});
export const model = nim(process.env.NVIDIA_MODEL ?? 'poolside/laguna-xs-2.1');
// sonra src/agents/assistant.ts içinde iki satır:
// import { model } from '../models/nvidia.js';
// …ve config nesnesinde: model, (echoModel() yerine)src/models/nvidia.ts dosyasını ve o dosyanın okuduğu değişkenleri adlandıran bir .env.example yazıyor. Anahtarı .env'e koyun: gnl dev ve gnl studio açılışta okuyor, shell'de zaten tanımlı olan her zaman kazanıyor. .env gitignore'lu; .env.example değil.
nvidia(id) değil .chat(id) kullanın — çıplak çağrı RESPONSES modelini döner, NVIDIA onu sunmaz; istek bir function id mesajıyla 404 verir ve sizin kodunuzda bir hata gibi okunur. Ve yönlendirme öneki TÜM kimliği sarar: router ilk eğik çizgiden böler ve gerisini olduğu gibi geçirir, dolayısıyla kimlikleri zaten nvidia/ ile başlayan NVIDIA'nın kendi modelleri için önek iki kez gerekir: nvidia/nvidia/nemotron-…. String'i yukarıdaki gibi tek yerde kurmak, bunu kimsenin hatırlamak zorunda kalmaması demek.4Araçlar — ajanın yapabildikleri
Aracı olmayan bir ajan bir sohbet uç noktasıdır. Bizimki iki tane alıyor, ve aralarındaki fark sonraki dört bölümün konusu.
pnpm add ai zod// src/tools.ts
import { tool } from 'ai';
import { z } from 'zod';
/** Stands in for the fleet, so the example can count what actually happened. */
export const fleet = { restarts: [] as string[] };
/**
* The one thing this agent can DO — and the reason the framework is here.
*
* Restarting a service twice is two outages. Unlike a double charge there is nothing to refund,
* which is why "it probably won't happen twice" is not a design.
*/
export const restartService = tool({
description: 'Restarts a service. Disruptive.',
inputSchema: z.object({ service: z.string() }),
execute: async ({ service }) => {
fleet.restarts.push(service);
return { service, restarted: true };
},
});Önce bir şeyi değiştiren araç:
/**
* A read — and the source of every secret in this tutorial.
*
* An agent that can act on a service is an agent that reads that service's logs, and real log lines
* carry connection strings and customer addresses.
*/
export const readLog = tool({
description: 'Reads the tail of a service log.',
inputSchema: z.object({ service: z.string() }),
execute: async ({ service }) => ({
service,
lines: [
'INFO boot: connected postgres://svc:hunter2@db-primary/orders',
'INFO request: customer [email protected] placed order',
'ERROR gc: heap 1.9GB / 2.0GB',
],
}),
});
// Reading a log twice is harmless, so this opts OUT of the default side-effect treatment.
// Part 7 measures exactly what that changes. Never put it on a tool that writes.
Object.assign(readLog, { idempotent: true });
export const tools = { restartService, readLog };Import'lara dikkat: ai'dan tool, zod'dan z — GNL'den değil. Bu sıradan bir Vercel AI SDK aracı. Projenizde zaten araçlar varsa, hâlihazırda bu şekildeler ve taşınmaya ihtiyaçları yok.
Sonra yalnızca okuyan araç — ve bu tutorial'daki her sırrın kaynağı:
5Ajan ve system prompt'u
createGnl depolamayı ve bir ajan tanımları haritasını alır — ama onu nadiren doğrudan çağırırsınız. Önce config'i yazın:
// src/agent.ts — the config, and nothing else.
//
// Note what this file does NOT do: it never calls the agent. It describes one. Who calls it is the
// next file's problem, and for HTTP traffic the answer is "the framework does".
import type { CreateGnlConfig } from '@gnldev/durable';
import { storage } from './storage.js';
import { MODEL } from './model.js';
import { tools } from './tools.js';
export const config: CreateGnlConfig = {
storage,
agents: {
ops: {
model: MODEL,
tools,
// The system prompt is the agent's JOB DESCRIPTION — the one place you say what it is for.
// Everything you add from here constrains what it MAY DO. They are not substitutes: a prompt
// asking the model nicely not to restart things is not a guard.
system:
'You are an on-call engineer. Read the service log before acting, and never restart a ' +
'service unless the log shows it is genuinely stuck.',
// A budget, not a target: caps how many tool-call rounds one run may take, which is what stops
// a confused model looping.
maxSteps: 4,
},
},
};Şimdi o config'i @gnldev/server'a verin, bitti. createRestApi içeride createGnl'i çağırıyor ve config'teki her ajanı HTTP üzerinden sunuyor — istek trafiği için gnl.run() hiç yazmıyorsunuz:
// src/server.ts — mount it. This is the whole integration.
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { createRestApi } from '@gnldev/server';
import { config } from './agent.js';
const app = new Hono();
app.mount('/api', createRestApi(config, { title: 'On-call', allowOpenAccess: true }));
serve({ fetch: app.fetch, port: 3000, hostname: '127.0.0.1' });System prompt ajanın iş tanımıdır — ne için olduğunu söylediğiniz tek yer. Buradan sonra eklediğiniz her şey ne yapabileceğini kısıtlar. Birbirinin yerine geçmezler: modelden bir şeyleri yeniden başlatmamasını kibarca isteyen bir prompt bir guard değildir, ve bir guard modele işinin ne olduğunu söylemez.
$ curl -XPOST localhost:3000/api/agents/ops/run \ -H 'content-type: application/json' \ -d '{"runId":"triage:INC-42","prompt":"The checkout service looks stuck. Investigate and fix it."}' {"ok":true,"runId":"triage:INC-42", "text":"The checkout service has been restarted. The issue was clear from the logs: **Problem Identified:** The service was using 1.9GB of a 2.0GB heap…", "interrupts":[],"finishReason":"stop"}O cevap, canlı bir modelin kendi başına önce logu okumaya sonra servisi yeniden başlatmaya karar vermesinden geldi — HTTP üzerinden, kodunuzun hiçbir yerinde gnl.run() olmadan.
6runId seçmek
O istek gövdesinde runId'yi gördünüz. API'deki en önemli argüman, ve çatının sizin yerinize karar vermeyeceği tek şey.
Aynısını iki kez gönderin, ikincisi hiçbir şeye mal olmasın — araç çağrısı yok, modele danışılmıyor. REST yolunda atlarsanız 400 alırsınız:
Yirmi bir saniye, sonra bir saniyenin altı. Bunu elde etmek için hiçbir şey yapılandırmadınız; günlüğün kendisi bu. Kimliği değiştirin, ajan doğru şekilde tekrar çalışır — günlük aynı çalıştırmanın tekrarını tekilleştirir, bir aracı susturmaz.
$ # POST it once — the model runs, the tools run$ curl -XPOST localhost:3000/api/agents/ops/run -d '{"runId":"triage:INC-42","prompt":"…"}'{"ok":true,"runId":"triage:INC-42","text":"The checkout service has been restarted…"}real 0m21.4s $ # POST the SAME runId again — the journal answers. No model call, no tool call.$ curl -XPOST localhost:3000/api/agents/ops/run -d '{"runId":"triage:INC-42","prompt":"…"}'{"ok":true,"runId":"triage:INC-42","text":"The checkout service has been restarted…"}real 0m0.852s $ # Omit it entirely:$ curl -XPOST localhost:3000/api/agents/ops/run -d '{"prompt":"…"}'400 {"error":"runId required (idempotency key)"}Kimin verdiği, işin hangi kapıdan geldiğine bağlı:
useChat → POST /chat derived `${body.id}:${lastMessage.id}` POST /api/agents/:name/run REQUIRED 400 without it POST /api/workflows/:name/run REQUIRED and the ONLY way to resume one gnl.run() from your own code REQUIRED queue workers, workflow steps, cron7Başarısızlık, tekrar oynatma değildir
Tekrar oynatma başarılı olmuş adımı kapsar. Zor olan yarı yolda başarısız olandır: yeniden başlatma gerçekleşti, sonra onay kaybedildi. Dışarıdan bu bir başarısızlık gibi görünür, ve naif bir yeniden deneme ikinci kesintiye yol açar.
GNL tam da bu yüzden her aracı yan etkili sayar ve başarısız olmuş birini yeniden çalıştırmayı reddeder. Araca sideEffect: true iliştirerek bunu açıkça bildirebilirsiniz — ama önce ölçümü okuyun:
sideEffect: true yazmak hiçbir şeyi değiştirmiyor. Zaten varsayılan: çözüm tool.sideEffect ?? tool.idempotent !== true, yani aksini söylemedikçe her şey yan etkili. Davranışı değiştiren bildirim bunun tersi — readLog'a koyduğumuz idempotent: true — ki bir aracı yeniden denenmeye dahil eder.
$ # a tool that FAILS after its side effect already landed, retried once: declared nothing executions: 1 ← refused on retry declared sideEffect executions: 1 ← identical: it was already the default declared idempotent executions: 2 ← opted IN to being repeatedBunu bir bilgi olarak değil bir alışkanlık olarak tutun: güvenli davranış varsayılandır, ve yazdığınız şey istisnadır. Size "exactly-once elde etmek için" bir satır eklemenizi söyleyen bir tutorial, hiçbir şey yapmayan bir satıra güvenmeyi öğretmiş olurdu.
8Guard'lar — insanın karar verdiği an
Dayanıklılık bir eylemin iki kez olmasını durdurur. Hiç olması gerekip gerekmediği hakkında bir şey söylemez, ve production'ı yeniden başlatmak bir insanın verdiği karardır.
// src/agent.ts — add the guard
//
// A guard runs before EVERY tool call and answers allow | require-approval. This one stops exactly
// one tool: a gate on a read costs an engineer's attention and buys nothing, and a system that asks
// about everything is a system people learn to click through.
export const guard = ({ toolName, args }: any) =>
toolName === 'restartService'
? { action: 'require-approval' as const, reason: `restarting ${args?.service} interrupts live traffic` }
: { action: 'allow' as const };
export const gnl = createGnl({
storage,
agents: { ops: { model: MODEL, tools, guard, system: '…', maxSteps: 4 } },
});
/** Continues a suspended run once a human has answered. */
export const resume = (runId: string, approvals: Record<string, boolean>) =>
resumeRun(runId, { journal: storage.runs, model: MODEL, tools, guard, stopWhen: stepCountIs(4), approvals });Artık çalıştırma aracı çağırmak yerine askıya alınıyor, ve ne yapmak istediğini, neden durduğunu anlatan bir kesintiyle dönüyor:
const r = await gnl.run('ops', { runId: `triage:${incidentId}`, prompt });
if (r.interrupts.length) {
const it = r.interrupts[0];
console.log(`⏸ ${it.toolName} — ${it.reason}`);
// In a real app this is an HTTP endpoint a human hits (Part 19), not a line below the run.
// The point of suspending is that the answer does not have to come from this process, or this hour.
const done = await resume(`triage:${incidentId}`, { [it.toolCallId]: true });
}Askıya almak göründüğünden önemli. Onay bir dakika da sürebilir bir saat de, ve bir saat boyunca soket açık tutan bir süreç, cevap gelmeden yeniden dağıtılacak bir süreçtir. Çalıştırma bellekte beklemiyor — yazılmış durumda, ve resume onu sonradan, muhtemelen bambaşka bir süreçte alıp devam ettiriyor.
$ pnpm start INC-42 ⏸ waiting for approval: restartService — restarting checkout interrupts live traffic the run is written down, not held in memory. Approve it whenever, from anywhere. ✅ approved: The checkout service has been restarted…9İşlemciler — modelin görebilecekleri
Guard'lar ajanın ne yapabileceğini kısıtlar. İşlemciler ne görebileceğini — ve log okuyan bir ajanın onlara ihtiyacı var, çünkü o satırlar bağlantı dizeleri ve müşteri adresleri taşıyor.
pnpm add @gnldev/processors// src/agent.ts — what the model is allowed to SEE
import { piiRedactor } from '@gnldev/processors';
export const gnl = createGnl({
storage,
processors: [
piiRedactor({
// Off by default because it changes what gets written to the journal. For an agent whose job
// is reading logs, it is the entire point.
redactToolResults: true,
// The built-in set is PII — email, phone, card, IBAN, SSN, IP. A database password is a
// CREDENTIAL, not PII, and is measurably not covered. This closes that, and keeps the HOST
// readable: at 3am "which database" is the question, and [REDACTED] answers none of it.
extraPatterns: [
{ name: 'dsn', pattern: /(?<=:\/\/)[^\s:@/]+:[^\s@/]+(?=@)/g, mask: '[REDACTED_DSN]' },
],
}),
],
agents: { ops: { model: MODEL, tools, guard, system: '…', maxSteps: 4 } },
});Yerleşik küme kişisel veridir — e-posta, telefon, kart, IBAN, SSN, IP. Bir veritabanı parolası kimlik bilgisidir, kişisel veri değil, ve ölçülebilir şekilde kapsanmıyor. Modeli dinleyin ve sayın:
Yalnızca varsayılanlarla hunter2 modele ulaşıyor, yanındaki adres ise doğru şekilde maskeleniyor. Üçüncü satıra da dikkat edin: desen host'u koruyor, çünkü sabaha karşı gerçekten sorduğunuz soru "hangi veritabanı". Tüm URL'yi maskeleyen bir kural ilk iki kontrolü geçer ve izi okuyan insana hiçbir işe yaramazdı.
$ # wiretap the model and count what actually reaches it: without processor — password visible : true with processor — password visible : false with processor — host still readable : true10Sağlayıcı şema uyumu
Tek satır, ve olmadığı güne kadar görünmez:
pnpm add @gnldev/tool-schema// src/agent.ts
import { defaultRules } from '@gnldev/tool-schema';
export const gnl = createGnl({
storage,
processors: [...],
// Rewrites each tool's JSON Schema for whichever provider is actually underneath. Invisible until
// the day you point this same config at a provider in strict mode and a schema shape it silently
// rejects takes the agent down.
schemaCompat: defaultRules,
agents: { ops: { model: MODEL, tools, guard, system: '…', maxSteps: 4 } },
});defaultRules her aracın JSON Schema'sını altta gerçekte hangi sağlayıcı varsa ona göre yeniden yazar. Aynı yapılandırmayı strict mode çalıştıran bir sağlayıcıya yönlendirin, ve sessizce reddettiği bir şema şekli tüm ajanı indirsin. Bunu önleyen satır bu — öğrendiğiniz olayın ortasında değil, tek ajanınız varken eklemeye değer.
11Gömmeler (embeddings)
Sonraki iki bölümün ikisi de "ilgili şeyi anlamla bul" istiyor: "RSS yükseliyor" diye okunan bir alarmla "bellek doygunluğu" başlıklı bir runbook tek kelime paylaşmaz, ve alt-dize araması hiçbir şey bulmaz.
pnpm add @gnldev/memory// src/embed.ts — one function from string to vector, used by memory AND retrieval.
const EMBED_MODEL = process.env.NVIDIA_EMBED_MODEL ?? 'nvidia/nemotron-3-embed-1b';
/**
* NVIDIA requires `input_type`: 'query' for what you search WITH, 'passage' for what you search
* THROUGH. Sending the wrong one still returns a vector, so nothing errors — retrieval just gets
* quietly worse, which is the hardest kind of bug to notice.
*/
async function embedWith(input: string, inputType: 'query' | 'passage'): Promise<number[]> {
const res = await fetch('https://integrate.api.nvidia.com/v1/embeddings', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.NVIDIA_API_KEY}` },
body: JSON.stringify({ model: EMBED_MODEL, input: [input], input_type: inputType }),
});
if (!res.ok) throw new Error(`embeddings ${res.status}`);
const json = await res.json();
return json.data[0].embedding;
}
export const embed = (t: string) => embedWith(t, 'query');
export const embedPassage = (t: string) => embedWith(t, 'passage');Bu string'den vektöre düz bir fonksiyon — GNL daha fazlasını istemiyor, o yüzden aynı fonksiyon hem bellek hatırlamasına hem runbook deposuna hizmet ediyor, ve sağlayıcı değiştirmek yalnızca bu dosyayı değiştiriyor.
$ # does it actually rank the right runbook higher? dimensions : 2048 vs memory runbook : 0.3157 vs disk runbook : 0.1758 picked the right one : true12Bellek
Son olayı unutan bir ajan, size her seferinde aynı soruları soran bir ajandır.
// src/agent.ts — memory
import { AgentMemory } from '@gnldev/memory';
import { z } from 'zod';
import { embed } from './embed.js';
const memory = new AgentMemory({
storage, // same Postgres — messages, threads and vectors all land there
embed,
recentN: 8, // the last 8 turns verbatim
// Recall scoped to the SERVICE, not the caller: at 3am what matters is what happened to `checkout`
// last time, whoever was holding the pager then.
recall: { topK: 3, scope: 'resource' },
// Structured facts the agent keeps across turns, validated by a schema rather than hoped for.
workingMemory: { schema: z.object({ service: z.string().optional(), severity: z.string().optional() }) },
});
export const gnl = createGnl({
storage,
memory, // TOP-LEVEL, not inside the agent — an agent-level `memory` is ignored
processors: [...],
schemaCompat: defaultRules,
agents: { ops: { model: MODEL, tools, guard, system: '…', maxSteps: 4 } },
});Hatırlama kaynağa kapsamlanıyor, çağırana değil: sabaha karşı önemli olan, checkout'a geçen sefer ne olduğudur, o gece pagerda kim olursa olsun. workingMemory yapılandırılmış yarısı — ajanın turlar arasında tuttuğu, umut edilerek değil bir şemayla doğrulanan gerçekler.
await gnl.run('ops', {
runId: `triage:${incidentId}`,
prompt,
threadId: incidentId, // the conversation — memory continuity across turns
resourceId: service, // WHOSE memory — recall is scoped to this
});createGnl memory'yi üst seviye yapılandırmadan okur. Bir ajan tanımının içine koyarsanız sessizce yok sayılır — hata yok, uyarı yok; threads tablosu boş kalınca öğrenirsiniz.13Runbook'lar — erişim
Doğaçlama yapan bir on-call ajanı, hiç ajan olmamasından kötüdür. "Bu kurum bu alarma karşı ne yapar" sorusunu yanıtlaması gerekir, ve o cevap geçen sefer olduktan sonra birinin sabaha karşı yazdığı düz metinde yaşar.
pnpm add @gnldev/rag @gnldev/cache// src/runbooks.ts — what the agent reads before it acts.
//
// An on-call agent that improvises is worse than no agent. It has to answer "what does THIS
// organisation do about this alert", and that lives in prose someone wrote at 3am last time.
import { InMemoryVectorStore, indexDocuments, createRagTool } from '@gnldev/rag';
import { createCache } from '@gnldev/cache';
import { storage } from './storage.js';
import { embed, embedPassage } from './embed.js';
const RUNBOOKS = [
{ id: 'rb-memory', text:
'RUNBOOK — memory saturation. Applies when memory_rss_mb exceeds 1800. Read the service log ' +
'first; a leak shows as rising RSS with flat request volume. If it matches, restart the ' +
'service — a restart is disruptive and needs SRE approval. Then watch for 30 minutes: a leak ' +
'that returns inside that window is a code bug, escalate rather than restart again.' },
{ id: 'rb-latency', text:
'RUNBOOK — latency regression. Applies when p99 exceeds 2000ms. Do NOT restart. Latency is ' +
'almost always downstream: check the database, then the cache hit rate.' },
{ id: 'rb-escalation', text:
'RUNBOOK — escalation. Page the on-call engineer once and only once per incident. A second page ' +
'for the same incident trains people to ignore the first.' },
];
// The corpus is re-indexed on boot. Cache the embeddings and you pay for each text once, ever —
// across runs, across restarts, because the key is the content, not the run.
const cache = createCache(storage.cache, 'embed');
const cachedPassage = (t: string) => cache.getOrCompute(t, () => embedPassage(t));
export async function buildRunbookTool() {
const store = new InMemoryVectorStore();
await indexDocuments(store, cachedPassage, RUNBOOKS);
// Returns an ORDINARY tool — the agent calls it by name like any other.
return createRagTool({ store, embed, topK: 1,
description: 'Finds the runbook for an alert. Always read this before acting.' });
}createRagTool sıradan bir araç döndürüyor — ajan searchRunbook'u diğerleri gibi isimle çağırıyor. Cache'e de dikkat: külliyat her açılışta yeniden indeksleniyor, o olmadan her yeniden başlatmada her gömmenin bedelini ödersiniz. Anahtar içerik, çalıştırma değil; yani yeniden kullanım süreçleri aşıyor.
Tutorial'ın geri kalanındaki her kural bu üç belgeden birine dayanıyor: SRE onayı ister guard oldu, yeniden başlatmayın 19. bölümde bir skorlayıcı olacak, bir kez ve yalnızca bir kez ise şimdi yazacağınız workflow.
14Workflow'lar — prompt değil, prosedür
Ajanın işi muhakemedir: hangi runbook geçerli, sayılar ne anlama geliyor. Hattın işi prosedürdür: triyaj, yükseltme, kayıt. Bir sistem promptuna yazılmış prosedür, denetleyemediğiniz ve tekrar oynatamadığınız prosedürdür — ve "nöbetçi mühendisi olay başına bir kez sayfala" prosedürdür.
pnpm add @gnldev/workflow// src/workflow.ts — procedure, not more prompt.
//
// The agent's job is JUDGEMENT: which runbook applies, what the numbers mean. The pipeline's job is
// PROCEDURE: triage, escalate, record. Procedure written into a system prompt is procedure you
// cannot audit and cannot replay — and "page once per incident" is procedure.
import { workflow, step } from '@gnldev/workflow';
// A step's output is the next step's input. Each is journaled under `runId#stepId`.
const triage = step('triage', async (alert: Alert, ctx) => {
// HERE is where you call the agent yourself — you are driving it from your own code, not serving
// an HTTP request. This is the case `gnl.run()` exists for.
const r = await gnl.run('ops', {
runId: `triage:${alert.incidentId}`,
prompt: alert.text,
threadId: alert.incidentId,
resourceId: alert.service,
});
const it = r.interrupts?.[0];
return { ...alert, diagnosis: r.text ?? '', suspended: !!it, awaiting: it };
});
const escalate = step('escalate', async (t) => {
if (t.severity !== 'sev1') return { ...t, paged: false };
await page('sre-oncall', t.incidentId);
return { ...t, paged: true };
});
const close = step('close', async (r) => { await record(r); return r; });
export const incident = workflow<Alert>().then(triage).then(escalate).then(close);gnl.run()'un nerede ortaya çıktığına dikkat edin: bir adımın içinde. Var olma sebebi bu — bir isteği sunmuyorsunuz, ajanı kendi kodunuzdan sürüyorsunuz, dolayısıyla runId'yi verecek başka kimse yok.
// src/agent.ts — register it alongside the agents.
export const config: CreateGnlConfig = {
storage,
agents: { ops: { … } },
workflows: { incident }, // now reachable at POST /api/workflows/incident/run
};Aynı config'e kaydedin, tıpkı bir ajan gibi sunulsun:
$ curl -XPOST localhost:3000/api/workflows/incident/run \ -d '{"runId":"wf:INC-42","input":{"incidentId":"INC-42","service":"checkout","severity":"sev1"}}' $ # A step suspended (the guard). POST the SAME runId to continue — completed steps replay,$ # the suspended one is re-evaluated. This endpoint is the ONLY way to resume a workflow.$ curl -XPOST localhost:3000/api/workflows/incident/run -d '{"runId":"wf:INC-42"}'Her adım runId#stepId altında günlükleniyor; bunun ajanın içinde üç satır daha değil de ayrı bir dosya olmasının sebebi bu: escalate'ten sonra ve close'dan önce ölen bir süreç, yeniden başlarken tekrar sayfalamıyor — kayıtlı sonucu elinde bulup doğrudan devam ediyor.
15Kuyruk, olaylar, zamanlayıcı
rb-memory otuz dakika sonraki bir ana dair bir sözle bitiyor — ve o sözü, o ana gelmeden büyük ihtimalle yeniden dağıtılacak bir süreç veriyor. Üç paket bunu niyetten olguya çeviriyor.
pnpm add @gnldev/queue @gnldev/events @gnldev/scheduler// src/ops.ts — how alerts arrive, and what watches afterwards.
import { enqueue, createWorker } from '@gnldev/queue';
import { emit, createConsumer } from '@gnldev/events';
import { scheduleWorkflow, pollScheduler } from '@gnldev/scheduler';
// ── queue: alerts are durable JOBS, not function calls ──────────────────────
// A crash mid-triage leaves the job pending, so it is picked up again instead of vanishing with the
// process that held it. The job id IS the incident id — a monitor that fires the same alert three
// times (they do) enqueues one job.
export const submitAlert = (alert: Alert) =>
enqueue(storage.work, 'alert', alert, { id: `alert:${alert.incidentId}` });
const worker = createWorker(storage, {
alert: async (payload) => { await incidentWorkflow.run(payload as Alert,
{ runId: `wf:${(payload as Alert).incidentId}`, journal: storage.runs }); },
});
// ── events: a restart publishes a FACT; whoever cares subscribes ────────────
// The agent does not know a watch exists, which is what lets you change the follow-up policy later
// without touching the prompt.
export const announceRestart = (incidentId: string, service: string) =>
emit(storage.work, 'service.restarted', { incidentId, service }, { id: `restarted:${incidentId}` });
// ── scheduler: the 30-minute watch, as a row with a time on it ──────────────
// rb-memory promises something about a moment thirty minutes from now, made by a process that will
// very likely be redeployed before then. setTimeout keeps that promise only if nothing restarts.
const watcher = createConsumer(storage.work, 'service.restarted', async (p: any) => {
await scheduleWorkflow(storage.runs, {
id: `watch:${p.incidentId}`, // idempotent on this id — a redelivered event schedules ONE watch
name: 'post-restart-watch',
input: { incidentId: p.incidentId, service: p.service },
at: Date.now() + 30 * 60 * 1000,
});
}, { name: 'post-restart-watcher' });
worker.start();
watcher.start();
// `.catch` is not decoration: a rejected promise from a bare interval is an unhandled rejection, and
// Node's default is to kill the process — so one transient poll error takes down alert intake too.
setInterval(() => { pollScheduler(storage.runs, runner).catch((e) => console.error('[sched]', e)); }, 5_000).unref();Kuyruk: alarmlar dayanıklı işler, yani triyaj ortasındaki bir çökme işi süreçle birlikte kaybetmek yerine beklemede bırakıyor. Olaylar: yeniden başlatma bir olgu yayınlıyor ve ilgilenen abone oluyor — ajan bir gözetimin varlığını bilmiyor, ki takip politikasını prompt'a dokunmadan değiştirebilmenizi sağlayan bu. Zamanlayıcı: gözetim günlükte zaman damgalı bir satır, dolayısıyla dağıtımdan uzun yaşıyor.
storage.work'e ekler, zamanlayıcı ise tetikleri storage.runs'ta tutar (tetik sayımı için listKeys ister, work mağazasında yoktur). İkisi de aynı Postgres veritabanında yaşar, yani yanlışını geçirmek tip kontrolünden sorunsuz geçer ve ilk poll'da patlar.16Ağ üzerinden ikinci bir ajan
rb-escalation'ın ikinci bir cümlesi var: ikinci bir görüş gerekiyorsa tekrar sayfalamak yerine veritabanı uzmanına sorun. Bunun ayrı bir ajan olmasının, daha uzun bir sistem promptu olmamasının sebebi bu — ve sebebin model kalitesiyle hiç ilgisi yok.
pnpm add @gnldev/a2a @gnldev/mcp @gnldev/server// src/specialist.ts — a SECOND agent, with its own blast radius.
//
// rb-escalation says: "if a second opinion is needed, ask the database specialist rather than
// paging again." That is why this is a separate agent and not a longer system prompt — the
// specialist's tools reach a production database, and the triage agent runs on whatever pages it.
// One agent with both toolsets is one blast radius covering both.
import { createA2ATool } from '@gnldev/a2a';
import { createMcpTools, connectMcp } from '@gnldev/mcp';
import { createRestApi } from '@gnldev/server';
// MCP: the specialist's tools come from the org's database-inspection server, not from this repo.
// Calls made through it are journaled, so a resumed run does not re-query.
const client = await connectMcp({ type: 'stdio', command: 'db-mcp' });
const dbTools = await createMcpTools(client, { prefix: 'db_' });
const specialistApp = createRestApi({
storage,
agents: { dbSpecialist: { model: MODEL, tools: dbTools, maxSteps: 4,
system: 'You are a database specialist. Answer with the query or pool number that explains the symptom.' } },
}, { title: 'DB Specialist', allowOpenAccess: true });
// A2A: the triage agent sees one more ordinary tool. Behind it is a POST to the specialist's REST
// API, wrapped so a timeout or a 500 becomes a failed tool result the triage model can read and
// react to — rather than an unhandled exception.
export const askSpecialist = createA2ATool({
endpoint: process.env.SPECIALIST_URL ?? 'http://specialist.internal',
agentName: 'dbSpecialist',
description: 'Asks the database specialist for a second opinion. Use this instead of paging again.',
timeoutMs: 10_000,
});MCP, uzmanın kurumuna ait araçları getiriyor, sizin deponuzdaki kodu değil — ve oradan geçen çağrılar günlükleniyor, dolayısıyla devam eden bir çalıştırma tekrar sorgulamıyor. A2A, uzmanı triyaj ajanının gözünde sıradan bir araç hâline getiriyor; bir zaman aşımını ya da 500'ü, işlenmemiş bir istisna yerine modelin okuyup tepki verebileceği başarısız bir araç sonucuna sarıyor.
17HTTP yüzeyi
Şimdiye kadarki her şey mantıktı. Bu ona bir yüzey veriyor — ve onay uç noktası, bunun neden tek bir createRestApi çağrısından ibaret olmadığının sebebi: yeniden başlatma bir insanı bekler, insanın da evet diyebileceği kimliği doğrulanmış bir yere ihtiyacı vardır.
pnpm add @gnldev/server @gnldev/studio @gnldev/auth @gnldev/chat-adapter hono @hono/node-server// src/server.ts — the surface a human touches.
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { createRestApi } from '@gnldev/server';
import { createStudioApp } from '@gnldev/studio';
import { createChatRoute } from '@gnldev/chat-adapter';
import { roleAuth, makeGate } from '@gnldev/auth';
import { toJournal } from '@gnldev/durable';
// Two credential classes, because during an incident they are genuinely different people: everyone
// needs to read the journal immediately, and exactly one person may say yes to taking production
// down. One token for both means everyone who can watch can also restart.
const auth = roleAuth({
admin: process.env.ONCALL_ADMIN ? { token: process.env.ONCALL_ADMIN } : undefined,
viewer: process.env.ONCALL_VIEWER ? { token: process.env.ONCALL_VIEWER } : undefined,
});
const gate = makeGate(auth, { allowOpenAccess: !auth });
const app = new Hono();
app.post('/alerts', async (c) => {
const body = await c.req.json();
const jobId = await submitAlert(body);
// 202, not 200: nothing has been triaged yet. Answering 200 with an empty diagnosis is how a
// monitoring system learns to stop trusting the acknowledgement.
return c.json({ accepted: true, jobId }, 202);
});
// The ONLY endpoint that can release a suspended restart. Note what is NOT here: any way to say
// "run the restart" — only a way to release a decision the agent already reached and the journal
// already recorded, addressed by that exact toolCallId.
app.post('/incidents/:runId/approve', async (c) => {
if (!(await gate.allow(c.req.raw, 'write'))) return gate.deny(c.req.raw, 'write');
const { toolCallId, approved } = await c.req.json();
const r = await resume(c.req.param('runId'), { [toolCallId]: approved !== false });
return c.json({ text: r.text, interrupts: r.interrupts });
});
// A streaming route the AI SDK's useChat talks to directly.
app.route('/chat', createChatRoute({ gnl }, { resolveThreadId: (_c, b) => b?.incidentId }));
// The generic REST API over agents/runs/workflows, and the journal inspector.
app.mount('/studio', createStudioApp({ reader: toJournal(storage.runs), apiBase: '/studio', auth }));
app.mount('/api', createRestApi({ storage, agents }, { auth, allowOpenAccess: !auth }));
// Loopback unless an address is named. A bare serve() binds every interface, which for an approval
// endpoint with no token is an admin surface offered to the network.
serve({ fetch: app.fetch, port: 3000, hostname: process.env.HOST ?? '127.0.0.1' });Tek uygulamada dört şey: kendi rotalarınız, AI SDK'nın useChat'inin konuştuğu akan bir /chat, ajanlar ve çalıştırmalar üzerindeki genel REST API, ve Studio — günlük denetleyicisi, onay kuyruğu ve zamanda gezinme arayüzü; diğer her şeyin yazdığı aynı günlüğü okuyor.
Bu iddia değil, ölçüm. Onay uç noktasının yapamadığına dikkat edin: "yeniden başlatmayı çalıştır" demenin bir yolu yok — yalnızca ajanın zaten vardığı ve günlüğün zaten kaydettiği bir kararı, tam o toolCallId ile adreslenmiş hâlde serbest bırakmak var.
$ export ONCALL_ADMIN=s3cr3t $ curl -XPOST localhost:3000/incidents/triage:INC-42/approve -d '{"toolCallId":"…"}'403 # restarts: 0 $ curl -XPOST … -H 'authorization: Bearer wrong' -d '…'403 # restarts: 0 $ curl -XPOST … -H 'authorization: Bearer s3cr3t' -d '…'200 # restarts: 118Zaten sahip olduğunuz bir uygulamaya bağlamak
Yukarıdaki hiçbir şey sıfırdan bir proje gerektirmiyor. GNL'nin HTTP yüzeyleri fetch handler, yani zaten çalıştırdığınız şeyin içine bağlanıyorlar — Hono onları doğrudan alıyor; Express, Fastify, Koa ve çıplak node:http tek bir adaptörden geçiyor:
pnpm add @gnldev/durable @gnldev/server @gnldev/studio// Express, Fastify, Koa and bare node:http go through one adapter.
import express from 'express';
import { toNodeHandler } from '@gnldev/server/node';
import { api, studio } from './gnl.js';
const server = express();
// YOUR routes first, with the body parser scoped to them.
//
// The one mistake worth naming: a GLOBAL express.json() mounted BEFORE GNL drains the request
// stream and hands the result to Express, not to us — the handler then sees a POST with no body
// and answers "runId is required" to a request that plainly carried one.
server.use('/app', express.json(), yourRouter);
server.use('/studio', toNodeHandler(studio));
server.use('/agents', toNodeHandler(api));
server.listen(3000, '127.0.0.1');gnl init --host express bu iki dosyayı mevcut bir projeye de yazar, elle yazmak istemezseniz.
runId is required yanıtı döner. Kendi rotalarınız ayrıştırıcılarını korur.19Skorlama ve izleme
"Runbook'a uydu mu", "çalıştı mı"dan farklı bir sorudur, ve ikisi de iş bittikten sonra günlükten yanıtlanır — ajandan yazması istenen log satırlarından değil.
pnpm add @gnldev/evals @gnldev/otel// src/observe.ts — did it follow the runbook, and what did it cost?
//
// Both answered from the JOURNAL after the fact, not from log lines the agent was asked to emit. A
// finished run can be scored, re-scored under a stricter rule later, and traced — because what
// happened is a data structure, not a transcript that existed once.
import { trajectoryScorerFor, scoreRun } from '@gnldev/evals';
import { exportRun } from '@gnldev/otel';
import { piiTextRedactor } from '@gnldev/processors';
import { toJournal } from '@gnldev/durable';
// The runbook, in a form a build can fail on. Deliberately about the SHAPE of the run, not the
// wording of the answer: a diagnosis can be phrased a hundred ways, but "called restartService
// during a latency incident" either happened or it did not.
export const RUNBOOK_RULES = {
memory: { requiredTools: ['searchRunbook', 'getMetric'], maxToolCalls: 8 },
latency: { requiredTools: ['searchRunbook'], forbiddenTools: ['restartService'], maxToolCalls: 8 },
};
export const scoreIncident = (runId: string, rules) => {
const reader = toJournal(storage.runs);
return scoreRun(reader, runId, [trajectoryScorerFor(reader, { ...rules, name: 'runbook-adherence' })]);
};
// `redact` matters here specifically: the run's verdict is written by a path NO processor sees, so a
// provider that quotes the offending input back inside a refusal puts that text into the span raw —
// and from there into a collector that is usually somebody else's.
export const traceIncident = (runId: string) =>
exportRun(toJournal(storage.runs), runId, {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
serviceName: 'oncall-agent',
redact: piiTextRedactor(),
});RUNBOOK_RULES kendi projenize kopyalamaya değer kısım: rb-memory ve rb-latency bir mühendisin bir kez okuduğu düz metin; bunlar ise aynı kuralların gelecekteki her çalıştırmayı otomatik kontrol eden hâli. Bitmiş bir çalıştırma sonradan daha katı bir kuralla yeniden bile skorlanabilir, çünkü kayıt bir kez var olmuş bir döküm değil, bir veri yapısı.
GET /incidents/triage:INC-42/score { "runId": "triage:INC-42", "scores": { "runbook-adherence": { "score": 1, "reason": "required: all 2 required tools present [score=1.00, weight=0.50] | budget: 4/8 tool calls (within budget) [score=1.00, weight=0.50] | => composite 1.000" } }}20Kırarak kanıtlayın
Yeşil bir test tek başına hiçbir şey kanıtlamaz. Yukarıdaki her garanti aynı şekilde doğrulanmalı: mekanizmayı kaldırın, ve belirli bir testin kırmızıya döndüğünü görün.
Davranış için testi yazın, sonra onu sağlayan satırı silin. Paket yeşil kalıyorsa, o satır hiçbir zaman sandığınız şeyi yapmıyordu — 7. bölümdeki sideEffect ölçümü tam olarak böyle bulundu.
dsn extraPattern'ı | veritabanı parolası modele olduğu gibi ulaşır |
guard | production kimse onaylamadan yeniden başlatılır |
readLog'daki idempotent: true | başarısız bir okuma yeniden denenmek yerine reddedilir |
| workflow günlüğü | devam ettirilen çalıştırma mühendisi ikinci kez sayfalar |
Davranış için testi yazın, sonra onu sağlayan satırı silin. Paket yeşil kalıyorsa, o satır hiçbir zaman sandığınız şeyi yapmıyordu — 7. bölümdeki sideEffect ölçümü tam olarak böyle bulundu.
Ne kurdunuz
Tek proje, yirmi paket, her biri senaryo hak ettiğinde eklendi — ve genelde sonradan üstüne eklenen, hiçbir zaman tam oturmayan dört özellik:
Etraflarında: kaynağa kapsamlanmış bellek, anlamla getirilen ve gömmeleri yeniden başlatmalar arasında saklanan runbook'lar, adımları tek tek oynatılabilir bir workflow, çökmeden sağ çıkan bir kuyruk, dağıtımdan uzun yaşayan bir zamanlayıcı, ağ üzerinden ikinci bir ajan, denetleyicisi olan kimliği doğrulanmış bir HTTP yüzeyi, ve iş bittikten sonra günlüğü okuyan skorlama.
Hiçbiri ilk altı bölümde öğrendiğinizi değiştirmedi. Üstüne biniyor.
- Dayanıklılık — yeniden denenen bir çalıştırma, sizin seçtiğiniz bir
runIdile anahtarlanmış günlüğünü oynatır, işi tekrarlamaz. - Başarısızlık güvenliği — başarısız olmuş yan etkili bir araç varsayılan olarak tekrarlanmak yerine reddedilir; okumalar için
idempotent: truebilinçli opt-out. - Onay — bir guard çalıştırmayı süreci bloklamak yerine Postgres'e askıya alır, ve cevap nerede gelirse orada devam eder.
- Redaksiyon — bir işlemci modele neyin gösterileceğine karar verir; production logu okuyan bir ajan için bu, bir izle bir sızıntı arasındaki farktır.