Agent Harness Oluşturmak: Döngü ve Ondan Çıkmanın Beş Yolu
İlk denemede çalışan 15 satırlık döngü; sonra bilerek 7 kez kırılıyor. İlk örnek, sıkı sınırlı olana göre 77 kat pahalıya kaçıyor.
Bu sayfada
Dürüst kısımla başlayalım, çünkü bunu başka kimse söylemeyecek: "harness" jargon, standart değil. Bir şartname yok, komite yok, referans tanım yok. Bu bölümün alıntıladığı dört makale — ReAct,1 CoALA,2 SWE-bench ve vLLM — özetlerinde bu kelimeyi bir kez bile kullanmıyor. Bu şeyin en çok indirilen implementasyonu, Vercel’in ayda 89,4 milyon indirilen ai paketi de kullanmıyor: harness dizgesi, 7.0.93 sürümüyle gelen 397 KB’lık type declaration dosyalarında sıfır kez geçiyor.3 Kelimenin gerçekten yük taşıdığı tek yerde ise bambaşka bir anlama geliyor. SWE-bench README’sinde "harness" beş kez geçiyor, her seferinde evaluation harness olarak — bir patch uygulayıp testleri çalıştıran container’lı iskele — ve Python modülü kelimenin tam anlamıyla swebench.harness.run_evaluation.4
Yani iki farklı şey aynı adı paylaşıyor. Bir evaluation harness, agent’ı sabit tutar ve puanlar. Bir agent harness, agent’ı çalıştıran programdır: modeli çağırır, modelin istediğini yürütür, ne zaman duracağına karar verir ve aradaki state’i tutar. Bu bölüm ikincisini, hiçbir framework kullanmadan, iki yüz satırdan az TypeScript ile kuruyor.
Döngünün kendisi on beş satır ve ilk denemede çalışıyor. Bundan sonraki her şey, ondan çıkmanın bir yolu.
Ayrıntıları göster
Bu bölümün önceki bölümlerden ihtiyaç duyduğu şeyler.
- Bölüm 14 client için: deadline’lar, status triage, cancellation, idempotency key’ler ve burada yeniden kullanılan mock provider tekniği.
- Bölüm 16 aritmetik için: input token’lar konuşmanın karesiyle büyür ve aşağıda kullanılan fiyatlar 6 Eylül 2026’da orada okunan fiyatlardır.
- Bölüm 18 araç kataloğu için: modelin gördüğü bir schema, asla görmediği bir endpoint ve hataların exception değil context olduğu kuralı.
- Bölüm 22 bu bölümün devraldığı döngü ve birbiriyle çelişen iki yayımlanmış "agent" tanımı için.
Burada tensor yok. Bu, kursun ikinci dependency hub’ı: 24, 25, 29 ve 30. bölümler aşağıdaki dosyanın üzerinde çalışıyor; 26’dan 28’e kadar olanlar ise onun erişebildiklerinin üstüne kuruluyor.
Script yazabileceğin bir provider
Bölüme bağlantı: Script yazabileceğin bir providerBölüm 14 gerçek bir provider’a karşı yazılamazdı, çünkü ondan seçtiğin anda bir 429 vermesini isteyemezsin. Bu bölümde aynı problem farklı bir biçimde var: gerçek bir modelden kontrolden çıkmasını ya da aynı aracı art arda iki kez, talep üzerine ve yeniden üretilebilir şekilde istemesini isteyemezsin.
Bu yüzden ilk program bir scripted provider: chat completions API biçiminde bir endpoint; yanıtı, tur indeksinin ve araçların o ana kadar döndürdüklerinin bir fonksiyonu. Token’ları gerçek bir byte-pair encoder ile sayıyor; dolayısıyla aşağıdaki para hesabı süs değil aritmetik.
const SCRIPTS = {
// A well-behaved task: list, read, answer.
plan: (t) =>
t === 0 ? asks(call("c1", "list_files", {}))
: t === 1 ? asks(call("c2", "read_file", { path: "errors.log" }))
: text("errors.log mentions a timeout: worker 7 timed out after 30000 ms."),
// Never declares itself done.
runaway: (t) => asks(call(`c${t}`, "list_files", {})),
// Guesses a file name, then corrects itself IF it was told what happened.
recover: (t, all) =>
t === 0 ? asks(call("c1", "read_file", { path: "timeout.log" }))
: /Call list_files/.test(all)
? (t === 1 ? asks(call("c2", "list_files", {}))
: t === 2 ? asks(call("c3", "read_file", { path: "errors.log" }))
: text("errors.log mentions a timeout."))
: text("I could not read the file, so I do not know."),
};
const turn = messages.filter((m) => m.role === "assistant").length;
const toolText = messages.filter((m) => m.role === "tool").map((m) => m.content).join("\n");
const message = SCRIPTS[scenario](turn, toolText);Tasarımı iki satır taşıyor. Tur indeksi bir değişkende tutulmuyor, konuşmadan türetiliyor; böylece provider stateless kalıyor ve bir run öldürülüp ona karşı sürdürülebiliyor. Ayrıca recover karar vermeden önce araç sonuçlarını okuyor: kendi transcript’ini okuyan bir scripted model, harness’in ona okumaya değer bir şey verip vermediğini ölçmek için gereken minimum şey.
Katalog Bölüm 18’deki katalog: üç dosya üzerinde dört araç: list_files, read_file, delete_file — needsApproval olarak işaretli — ve özellikle yavaş olan scan_archive.
Çalışan döngü
Bölüme bağlantı: Çalışan döngüOnu yaşanabilir kılan parçaların hiçbirinden önce, tüm fikir burada.
while (true) {
const reply = await callModel(base, messages, tools, signal);
messages.push(reply.message);
const calls = reply.message.tool_calls ?? [];
if (!calls.length) return reply.message.content;
for (const c of calls) {
const tool = byName.get(c.function.name);
const result = await tool.run(JSON.parse(c.function.arguments));
messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: result });
}
}Scripted provider’a yönelt ve tam olarak göründüğü şeyi yapar:
plan, cap 20 turns=3 tools=2 in=815 out=70 cost=$0.002470 ms=89 status=completed
answer: "errors.log mentions a timeout: worker 7 timed out after 30000 ms."
per-turn prompt tokens: 204, 269, 342Üç tur, iki araç yürütmesi, bir ABD sentinin çeyreği. Son satıra dikkat et: 204, 269, 342. Her tur kendinden önceki her şeyi yeniden gönderiyor; bu, Bölüm 16’daki quadratic faturanın kimse hiçbir şey yazmamışken geldiği yer. Bu bölümün geri kalanı, o satır büyümeyi bırakmadığında ne olduğunu anlatıyor.
Kırılma bir: hiç bitmeyen görev
Bölüme bağlantı: Kırılma bir: hiç bitmeyen görevAynı döngüyü runaway script’ine yönelt — her turda araç isteyen ve asla prose üretmeyen bir model — ve işaretli return hiçbir zaman çalışmaz. Başka çıkış yoktur. Program, process ölene ya da kredi kartı bitene kadar çalışır.
Çözüm tek satırdır, literatürün önerdiği ilk controldür,5 ve eninde sonunda herkes yazar. Neredeyse kimsenin yapmadığı şey ise ne kadar değerli olduğunu ölçmektir:
| tur sınırı | model çağrısı | input token | maliyet |
|---|---|---|---|
| 8 | 8 | 3.431 | $0,009070 |
| 20 | 20 | 16.259 | $0,038038 |
| 50 | 50 | 88.649 | $0,191098 |
| 100 | 100 | 337.299 | $0,702198 |
Son iki satırı birlikte oku. Sınırı 50’den 100’e çıkarmak maliyeti ikiye katlamadı; 3,7 ile çarptı. Input token’lar 88.649’dan 337.299’a çıktı, yani 3,8 katına; çünkü turu önceki her turu da taşır ve toplam olur. Tur sınırı doğrusal bir düğme değildir. En kötü durumunun karekökü üzerinde bir düğmedir; bu yüzden 20’den 100’e "güvende olmak için" çıkarmak, vermeden önce fiyatlandırmaya değer bir karardır.
Kırılma iki: tur sınırı para sınırı değildir
Bölüme bağlantı: Kırılma iki: tur sınırı para sınırı değildirTur sınırının sorunu, bir turun sabit fiyatı olmamasıdır. Kısa bir transcript üzerinde yirmi tur yukarıda $0,038 tutar. 200 araçlık katalog, retrieve edilmiş belge seti ve kırk mesajlık geçmişle yirmi tur bunun yüzlerce katına mal olur; sınır bunu bilmez. Operatörün sınırlamak istediği şey faturadır.
Bu yüzden döngü parayı sayar; Bölüm 16’daki computeCost, orada okunan fiyatlarla kullanılır — bu kurs boyunca fiyatlandırılan model için milyon input token başına $2,00 ve milyon output başına $12,00:
const PRICE_IN = 2.0 / 1e6, PRICE_OUT = 12.0 / 1e6;
export const cost = (u: Usage) => u.prompt_tokens * PRICE_IN + u.completion_tokens * PRICE_OUT;
// at the top of every iteration, before asking the model anything:
if (state.turns >= opts.limits.maxTurns) return stop("max_turns_exceeded", { type: "max_turns" });
if (state.costUsd >= opts.limits.maxBudgetUsd) return stop("budget_exceeded", { type: "max_budget" });
// ...and once the reply is back, before anything else happens with it:
state.costUsd += cost(reply.usage);Aynı runaway script, hiç tur sınırı yok, üç bütçe:
| bütçe | ulaşılan tur | gerçekten harcanan |
|---|---|---|
| $0,01 | 9 | $0,010780 |
| $0,05 | 24 | $0,051790 |
| $0,20 | 52 | $0,205398 |
Adını koymaya değer iki şey var. Birincisi, bütçe her seferinde farklı sayıda tur satın alıyor; zaten amaç bu: operatörün önemsediği şeyi sınırlıyor ve tur sayısını transcript’in getirdiği yere bırakıyor. İkincisi, her satır aşım yapıyor. Bütçe $0,010 idi ve $0,010780 harcandı; çünkü kontrol turdan önce çalışır, turun fiyatı ise bitene kadar bilinmez. Harcamayı tam olarak sınırlayamazsın; onu bir turun maliyeti içinde sınırlayabilirsin. Arayüzde bunu saklamaya çalışmak yerine söyle ve kontrolü çağrıdan önce koy ki aşım iki tur değil bir tur olsun.
Döngüden çıkmanın tek değil, beş yolu
Bölüme bağlantı: Döngüden çıkmanın tek değil, beş yoluBu noktada döngünün üç çıkışı var ve bölümün geri kalanının biçimi görünüyor. Production run tam olarak beş yoldan biriyle biter ve bunlar birbirinin varyasyonu değildir:
| nasıl biter | kim karar verdi | caller ne yapmalı |
|---|---|---|
| model istemeyi bıraktı | model | yanıtı oku |
| tur sınırı | sen, önceden | sınırı yükselt ya da kısmi sonucu kabul et |
| bütçe tükendi | sen, önceden | daha fazla parayı onayla ya da kısmi sonucu kabul et |
| retry edemeyeceğin bir hata | provider veya araç | deployment’ı düzelt; Bölüm 14’ün triage’ı karar verir |
| insan müdahale etti | bir kişi | kararı bekle, sonra sürdür |
Bunları tek bir boolean’a indirgemek bu dosyadaki en yaygın tasarım hatasıdır ve belirli bir şekilde pahalıdır: beşinden üçü resumable, ikisi değildir. Tur sınırına ulaşan bir agent’ın geçerli bir transcript’i, gerçek bir kısmi sonucu ve bir sonraki adımı vardır; 401 alan bir agent’ta bunların hiçbiri yoktur. Bu yüzden harness nedeni data olarak kaydeder:
export type RunStatus =
| "running" | "completed" | "failed"
| "max_turns_exceeded" | "budget_exceeded" | "interrupted";
export type Interruption =
| { type: "approval"; callId: string; toolName: string; args: unknown }
| { type: "max_turns" } | { type: "max_budget" }
| { type: "cancelled"; reason: string };Kırılma üç: bir araç başarısız olur
Bölüme bağlantı: Kırılma üç: bir araç başarısız olurBölüm 18 bir sayı vermeden şu iddiayla bitmişti: bir aracın hatasını raise etmek yerine araç sonucu olarak modele geri ver, model çoğunlukla kendini düzeltir. İşte sayı.
Bir failure, üç policy. Scripted model var olmayan bir dosya tahmin eder; araç no such file: timeout.log. Call list_files to see what exists. fırlatır
| harness hatayla ne yapar | tur | araç çalışması | maliyet | kullanıcı ne aldı |
|---|---|---|---|---|
| döngüden dışarı fırlatır | 1 | 1 | $0,000756 | stack trace |
Error: the tool failed. döndürür | 2 | 1 | $0,001462 | "Dosyayı okuyamadım, bu yüzden bilmiyorum." |
| gerçekten olanı döndürür | 4 | 3 | $0,003550 | "errors.log bir timeout’tan bahsediyor." |
Üçüncü satır birincinin 4,7 katına mal olur ve soruyu yanıtlayan tek satırdır. İlginç olan ikinci satırdır; çünkü çoğu codebase’in gerçekten yaptığı şey budur: hata yakalandı, döngü hayatta kaldı, modele bir şeyin başarısız olduğu söylendi ama neyin olduğu söylenmedi ve model kibarca vazgeçti. İkinci ve üçüncü satır arasındaki fark error handling değildir. Bir okur için yazılmış bir cümledir.
Bu yüzden harness, throw edilmiş aracı data olarak ele alır ve ifade biçimini bir policy yapar:
} catch (err: any) {
if (signal.aborted) return stop("interrupted", { type: "cancelled", reason: String(signal.reason) });
if (opts.toolErrorsAreFatal) { state.error = err.message; return stop("failed"); }
result = (opts.toolErrorText ?? ((e: Error) => `Error: ${e.message}`))(err);
}Bölüm 18 diğer taraf konusunda da uyarmıştı ve onun da bir bedeli var. Döngüyü hiçbir mesajın düzeltemeyeceği bir nedenle başarısız olan bir araca yönelt — process’in yapmasına izin verilmeyen bir okuma — ve model sonsuza kadar tekrar dener:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Başarılı olamayacak bir çağrının on bir aynı yürütmesi, düzeltilebilir bir hatadan toparlanan run’ın maliyetinin 5,2 katı ve sonunda hiçbir şey yok. Hatalar context’tir; kalıcı hata, run’ın geri kalanını zehirleyen context’tir. Ayrım, Bölüm 14’ün status triage’ının bir katman yukarı taşınmış halidir: modelin üzerine hareket edebileceği hata transcript’e geri döner; edemeyeceği hata ise run’ı bir reason ile durdurmalıdır. Bugün seninle ikinci durumun arasında duran şey tur sınırıdır; bu bir taban, çözüm değil.
Kırılma dört: aynı çağrı, iki kez
Bölüme bağlantı: Kırılma dört: aynı çağrı, iki kezŞimdi çoğu insanın olamayacağını varsaydığı failure. Modeller kendini tekrar eder. Herhangi bir döngüden yeterince uzun çalışmasını iste, iki ardışık turda aynı argümanlarla aynı aracı göreceksin.
Aynı görevin tekrarsız baseline’ına karşı ölçüldüğünde:
| tur | araç çalışması | maliyet | |
|---|---|---|---|
| görev, tekrar yok | 2 | 1 | $0,001396 |
| aynı görev, bir çağrı tekrarlandı | 3 | 2 | $0,002446 |
| tekrarlandı, read-only araçlarda result cache ile | 3 | 1 | $0,002446 |
Yinelenen çağrı $0,001050 ekstra maliyete, yani %75 artışa neden oldu; insanları şaşırtan kısım ise şu: sonucu cache’lemek bunun hiçbirini geri kazanmadı. Deduplication araç yürütmesini kurtardı, turu değil; çünkü kodun tekrar fark ettiğinde model sorduğu için zaten ücretlendirilmiştir. Araç yavaş, rate-limit’li veya çağrı başına ücretli olduğunda tasarruf gerçektir — ama büyüyen kalem üzerinde sıfırdır.
Daha kötü bir versiyon var. Aynı cache’i yazan bir araca uygula, ikinci çağrı sessizce gerçekleşmez:
naive cache on every tool 3 turns, 1 tool run, files deleted: ["access.log"]
cache only on read-only tools 3 turns, 2 tool runs, files deleted: ["access.log","access.log"]Bunlardan hangisi doğru? Bilinebilir şekilde, hiçbiri. Protocol bunların iki çağrı olduğunu söyler: iki farklı tool_call_id değeri taşırlar. Argümanlar bir olabileceklerini söyler. Argüman string’lerini karşılaştırarak karar veren bir harness, bir gün amaçlanmış iki aynı ücretlendirmenin ikincisini yutacaktır — ve Bölüm 14 bunun dürüstçe çözen tek mekanizmayı zaten adlandırdı: operasyonun ne olduğunu bilen katman tarafından logical operation başına üretilen bir idempotency key. Araç böyle bir key taşıyana kadar savunulabilir default yukarıdaki read-only kapıdır: okumaları cache’le, yazmaları yürüt ve geri kalanını yazmanın kendi idempotency’sine bırak.
if (opts.dedupe && (tool.readOnly || opts.dedupeAll) && seen.has(signature)) {
state.messages.push({ role: "tool", tool_call_id: c.id, name: c.function.name, content: seen.get(signature)! });
continue;
}Kırılma beş: bir şeyi siler
Bölüme bağlantı: Kırılma beş: bir şeyi silerdestructive script’i dosyaları listeler ve sonra görevin hiç bahsetmediği bir dosyayı silmek ister. Döngüde şimdiye kadar hiçbir şey onu durdurmazdı.
needsApproval olarak işaretlenmiş bir araç başarısız olmaz ve devam da etmez. Run’ı durdurur ve kontrolü geri verir; bir kişinin karar vermesi için gereken her şeyle birlikte:
if (tool.needsApproval && !state.approved.includes(c.id)) {
trace(state.runId, "approval_required", { toolName: tool.name, args: c.function.arguments, callId: c.id });
return stop("interrupted", { type: "approval", callId: c.id, toolName: tool.name, args: JSON.parse(c.function.arguments) });
}stopped at turn 2: interrupted / approval -> delete_file({"path":"access.log"})
files deleted so far: []
approve -> total turns=3 deleted=["access.log"] "Deleted access.log to free space."
reject -> total turns=3 deleted=[] "I did not delete anything: you declined the deletion."Mekanizmanın tamamı bu; bunun callback değil return olmasının nedeni ise bir sonraki bölüm: durma ile karar arasında process artık var olmayabilir.
Ama önce kimsenin beklemediği ölçüm. Bir reddetme, sonucun yokluğu değildir — transcript’te tool_call_id ile anahtarlanmış bir slot vardır ve içine bir şey konmalıdır. Aynı reddi iki kez çalıştır; yalnızca o şeyin ne söylediğini değiştir:
rejected with a reason deleted=[] the agent then told the user:
"I did not delete anything: you declined the deletion."
rejected with nothing deleted=[] the agent then told the user:
"Deleted access.log to free space."İki run’da da hiçbir şey silinmedi, ikincisinde ise kullanıcıya silindiği söylendi. Permission system kusursuz çalıştı; rapor yalan. Bu, tool-error tablosuyla aynı mekanizmadır ama çok daha önemli bir yere gelir — bir insan hayır dedi, eylem doğru şekilde engellendi ve agent’ın özeti gerçekle çelişiyor; çünkü ret, modelin okuduğu yere hiç yazılmadı. Buradan çıkan kural kısa: kodun bir tool call hakkında neye karar verirse versin, kararı transcript’e kelimelerle yaz. Bölüm 30 buna güvenlik tarafından geri döner; orada bu, audit trail ile kurgu arasındaki farktır.
Kırılma altı: process ölür
Bölüme bağlantı: Kırılma altı: process ölürBir onay dakikalar ya da saatler sürer. Bir deploy saniyeler sürer. Run bir HTTP request içindeki local variable’da yaşıyorsa, her restart kayıp bir run ve her onay bir race’tir.
Bu yüzden run bir closure değildir. Düz, serialisable bir object’tir — messages, turn count, cost, status, interruption, approved call id listesi — ve döngü onun üzerinde pure function’dır. Persistence’ı tek satırlık bir mesele yapan şey bu tek kısıttır:
export const save = (s: RunState, dir: string) => writeFileSync(`${dir}/${s.runId}.json`, JSON.stringify(s));
export const load = (dir: string, runId: string) => JSON.parse(readFileSync(`${dir}/${runId}.json`, "utf8"));Doğruluk sorusu kaydetmek değildir. Geri gelindiğinde ne olduğudur ve naif yanıt seni iki kez ücretlendirir. Process, model bir araç istedikten sonra ama sonuç yazılmadan önce öldüyse, modeli yeniden çağırarak başlayan bir resume zaten sahip olduğu bir tur için para öder — araçları yeniden çalıştırarak başlarsa, bir yazmayı iki kez yapar.
Çözüm, döngüyü transcript’e neyin outstanding olduğunu sorarak başlatmaktır:
export function pending(state: RunState): ToolCall[] {
const answered = new Set(state.messages.filter((m) => m.role === "tool").map((m) => m.tool_call_id));
const last = state.messages.at(-1);
if (last?.role !== "assistant") return [];
return (last.tool_calls ?? []).filter((c) => !answered.has(c.id));
}Her iteration önce pending boşaltır ve ancak outstanding hiçbir şey kalmadığında modele sorar. Resume normal code path ile aynı hale gelir; approval da öyle — onaylanmış çağrı, artık çalışmasına izin verilen pending çağrıdan ibarettir. Process’i task ortasında öldür ve yeniden başlat:
process died after turn 2. tool runs so far: list_files, read_file:errors.log
restored from disk: turns=2 cost=$0.001570 messages=6 status=running
resumed and finished: turns=3 cost=$0.002470 status=completed
tool runs across BOTH processes: list_files, read_file:errors.logİki araç gerektiren bir task için iki process boyunca iki araç yürütmesi; final maliyet hiç crash etmemiş run ile aynı. Cost restart boyunca birikir, çünkü bir değişkende değil state’in içindeydi.
Kırılma yedi: üç dakikalık sessizlik
Bölüme bağlantı: Kırılma yedi: üç dakikalık sessizlikscan_archive burada üç saniye sürer ve production’da üç dakika süren aracın yerine geçer. Çalışırken iki şey eksiktir: kullanıcı hiçbir şey olup bittiğini bilmez ve Stop düğmesi hiçbir şey yapmaz.
İkisi de aynı fix’tir ve Bölüm 14’ün AbortSignal öğesinin bir katman aşağı itilmiş halidir. Signal yalnızca fetch için değildir — aracın içine geçirilir ve iyi yazılmış bir araç buna uyar:
result = await tool.run(JSON.parse(c.function.arguments), {
signal,
progress: (label) => { trace(state.runId, "tool_progress", { toolName: tool.name, label }); opts.onProgress?.(label); },
});progress: scanned 200 of 1200 files (t+506 ms)
progress: scanned 400 of 1200 files (t+1007 ms)
no cancellation: stopped after 3,015 ms, status=completed
user presses Stop at 1.2 s: stopped after 1,202 ms, status=interrupted, reason="user pressed Stop"Tıklamadan durmaya iki milisaniye; çünkü aracın içindeki sleep, fetch’in dinlediği signal’ın aynısını dinler. Bunu yalnızca fetch içine geçirirsen, aynı Stop düğmesi üç saniye — yani aracın süresi — bekler ve run iptal ettiği iş zaten bittikten sonra "cancels" olur. En aşağıya kadar bağlanmamış cancellation, doğru kelimeyi söyleyen bir spinner’dır.
Trace ve neden log olmadığı
Bölüme bağlantı: Trace ve neden log olmadığıHarness event başına bir satır yayar ve vocabulary ezberlenecek kadar küçüktür: turn, tool_start, tool_progress, tool_result, approval_required, run_stopped.
{"runId":"n1","type":"turn","turn":1,"prompt_tokens":204,"completion_tokens":23,"total_tokens":227,"costUsd":0.000684,"finish":"tool_calls"}
{"runId":"n1","type":"tool_start","toolName":"list_files","args":"{}","callId":"c1"}
{"runId":"n1","type":"tool_result","toolName":"list_files","ms":1,"ok":true}
{"runId":"n1","type":"turn","turn":2,"prompt_tokens":269,"completion_tokens":29,"total_tokens":298,"costUsd":0.00157,"finish":"tool_calls"}
{"runId":"n1","type":"approval_required","toolName":"delete_file","args":"{\"path\":\"access.log\"}","callId":"c2"}
{"runId":"n1","type":"run_stopped","status":"interrupted","reason":"approval","turns":2,"costUsd":0.00157}Bunu logging değil trace yapan üç özellik var. Her satır run id taşır; böylece üç process ve iki güne yayılan bir run tek bir query’dir. Her turn satırı kendi token count’larını ve running cost’u taşır; böylece "bu run neden kırk dolar tuttu" sorusu sonradan yanıtlanabilir, yalnızca teoride yeniden üretilebilir kalmaz. Ve run_stopped reason taşır; bu alan support ticket’ı tek satırlık cevaba çevirir: bütçede duran agent ile crash eden agent dışarıdan aynı görünür ve zıt yanıtlar gerektirir.
Latency aritmetiği
Bölüme bağlantı: Latency aritmetiğiBölüm 13 time to first token’ı sahip olduğun hardware üzerinde ölçtü. Bölüm 14 onu bir socket üzerinden ölçtü. Bir agent bunu çarpar ve çarpan, kimsenin seçmediği bir sayıdır:
Aynı üç turlu task, yalnızca provider latency’si değiştirilerek:
| tur başına provider latency | wall clock, 3 tur |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2.413 ms |
Harness’in kendisi üç turlu run’a on beş milisaniye ekler. Geri kalan her şey çarpı kontrol etmediğin bir sayı — request’ini yabancıların request’leriyle batch’leyen serving scheduler içinde ayarlanır6 — ve model tarafından seçilir. Bu yüzden Bölüm 14’ün streaming’i burada chat’te olduğundan daha önemlidir ve daha az yardımcı olur: final turu stream edebilirsin, ondan önceki dört tur ise harness progress yaymazsa sessizliktir. Yukarıdaki tool_progress event’inin tüm argümanı da budur — bir agent’ta dürüst feedback birimi token değil, step’tir.
Aynı harness, portun arkasında gerçek bir model
Bölüme bağlantı: Aynı harness, portun arkasında gerçek bir modelYukarıdaki her şey scripted provider’a karşı çalıştı; bu harness’i kanıtlar, modeller hakkında hiçbir şeyi kanıtlamaz. Bu yüzden bir satırı değiştir — Bölüm 14’teki seam, LLM_BASE_URL — ve aynı kodu, aynı dört araçla local Qwen2.5-0.5B-Instruct’a yönelt. Aynı üç dosya üzerinde altı task:
turns=2 tools=1 wall= 15,260ms Which file mentions a timeout? -> "The file timeout.txt does not exist..."
turns=2 tools=1 wall= 13,037ms How many files are in the directory? -> "There are three files..."
turns=2 tools=1 wall= 10,121ms Read notes.txt and tell me what it says. -> "Remember to rotate your logs."
turns=2 tools=2 wall= 21,290ms List the files and then read each one.
turns=2 tools=1 wall= 10,698ms Which file is the largest? -> "The largest file is access.log."
turns=2 tools=1 wall= 12,490ms Is there a file about rotating logs?
TOTAL turns=12 toolruns=7 wall=82,896ms mean turn=6,908msÜç bulgu var; üçüncüsü bu bölümün var olma nedeni.
Her bir task tam olarak iki turda bitti. Tur sınırı hiç devreye girmedi, bütçe hiç devreye girmedi ve döngünün tek çıkışı modelin prose üretmesiydi. Yarım milyar parametreli model iterate etmez; ihtiyacı olan şeye sahip olsun ya da olmasın ikinci nefesinde yanıt verir. Tur sayısı döngünün değil modelin özelliğidir.
Ortalama tur 6.908 milisaniye sürdü, yani yukarıdaki latency tablosu oyuncak değil: bu boyutta hipotetik sekiz turlu run ekranda hiçbir şey yokken neredeyse bir dakikalık wall clock demektir.
Ve yanıtlar yanlış. En büyük dosya errors.log; model dosyaları listeledi, hiçbirini okumadı ve yine de birini adlandırdı. İlk task bir dosya adı tahmin etti, var olmadığı söylendi ve sonuca vardı. Harness altı run’ın tamamında kusursuz çalıştı. Harness bir agent’ı yönetilebilir yapar, doğru değil — Bölüm 29 hangisi olduğunu nasıl anlayacağını, Bölüm 30 ise kimse bunu yapmadığında maliyetini anlatır.
Subagent’lar, burada adlandırıldı ve sonra ücretlendirilecek
Bölüme bağlantı: Subagent’lar, burada adlandırıldı ve sonra ücretlendirilecekKatalogdaki bir aracın arkasında başka bir run olabilir. Interface Bölüm 18’deki gibidir — bir schema ve endpoint — ve bu interface dar olduğu için bütün bir agent onun arkasına sığar:
const research: Tool = {
name: "research",
description: "Investigate one question and return a short summary.",
parameters: { type: "object", properties: { question: { type: "string" } }, required: ["question"] },
readOnly: true,
async run(args, ctx) {
const child = newRun(RESEARCH_SYSTEM, args.question); // its own transcript
const out = await run(child, researchTools, { base, limits: { maxTurns: 6, maxBudgetUsd: 0.05 }, signal: ctx.signal });
return out.output ?? "no result";
},
};Bu on satırda üç şey şimdiden doğru ve üçünün de nedeni yukarıda verilen kararlardır: child’ın kendi window’u vardır, bu yüzden parent transcript’i child’ın okuduğu her şeyi değil bir summary alır; kendi limitleri vardır, bu yüzden runaway child parent budget’ını harcayamaz; ve signal’ı miras alır, böylece tek Stop tüm tree’yi iptal eder. Temiz window’un yan etki değil asıl nokta olmasının nedeni Bölüm 24; beş orchestration pattern — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — ve handoff ise Bölüm 25.
Framework’ler nerede ve bu kurs neden birini kullanmadı
Bölüme bağlantı: Framework’ler nerede ve bu kurs neden birini kullanmadıYukarıdaki hiçbir şey libraries’e karşı bir argüman olarak okunmamalı. 7 Eylül 2026’da, 29 Ağustos’ta biten ay için ölçüldüğünde:7
| package | o ayki indirme | sana ne verir |
|---|---|---|
ai (Vercel AI SDK) | 89.385.860 | ToolLoopAgent, stopWhen, tool approval, step hooks |
@anthropic-ai/claude-agent-sdk | 41.558.352 | library olarak Claude Code harness: döngü, sessions, hooks, permissions, subagents8 |
@langchain/langgraph | 12.812.815 | explicit state graph olarak döngü |
langchain | 11.359.058 | chains, agents, integrations |
@openai/agents | 6.093.155 | agents, handoffs, guardrails |
@mastra/core | 5.914.502 | agents, workflows, memory |
Bu kursun bunlardan birini öğretmek yerine döngüyü elle yazmasının nedeni ima edilmek yerine açıkça söyleniyor ve ölçülebilir. 7 Eylül 2026’ya kadar olan on iki ayda ai 945 sürüm yayımladı ve major 5’ten major 7’ye geçti; agent class hâlâ Experimental_Agent olarak export ediliyor; langchain aynı aralıkta 132 sürüm yayımladı; @openai/agents 83 sürüm yayımladı ve ilk release’inden on beş ay sonra hâlâ 0.x’te.7 Bu API’lerin herhangi birine karşı yazılmış bölüm bir sezon içinde eskir; bu bölüm otuz üç dilde yayımlandığı için her yeniden basım bütün çeviri yüküne mal olur. Hepsinin altında kalan şey hareket etmez: bir döngü, bir stopping rule, bir katalog, bir executor, biraz state.
Ve reference implementation, önemli olan kısımda bu bölümle aynı fikirde. ai 7.0.93 sürümünde döngünün çıkışı bir sayı değildir — stopWhen, yani predicate listesi; step count bunlardan yalnızca biridir:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsStopping, bu döngünün en çok kullanılan implementasyonunda çoğuldur; yukarıdaki yüz doksan altı satırda çoğul olmasının nedeni de aynıdır.
Buradan sonra nereye gidiyoruz
Bölüme bağlantı: Buradan sonra nereye gidiyoruzArtık bir harness’in var: döngü, katalog, executor, beş çıkış yolu, persisted run, araçlara ulaşan signal ve her satırında run id olan trace. 24, 25, 29 ve 30. bölümler bu dosyanın, 26’dan 28’e kadar olanlar ise onun erişebildiklerinin üstüne kuruluyor.
Geriye bir problemi kaldı ve yukarıdaki ölçümler baştan beri onu işaret ediyor. Runaway tablosuna bir kez daha bak: sekiz turda 3.431 input token, yüz turda 337.299. Çalışan run’a bak: 204, 269, 342. Her tur bütün transcript’i yeniden gönderiyor; dolayısıyla agent’ın context’i kendi history’siyle doluyor — ve model uzun window’un uzak ucunu yakın ucu kadar iyi kullanamıyor; bu yüzden beşinci turda iyi olan agent kırkıncı turda kafası karışmış oluyor.
Tur sınırı bunu düzeltmez. Yalnızca bunun olmasını izlemek için para ödemenizi durdurur. Bunu düzelten şey, her tek turda hangi token’ların window’u hak ettiğine karar vermektir: ne compact edilecek, ne agent’ın fetch edebileceği bir nota taşınacak, ne temiz window’lu bir subagent’a verilecek ve hangi tool definition’lar kalıcı vergilerine değer. Bölüm 24 window’un gerçekte nereye gittiğini ölçüyor — sürpriz şu ki, konuşmaya değil.
Kaynaklar ve yöntem
Bölüme bağlantı: Kaynaklar ve yöntemBu bölümdeki her sayı yukarıda anlatılan iki server’dan çıktı; Node 22 üzerinde loopback interface ile: o200k_base encoding’iyle token sayan scripted provider ve aynı biçimdeki endpoint’in arkasında CPU üzerinde greedy decoding yapan Qwen/Qwen2.5-0.5B-Instruct. Maliyetler, Bölüm 16’nın 6 Eylül 2026’da okuduğu fiyatlarla — milyon input token başına $2,00 ve milyon output başına $12,00 — ölçülen token count’lardan hesaplandı ve bu bölümdeki hiçbir request ücretli bir endpoint’e gitmedi. Local model’in yanıtları küçük bir model’in yanıtlarıdır; onları güncel modellerin ne yaptığına dair benchmark olarak değil, her iki durumda da aynı olan döngü hakkında evidence olarak oku.
Referanslar
Bölüme bağlantı: Referanslar-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. ve Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Döngünün implement ettiği reasoning trace’ler ile eylemlerin iç içe geçmesi ve acting’in bir modelin "handle exceptions" yapmasına izin verdiği gözleminin kaynağı — yukarıdaki tool-error tablosu tam olarak bunu ölçüyor. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. ve Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Yukarıdaki döngünün informal olarak yaptığı şeyin formal treatment’ı: modular memory components, internal memory ve external environment’ları kapsayan structured action space ve "a generalized decision-making process to choose actions". Industry term’ün kaçırdığı vocabulary için oku — özellikle working, episodic, semantic ve procedural memory ayrımı; bunun pratik gölgesi Bölüm 24’ün üç-store tablosudur. ↩
-
ai(Vercel AI SDK) sürüm 7.0.93, 4 Eylül 2026’da yayımlandı; type declaration’lar 7 Eylül 2026’dacdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tsüzerinden okundu. 397 KB’lık dosyadaharnessdizgesi sıfır kez geçiyor. Agent classdeclare class ToolLoopAgent; hemToolLoopAgenthem deExperimental_Agentolarak export ediliyor;declare function isStepCount(stepCount: number)—stepCountIsolarak export edilen — yukarıda aynen alıntılandı;type StopConditionikinci type parameter’ı (RUNTIME_CONTEXT extends Context = Context) olmadan gösterildi; alıntıdaki tek elision budur,generateTextvestreamTextüzerindekistopWhen?: Arrayable<StopCondition<...>>biçimi de öyle. Aynı dosyatoolApproval,ToolApprovalStatus,prepareStepverepairToolCalltanımlar; yani reference implementation bağımsız olarak approval gate’lere, per-step preparation’a ve error repair’e varmıştır. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. ve Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Özet, artefact’ı 2.294 problemlik bir "evaluation framework" olarak adlandırır ve "harness" kelimesini hiç kullanmaz; projenin kendi README’si (
github.com/SWE-bench/SWE-bench, 7 Eylül 2026’da okundu) kelimeyi beş kez kullanır, her seferinde "evaluation harness" olarak; entry pointpython -m swebench.harness.run_evaluation. Bu, kelimenin diğer anlamıdır: agent’ı sabit tutup puanlayan bir iskele, onu çalıştıran döngü değil. ↩ -
Anthropic, Building effective agents, 19 Aralık 2024,
anthropic.com/engineering/building-effective-agents, 7 Eylül 2026’da okundu. Building block olarak augmented model, "using tools based on environmental feedback in a loop" yapan LLM olarak agent ve kontrolü korumak için "such as a maximum number of iterations" stopping condition önerisi. Bölüm 22 tanımını tam olarak alıntılar. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. ve Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Diğer döngü — request’ini yabancıların request’leriyle batch’leyen ve Bölüm 13’ün KV cache’ini yöneten serving scheduler. Tam da sana ait olmadığı için var olduğunu bilmeye değer: harness’inin çarptığı latency onun içinde belirlenir ve kendi döngün üzerinde ne kadar çalışırsan çalış bunu oynatmaz. ↩
-
npm registry download count’ları,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>; rollinglast-monthyerine explicit window ve release history’leriregistry.npmjs.org/<package>üzerinden; ikisi de 7 Eylül 2026’da sorgulandı. Release count’ları, canary build’ler dahil o tarihe kadarki on iki ayda yayımlanan version sayısıdır:ai945 (latest 7.0.93, 2026-09-04; major 5, 6 ve 7’nin hepsi window içinde görünüyor),langchain132 (latest 1.5.10, 2026-08-20),@openai/agents83 (latest 0.17.0, 2026-08-19; ilk yayımlanma 2025-06-03). ↩ ↩2 -
Claude Agent SDK (
@anthropic-ai/claude-agent-sdk), library olarak paketlenmiş Claude Code harness’tir — agent loop, built-in file ve shell araçları, context management, sessions, hooks, permissions ve subagents —code.claude.com/docs/en/agent-sdkadresinde belgelenir. Bu bölümün elle kurduğu her mekanizmanın yayımlanmış anlatımına en yakın şeydir ve bu bölümün yalnızca işaret ettiği parçaları adlandırdığı için kendi implementasyonunun yanında okumaya değerdir. ↩