Membangun Agent Harness: Loop dan Lima Jalan Keluarnya
Loop 15 baris yang langsung jalan, lalu sengaja dirusak 7 kali—dimulai dari runaway yang biayanya 77x versi dengan batas ketat.
Di halaman ini
Mulailah dari bagian yang jujur, karena tidak ada orang lain yang akan mengatakannya: "harness" adalah jargon, bukan standar. Tidak ada spesifikasi, tidak ada komite, tidak ada definisi rujukan. Empat paper yang dikutip bab ini — ReAct,1 CoALA,2 SWE-bench dan vLLM — tidak memakai kata itu sekali pun di abstrak mereka. Implementasi paling banyak diunduh dari benda ini, package ai milik Vercel dengan 89,4 juta unduhan per bulan, juga tidak memakainya: string harness muncul nol kali dalam 397 KB deklarasi tipe yang dikirim oleh versi 7.0.93.3 Satu tempat kata itu memang menjadi penopang makna berarti sesuatu yang sama sekali lain. SWE-bench menyebut "harness" lima kali di README-nya, selalu sebagai evaluation harness — kerangka containerised yang menerapkan patch dan menjalankan test — dan modul Python-nya secara harfiah adalah swebench.harness.run_evaluation.4
Jadi dua hal berbeda berbagi nama. Evaluation harness menahan agent tetap diam dan menilainya. Agent harness adalah program yang menjalankan agent: ia memanggil model, mengeksekusi apa yang diminta model, memutuskan kapan berhenti, dan menyimpan state di antaranya. Bab ini membangun yang kedua, dalam kurang dari dua ratus baris TypeScript, tanpa framework sama sekali.
Loop itu sendiri lima belas baris dan bekerja pada percobaan pertama. Semua setelah itu adalah cara untuk keluar darinya.
Tampilkan detail
Yang dibutuhkan bab ini dari bab-bab sebelumnya.
- Bab 14 untuk client: deadline, triase status, cancellation, idempotency key, dan teknik mock provider yang dipakai lagi di sini.
- Bab 16 untuk aritmetikanya: input token tumbuh mengikuti kuadrat percakapan, dan tarif yang dipakai di bawah adalah tarif yang dibaca di sana pada 6 September 2026.
- Bab 18 untuk katalog tool: schema yang dilihat model, endpoint yang tidak pernah dilihatnya, dan aturan bahwa error adalah context, bukan exception.
- Bab 22 untuk loop yang diwarisi bab ini, dan untuk dua definisi "agent" yang sudah dipublikasikan tetapi saling tidak sepakat.
Tidak ada tensor di sini. Ini hub dependensi kedua dalam kursus: Bab 24, 25, 29 dan 30 berjalan di atas file di bawah, dan 26 sampai 28 membangun di atas apa yang bisa dijangkaunya.
Provider yang bisa kamu skrip
Tautan ke bagian: Provider yang bisa kamu skripBab 14 tidak bisa ditulis terhadap provider nyata, karena kamu tidak bisa meminta 429 pada momen yang kamu pilih. Bab ini punya masalah yang sama dalam bentuk berbeda: kamu tidak bisa meminta model nyata untuk runaway, atau meminta tool yang identik dua kali berturut-turut, secara on-demand dan reproducible.
Jadi program pertama adalah scripted provider: endpoint dengan bentuk chat completions API yang balasannya merupakan fungsi dari indeks turn dan dari apa yang sudah dikembalikan tool sejauh ini. Ia menghitung token dengan byte-pair encoder nyata, jadi biaya di bawah adalah aritmetika, bukan dekorasi.
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);Dua baris memikul desainnya. Indeks turn diturunkan dari percakapan, bukan disimpan dalam variable, jadi provider stateless dan sebuah run bisa dimatikan lalu dilanjutkan terhadapnya. Dan recover membaca hasil tool sebelum memutuskan: scripted model yang membaca transkripnya sendiri adalah minimum yang dibutuhkan untuk mengukur apakah harness memberinya sesuatu yang layak dibaca.
Katalognya adalah milik Bab 18, empat tool di tiga file: list_files, read_file, delete_file — ditandai needsApproval — dan scan_archive, yang sengaja dibuat lambat.
Loop yang bekerja
Tautan ke bagian: Loop yang bekerjaInilah seluruh idenya, sebelum bagian mana pun yang membuatnya bisa bertahan hidup.
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 });
}
}Arahkan ke scripted provider dan ia melakukan persis seperti kelihatannya:
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, 342Tiga turn, dua eksekusi tool, seperempat sen AS. Perhatikan baris terakhir: 204, 269, 342. Setiap turn mengirim ulang semua yang terjadi sebelumnya, yaitu tagihan kuadratik dari Bab 16 yang tiba di tempat tanpa ada siapa pun mengetik apa pun. Sisa bab ini adalah apa yang terjadi ketika baris itu tidak berhenti tumbuh.
Kerusakan satu: task yang tidak pernah selesai
Tautan ke bagian: Kerusakan satu: task yang tidak pernah selesaiArahkan loop yang sama ke skrip runaway — model yang meminta tool di setiap turn dan tidak pernah mengeluarkan prosa — dan return yang ditandai tidak pernah terpanggil. Tidak ada exit lain. Program berjalan sampai process mati atau kartu kreditnya yang mati.
Perbaikannya satu baris, ini kontrol pertama yang direkomendasikan literatur,5 dan semua orang akhirnya menulisnya. Yang hampir tidak dilakukan siapa pun adalah mengukur nilainya:
| turn cap | model calls | input tokens | cost |
|---|---|---|---|
| 8 | 8 | 3.431 | $0.009070 |
| 20 | 20 | 16.259 | $0.038038 |
| 50 | 50 | 88.649 | $0.191098 |
| 100 | 100 | 337.299 | $0.702198 |
Baca dua baris terakhir bersama-sama. Menggandakan cap dari 50 ke 100 tidak menggandakan biaya; ia mengalikannya 3,7 kali. Input tokens naik dari 88.649 ke 337.299, faktor 3,8, karena turn membawa setiap turn sebelumnya dan totalnya adalah . Turn cap bukan dial linear. Ia dial pada akar kuadrat dari kasus terburukmu, itulah sebabnya menaikkannya dari 20 ke 100 "sekadar aman" adalah keputusan yang layak dihitung harganya sebelum kamu ambil.
Kerusakan dua: cap pada turn bukan cap pada uang
Tautan ke bagian: Kerusakan dua: cap pada turn bukan cap pada uangMasalah turn cap adalah sebuah turn tidak punya harga tetap. Dua puluh turn pada transkrip pendek berbiaya $0.038 di atas. Dua puluh turn dengan katalog 200 tool, satu set dokumen yang di-retrieve, dan empat puluh pesan riwayat berbiaya ratusan kali lipat, dan cap tidak tahu. Yang ingin dibatasi operator adalah tagihannya.
Jadi loop menghitung uang, memakai computeCost dari Bab 16 terhadap tarif yang dibaca di sana — $2.00 per juta input tokens dan $12.00 per juta output, untuk model yang dipakai harganya sepanjang kursus ini:
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);Skrip runaway yang sama, tanpa turn cap sama sekali, tiga budget:
| budget | turns reached | actually spent |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
Dua hal layak diberi nama. Pertama, budget membeli jumlah turn yang berbeda setiap kali, dan itu intinya: ia membatasi hal yang dipedulikan operator, lalu membiarkan jumlah turn jatuh di tempat transkrip menaruhnya. Kedua, setiap baris overshoot. Budget-nya $0.010 dan yang terpakai $0.010780, karena pemeriksaan berjalan sebelum sebuah turn dan harga sebuah turn tidak diketahui sampai turn itu selesai. Kamu tidak bisa membatasi spend secara persis; kamu bisa membatasinya sampai dalam jarak biaya satu turn. Katakan itu di interface alih-alih berpura-pura, dan letakkan pemeriksaan sebelum call supaya overshoot-nya satu turn, bukan dua.
Lima cara keluar dari loop, bukan satu
Tautan ke bagian: Lima cara keluar dari loop, bukan satuSekarang loop punya tiga exit, dan bentuk sisa bab ini sudah terlihat. Run production berakhir tepat dalam satu dari lima cara, dan mereka bukan variasi satu sama lain:
| how it ends | who decided | what the caller should do |
|---|---|---|
| model berhenti meminta | model | baca jawabannya |
| turn cap | kamu, sebelumnya | naikkan cap, atau terima hasil parsial |
| budget habis | kamu, sebelumnya | setujui uang tambahan, atau terima hasil parsial |
| error yang tidak bisa kamu retry | provider atau tool | perbaiki deployment; triase Bab 14 yang memutuskan |
| manusia mengintervensi | seseorang | tunggu verdict, lalu lanjutkan |
Menciutkan semua ini menjadi satu boolean adalah kesalahan desain paling umum di file ini, dan mahal dengan cara yang spesifik: tiga dari lima bisa dilanjutkan dan dua tidak. Agent yang terkena turn cap punya transkrip valid, hasil parsial nyata, dan langkah berikutnya; agent yang mendapat 401 tidak punya semua itu. Jadi harness mencatat alasannya sebagai data:
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 };Kerusakan tiga: sebuah tool gagal
Tautan ke bagian: Kerusakan tiga: sebuah tool gagalBab 18 berakhir dengan klaim tanpa angka: kembalikan error sebuah tool ke model sebagai hasil tool alih-alih melemparkannya, dan model biasanya memperbaiki diri. Ini angkanya.
Satu kegagalan, tiga policy. Scripted model menebak file yang tidak ada; tool melempar no such file: timeout.log. Call list_files to see what exists.
| yang dilakukan harness terhadap error | turns | tool runs | cost | yang didapat user |
|---|---|---|---|---|
| melemparnya keluar dari loop | 1 | 1 | $0.000756 | stack trace |
mengembalikan Error: the tool failed. | 2 | 1 | $0.001462 | "Saya tidak bisa membaca file itu, jadi saya tidak tahu." |
| mengembalikan yang sebenarnya terjadi | 4 | 3 | $0.003550 | "errors.log menyebut timeout." |
Baris ketiga berbiaya 4,7 kali baris pertama dan satu-satunya yang menjawab pertanyaan. Dan baris kedua adalah yang menarik, karena itulah yang sebenarnya dilakukan sebagian besar codebase: error ditangkap, loop bertahan, model diberi tahu bahwa sesuatu gagal dan bukan apa, lalu ia menyerah dengan sopan. Perbedaan antara baris dua dan tiga bukan error handling. Itu kalimat yang ditulis untuk seorang pembaca.
Karena itu harness memperlakukan tool yang throw sebagai data, dan membuat wording-nya sebagai policy:
} 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);
}Bab 18 juga memperingatkan tentang sisi lainnya, dan itu juga punya harga. Arahkan loop ke tool yang gagal karena alasan yang tidak bisa diperbaiki pesan apa pun — read yang tidak boleh dilakukan process — dan model me-retry-nya selamanya:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Sebelas eksekusi identik dari call yang tidak mungkin berhasil, 5,2 kali biaya run yang pulih dari error yang bisa diperbaiki, dan tidak ada apa pun di akhir. Error adalah context; error permanen adalah context yang meracuni sisa run. Pembedaan ini adalah triase status Bab 14 yang dipindahkan satu lapisan ke atas: error yang bisa ditindaklanjuti model kembali ke transkrip, dan error yang tidak bisa harus menghentikan run dengan alasan. Turn cap adalah hal yang berdiri antara kamu dan kasus kedua hari ini, yang merupakan lantai, bukan perbaikan.
Kerusakan empat: call yang sama, dua kali
Tautan ke bagian: Kerusakan empat: call yang sama, dua kaliSekarang kegagalan yang diasumsikan kebanyakan orang tidak mungkin terjadi. Model mengulang dirinya. Minta loop apa pun berjalan cukup lama dan kamu akan melihat tool identik dengan argument identik pada dua turn berturut-turut.
Diukur terhadap baseline task yang sama tanpa pengulangan:
| turns | tool runs | cost | |
|---|---|---|---|
| task, tanpa pengulangan | 2 | 1 | $0.001396 |
| task yang sama, satu call diulang | 3 | 2 | $0.002446 |
| diulang, dengan result cache pada tool read-only | 3 | 1 | $0.002446 |
Call duplikat berbiaya $0.001050 ekstra, kenaikan 75 %, dan inilah bagian yang mengejutkan orang: caching hasil tidak memulihkan apa pun darinya. Deduplication menyelamatkan eksekusi tool dan bukan turn, karena saat kodemu menyadari pengulangan itu, model sudah dibayar untuk memintanya. Penghematan itu nyata ketika tool lambat, rate-limited, atau ditagih per call — dan nol pada line item yang tumbuh.
Ada versi yang lebih buruk. Terapkan cache yang sama pada tool yang menulis, dan call kedua diam-diam tidak terjadi:
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"]Mana yang benar? Tidak ada, secara dapat diketahui. Protocol mengatakan ini dua call: keduanya membawa nilai tool_call_id yang berbeda. Argument mengatakan mungkin keduanya satu. Harness yang memutuskan dengan membandingkan string argument suatu hari akan menelan yang kedua dari dua charge identik yang memang disengaja — dan Bab 14 sudah menyebut satu-satunya mekanisme yang menyelesaikan ini secara jujur, yaitu idempotency key yang dibuat per operasi logis oleh lapisan yang tahu operasi itu apa. Sampai tool membawa salah satunya, default yang bisa dipertahankan adalah gate read-only di atas: cache read, eksekusi write, dan biarkan idempotency milik write sendiri menangani sisanya.
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;
}Kerusakan lima: ia menghapus sesuatu
Tautan ke bagian: Kerusakan lima: ia menghapus sesuatuSkrip destructive membuat daftar file lalu meminta menghapus satu file yang tidak pernah disebut task. Tidak ada apa pun dalam loop sejauh ini yang akan menghentikannya.
Tool yang ditandai needsApproval tidak gagal dan tidak lanjut. Ia menghentikan run dan mengembalikan kontrol, dengan semua yang dibutuhkan seseorang untuk memutuskan:
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."Itulah seluruh mekanismenya, dan alasan ia berupa return alih-alih callback adalah bagian berikutnya: antara penghentian dan verdict, process mungkin sudah tidak ada lagi.
Tapi pertama, pengukuran yang tidak diduga siapa pun. Penolakan bukan ketiadaan hasil — transkrip punya slot yang di-key oleh tool_call_id dan sesuatu harus masuk ke dalamnya. Jalankan penolakan yang sama dua kali, hanya mengubah apa yang dikatakan sesuatu itu:
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."Tidak ada yang dihapus dalam kedua run, dan pada run kedua user diberi tahu bahwa itu dihapus. Sistem permission bekerja sempurna; laporannya bohong. Ini mekanisme yang sama seperti tabel tool-error, tiba di tempat yang jauh lebih penting — manusia berkata tidak, action diblokir dengan benar, dan ringkasan agent bertentangan dengan kenyataan karena penolakan tidak pernah ditulis di tempat yang dibaca model. Aturan yang keluar dari sini pendek: apa pun keputusan kodemu tentang sebuah tool call, tulis keputusan itu ke transkrip dalam kata-kata. Bab 30 kembali ke ini dari sisi keamanan, tempat ini menjadi perbedaan antara audit trail dan fiksi.
Kerusakan enam: process mati
Tautan ke bagian: Kerusakan enam: process matiApproval butuh menit atau jam. Deploy butuh detik. Jika run hidup dalam local variable di dalam HTTP request, setiap restart adalah run yang hilang dan setiap approval adalah race.
Jadi run bukan closure. Ia adalah object biasa yang serialisable — messages, turn count, cost, status, interruption, daftar approved call ids — dan loop adalah pure function di atasnya. Satu constraint itu yang membuat persistence menjadi urusan satu baris:
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"));Pertanyaan correctness bukan menyimpan. Pertanyaannya adalah apa yang terjadi saat kembali masuk, dan jawaban naif membuatmu ditagih dua kali. Jika process mati setelah model meminta tool tetapi sebelum hasilnya ditulis, resume yang dimulai dengan memanggil model lagi membayar turn yang sudah dimilikinya — dan jika dimulai dengan menjalankan ulang tool, ia melakukan write dua kali.
Perbaikannya adalah membuat loop mulai dengan bertanya kepada transkrip apa yang masih outstanding:
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));
}Setiap iteration menguras pending terlebih dahulu dan hanya meminta model ketika tidak ada yang outstanding. Resume menjadi code path yang sama dengan yang normal, begitu juga approval — call yang disetujui hanyalah pending call yang kini diizinkan berjalan. Matikan process di tengah task lalu restart:
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.logDua eksekusi tool di dua process untuk task yang membutuhkan dua, dan biaya final identik dengan run yang tidak pernah crash. Cost terakumulasi melintasi restart karena ia ada di state, bukan di variable.
Kerusakan tujuh: tiga menit hening
Tautan ke bagian: Kerusakan tujuh: tiga menit heningscan_archive memakan tiga detik di sini dan mewakili tool yang memakan tiga menit di production. Dua hal hilang saat ia berjalan: user tidak tahu apa pun sedang terjadi, dan tombol Stop tidak melakukan apa-apa.
Keduanya punya perbaikan yang sama, yaitu AbortSignal dari Bab 14 yang didorong satu level lebih dalam. Signal bukan hanya untuk fetch — ia diteruskan ke dalam tool, dan tool yang ditulis dengan baik menghormatinya:
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"Dua milidetik dari klik ke stop, karena sleep di dalam tool mendengarkan signal yang sama dengan fetch. Masukkan hanya ke fetch dan tombol Stop yang identik menunggu tiga detik — sepanjang tool — dan run "cancel" setelah pekerjaan yang sedang dibatalkannya sudah selesai. Cancellation yang tidak dipasang sampai ke bawah hanyalah spinner yang mengatakan kata yang benar.
Trace, dan mengapa ia bukan log
Tautan ke bagian: Trace, dan mengapa ia bukan logHarness mengeluarkan satu baris per event, dan kosakatanya cukup kecil untuk dihafal: 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}Tiga properti membuat ini trace, bukan logging. Setiap baris membawa run id, jadi run yang melintasi tiga process dan dua hari adalah satu query. Setiap baris turn membawa token count-nya sendiri dan running cost, jadi "mengapa run ini berbiaya empat puluh dolar" bisa dijawab setelah kejadian, bukan hanya bisa direproduksi secara teori. Dan run_stopped membawa reason, yaitu field yang mengubah tiket support menjadi jawaban satu baris: agent yang berhenti karena budget dan agent yang crash terlihat identik dari luar dan membutuhkan respons berlawanan.
Aritmetika latency
Tautan ke bagian: Aritmetika latencyBab 13 mengukur time to first token pada hardware milikmu. Bab 14 mengukurnya lewat socket. Agent mengalikannya, dan pengalinya adalah angka yang tidak dipilih siapa pun:
Task tiga turn yang sama, hanya mengubah latency provider:
| provider latency per turn | wall clock, 3 turns |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2.413 ms |
Harness sendiri menyumbang lima belas milidetik pada run tiga turn. Semua sisanya adalah dikalikan angka yang tidak kamu kendalikan — disetel di dalam serving scheduler yang melakukan batching request-mu dengan request orang asing6 — dan dipilih oleh model. Inilah mengapa streaming dari Bab 14 lebih penting di sini daripada di chat dan membantu lebih sedikit: kamu bisa stream turn final, dan empat turn sebelumnya adalah keheningan kecuali harness memancarkan progress. Ini juga seluruh argumen untuk event tool_progress di atas — dalam agent, unit feedback yang jujur bukan token, melainkan step.
Harness yang sama, model nyata di balik port
Tautan ke bagian: Harness yang sama, model nyata di balik portSemua di atas berjalan terhadap scripted provider, yang membuktikan harness dan tidak membuktikan apa pun tentang model. Jadi ubah satu baris — seam dari Bab 14, LLM_BASE_URL — dan arahkan code identik ke Qwen2.5-0.5B-Instruct lokal dengan empat tool yang sama. Enam task atas tiga file yang sama:
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,908msTiga temuan, dan yang ketiga adalah alasan bagian ini ada.
Setiap task selesai persis dalam dua turn. Turn cap tidak pernah terpanggil, budget tidak pernah terpanggil, dan satu-satunya exit loop adalah model menghasilkan prosa. Model setengah miliar parameter tidak beriterasi; ia menjawab pada napas keduanya, entah ia punya yang dibutuhkan atau tidak. Turn count adalah properti model, bukan loop-mu.
Turn rata-rata memakan 6.908 milidetik, jadi tabel latency di atas bukan mainan: pada ukuran ini, run hipotetis delapan turn hampir satu menit wall clock tanpa apa pun di layar.
Dan jawabannya salah. File terbesar adalah errors.log; model membuat daftar file, tidak pernah membacanya, lalu menyebut salah satunya. Task pertama menebak nama file, diberi tahu bahwa file itu tidak ada, lalu menyimpulkan. Harness mengeksekusi tanpa cacat di keenam run. Harness membuat agent bisa dikendalikan, bukan benar — Bab 29 adalah cara kamu mencari tahu yang mana, dan Bab 30 adalah berapa biayanya ketika tidak ada yang melakukannya.
Subagents, dinamai di sini dan ditagih nanti
Tautan ke bagian: Subagents, dinamai di sini dan ditagih nantiSatu tool dalam katalog bisa punya run lain di belakangnya. Interface-nya milik Bab 18 — schema dan endpoint — dan seluruh agent muat di belakangnya karena interface itu sempit:
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";
},
};Tiga hal sudah benar dalam sepuluh baris itu dan ketiganya adalah konsekuensi dari keputusan di atas: child punya window-nya sendiri, jadi transkrip parent menerima ringkasan alih-alih semua yang dibaca child; ia punya limit-nya sendiri, jadi child yang runaway tidak bisa menghabiskan budget parent; dan ia mewarisi signal, jadi satu Stop membatalkan tree. Mengapa window bersih adalah poinnya alih-alih efek samping dibahas di Bab 24; lima pola orchestration — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — dan handoff ada di Bab 25.
Di mana framework berada, dan mengapa kursus ini tidak memakai salah satunya
Tautan ke bagian: Di mana framework berada, dan mengapa kursus ini tidak memakai salah satunyaTidak ada di atas yang harus dibaca sebagai argumen melawan library. Diukur pada 7 September 2026, untuk bulan yang berakhir 29 Agustus:7
| package | downloads that month | what it gives you |
|---|---|---|
ai (Vercel AI SDK) | 89.385.860 | ToolLoopAgent, stopWhen, tool approval, step hooks |
@anthropic-ai/claude-agent-sdk | 41.558.352 | Claude Code harness sebagai library: loop, sessions, hooks, permissions, subagents8 |
@langchain/langgraph | 12.812.815 | loop sebagai state graph eksplisit |
langchain | 11.359.058 | chains, agents, integrations |
@openai/agents | 6.093.155 | agents, handoffs, guardrails |
@mastra/core | 5.914.502 | agents, workflows, memory |
Alasan kursus ini menulis loop dengan tangan alih-alih mengajarkan salah satunya dinyatakan, bukan diimplikasikan, dan bisa diukur. Dalam dua belas bulan sampai 7 September 2026, ai menerbitkan 945 versi dan bergerak dari major 5 ke major 7, dan class agent-nya masih diekspor sebagai Experimental_Agent; langchain menerbitkan 132 versi dalam window yang sama; @openai/agents menerbitkan 83 dan masih di 0.x, lima belas bulan setelah rilis pertamanya.7 Bab yang ditulis terhadap API mana pun dari mereka akan basi dalam satu musim, dan bab ini diterbitkan dalam tiga puluh tiga bahasa, jadi setiap edisi ulang membebani seluruh terjemahan. Yang ada di bawah semuanya tidak bergerak: loop, stopping rule, katalog, executor, sedikit state.
Dan reference implementation sepakat dengan bab ini tentang bagian yang penting. Di ai versi 7.0.93, exit loop bukan angka — ia adalah stopWhen, daftar predicate, dan step count hanya salah satunya: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 itu plural dalam implementasi loop ini yang paling banyak dipakai, dengan alasan yang sama ia plural dalam seratus sembilan puluh enam baris di atas.
Ke mana ini selanjutnya
Tautan ke bagian: Ke mana ini selanjutnyaSekarang kamu punya harness: loop, katalog, executor, lima jalan keluar, run yang persisted, signal yang mencapai tool, dan trace dengan run id di setiap baris. Bab 24, 25, 29 dan 30 membangun di atas file ini, dan 26 sampai 28 di atas apa yang bisa dijangkaunya.
Masih ada satu masalah tersisa, dan pengukuran di atas telah menunjuk ke sana sepanjang jalan. Lihat lagi tabel runaway: 3.431 input tokens pada delapan turn, 337.299 pada seratus. Lihat run yang bekerja: 204, 269, 342. Setiap turn mengirim ulang seluruh transkrip, jadi context sebuah agent terisi oleh riwayatnya sendiri — dan model lebih buruk memakai ujung jauh dari window panjang daripada ujung dekatnya, itulah sebabnya agent yang bagus di turn lima menjadi bingung di turn empat puluh.
Turn cap tidak memperbaiki itu. Ia hanya menghentikanmu membayar untuk menontonnya terjadi. Yang memperbaikinya adalah memutuskan, pada setiap turn, token mana yang layak mendapat window: apa yang dipadatkan, apa yang dipindahkan keluar ke note yang bisa diambil agent, apa yang diserahkan ke subagent dengan window bersih, dan definisi tool mana yang layak membayar pajak permanennya. Bab 24 mengukur ke mana window sebenarnya pergi — dan kejutannya adalah bukan percakapan.
Sumber dan metode
Tautan ke bagian: Sumber dan metodeSetiap angka dalam bab ini berasal dari dua server yang dijelaskan di atas, pada Node 22 melalui loopback interface: scripted provider yang menghitung token dengan encoding o200k_base, dan Qwen/Qwen2.5-0.5B-Instruct di balik endpoint dengan bentuk yang sama, greedy decoding, di CPU. Biaya dihitung dari token count terukur pada tarif yang dibaca Bab 16 pada 6 September 2026 — $2.00 per juta input tokens dan $12.00 per juta output — dan tidak ada request dalam bab ini yang pergi ke paid endpoint. Jawaban model lokal adalah jawaban model kecil; bacalah sebagai bukti tentang loop, yang identik dalam kedua kasus, dan bukan sebagai benchmark tentang apa yang dilakukan model saat ini.
Referensi
Tautan ke bagian: Referensi-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. dan Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Interleaving antara reasoning traces dan actions yang diimplementasikan loop, dan sumber observasi bahwa acting memungkinkan model "handle exceptions" — persis yang diukur tabel tool-error di atas. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. dan Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Perlakuan formal atas apa yang dilakukan loop di atas secara informal: komponen memory modular, action space terstruktur yang mencakup internal memory dan external environments, serta "a generalized decision-making process to choose actions". Bacalah untuk kosakata yang hilang dari istilah industri — khususnya pemisahan working, episodic, semantic dan procedural memory, yang bayangan praktisnya adalah tabel tiga-store Bab 24. ↩
-
ai(Vercel AI SDK) versi 7.0.93, dipublikasikan 4 September 2026; deklarasi tipe dibaca daricdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tspada 7 September 2026. File 397 KB itu berisi nol kemunculan stringharness. Class agent adalahdeclare class ToolLoopAgent, diekspor baik sebagaiToolLoopAgentmaupun sebagaiExperimental_Agent;declare function isStepCount(stepCount: number)— diekspor sebagaistepCountIs— dikutip verbatim di atas;type StopConditionditampilkan tanpa parameter tipe keduanya (RUNTIME_CONTEXT extends Context = Context), yang merupakan satu-satunya elision dalam excerpt, seperti juga bentukstopWhen?: Arrayable<StopCondition<...>>padagenerateTextdanstreamText. File yang sama mendeklarasikantoolApproval,ToolApprovalStatus,prepareStepdanrepairToolCall, yang berarti reference implementation secara independen telah sampai pada approval gates, per-step preparation dan error repair. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. dan Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Abstraknya menyebut artefak itu "evaluation framework" berisi 2.294 problem dan tidak pernah memakai kata "harness"; README proyeknya sendiri (
github.com/SWE-bench/SWE-bench, dibaca 7 September 2026) memakainya lima kali, selalu sebagai "evaluation harness", dan entry point-nya adalahpython -m swebench.harness.run_evaluation. Itulah makna lain dari kata itu: scaffold yang menahan agent tetap diam dan menilainya, bukan loop yang menjalankannya. ↩ -
Anthropic, Building effective agents, 19 Desember 2024,
anthropic.com/engineering/building-effective-agents, dibaca 7 September 2026. Augmented model sebagai building block, agent sebagai LLM yang "using tools based on environmental feedback in a loop", dan rekomendasi stopping conditions "such as a maximum number of iterations" untuk mempertahankan kontrol. Bab 22 mengutip definisinya secara penuh. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. dan Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Loop yang lain — serving scheduler yang melakukan batching request-mu dengan request orang asing dan mengelola KV cache dari Bab 13. Layak mengetahui ia ada justru karena ia bukan milikmu: latency yang dikalikan harness-mu disetel di dalamnya, dan sebanyak apa pun kerja pada loop-mu tidak menggesernya. ↩
-
Jumlah unduhan registry npm,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, window eksplisit alih-alih rollinglast-month, dan riwayat rilis dariregistry.npmjs.org/<package>; keduanya di-query 7 September 2026. Jumlah rilis adalah jumlah versi yang dipublikasikan dalam dua belas bulan sampai tanggal itu, termasuk canary builds:ai945 (terbaru 7.0.93 pada 2026-09-04, dengan major versions 5, 6 dan 7 semuanya muncul di dalam window),langchain132 (terbaru 1.5.10 pada 2026-08-20),@openai/agents83 (terbaru 0.17.0 pada 2026-08-19, pertama dipublikasikan 2025-06-03). ↩ ↩2 -
Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) adalah Claude Code harness yang dikemas sebagai library — agent loop, built-in file and shell tools, context management, sessions, hooks, permissions dan subagents — didokumentasikan dicode.claude.com/docs/en/agent-sdk. Ini hal terdekat dengan laporan terpublikasi tentang setiap mekanisme yang bab ini bangun dengan tangan, dan layak dibaca berdampingan dengan implementasimu sendiri untuk bagian-bagian yang ia beri nama dan bab ini hanya isyaratkan. ↩