Lewati ke konten
23/30Bab 23 dari 30

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.

Bab 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.

mock-provider.mjsJS
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.

Inilah seluruh idenya, sebelum bagian mana pun yang membuatnya bisa bertahan hidup.

loop.tsTS
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:

TEXT
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

Tiga 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 selesai

Arahkan 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 capmodel callsinput tokenscost
883.431$0.009070
202016.259$0.038038
505088.649$0.191098
100100337.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 nn membawa setiap turn sebelumnya dan totalnya adalah Θ(n2)\Theta(n^2). 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 uang

Masalah 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:

harness.tsTS
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:

budgetturns reachedactually spent
$0.019$0.010780
$0.0524$0.051790
$0.2052$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 satu

Sekarang 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 endswho decidedwhat the caller should do
model berhenti memintamodelbaca jawabannya
turn capkamu, sebelumnyanaikkan cap, atau terima hasil parsial
budget habiskamu, sebelumnyasetujui uang tambahan, atau terima hasil parsial
error yang tidak bisa kamu retryprovider atau toolperbaiki deployment; triase Bab 14 yang memutuskan
manusia mengintervensiseseorangtunggu 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:

harness.tsTS
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 };

Bab 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 errorturnstool runscostyang didapat user
melemparnya keluar dari loop11$0.000756stack trace
mengembalikan Error: the tool failed.21$0.001462"Saya tidak bisa membaca file itu, jadi saya tidak tahu."
mengembalikan yang sebenarnya terjadi43$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:

harness.tsTS
} 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:

TEXT
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 kali

Sekarang 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:

turnstool runscost
task, tanpa pengulangan21$0.001396
task yang sama, satu call diulang32$0.002446
diulang, dengan result cache pada tool read-only31$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:

TEXT
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.

harness.tsTS
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;
}

Skrip 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:

harness.tsTS
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) });
}
TEXT
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:

TEXT
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.

Approval 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:

harness.tsTS
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:

harness.tsTS
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:

TEXT
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

Dua 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.

scan_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:

harness.tsTS
result = await tool.run(JSON.parse(c.function.arguments), {
  signal,                                                                     
  progress: (label) => { trace(state.runId, "tool_progress", { toolName: tool.name, label }); opts.onProgress?.(label); },
});
TEXT
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.

Harness mengeluarkan satu baris per event, dan kosakatanya cukup kecil untuk dihafal: turn, tool_start, tool_progress, tool_result, approval_required, run_stopped.

TEXT
{"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.

Bab 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:

TrunN(tmodel+ttools)T_{\text{run}} \approx N \cdot \left( t_{\text{model}} + t_{\text{tools}} \right)

Task tiga turn yang sama, hanya mengubah latency provider:

provider latency per turnwall clock, 3 turns
0 ms15 ms
200 ms615 ms
800 ms2.413 ms

Harness sendiri menyumbang lima belas milidetik pada run tiga turn. Semua sisanya adalah NN dikalikan angka yang tidak kamu kendalikan — disetel di dalam serving scheduler yang melakukan batching request-mu dengan request orang asing6 — dan NN 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 port

Semua 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:

TEXT
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

Tiga 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 nanti

Satu 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:

subagent.tsTS
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 satunya

Tidak ada di atas yang harus dibaca sebagai argumen melawan library. Diukur pada 7 September 2026, untuk bulan yang berakhir 29 Agustus:7

packagedownloads that monthwhat it gives you
ai (Vercel AI SDK)89.385.860ToolLoopAgent, stopWhen, tool approval, step hooks
@anthropic-ai/claude-agent-sdk41.558.352Claude Code harness sebagai library: loop, sessions, hooks, permissions, subagents8
@langchain/langgraph12.812.815loop sebagai state graph eksplisit
langchain11.359.058chains, agents, integrations
@openai/agents6.093.155agents, handoffs, guardrails
@mastra/core5.914.502agents, 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

ai-sdk.tsTS
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>;   // exported as stepCountIs

Stopping itu plural dalam implementasi loop ini yang paling banyak dipakai, dengan alasan yang sama ia plural dalam seratus sembilan puluh enam baris di atas.

Sekarang 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.


Setiap 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.

  1. 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.

  2. 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.

  3. ai (Vercel AI SDK) versi 7.0.93, dipublikasikan 4 September 2026; deklarasi tipe dibaca dari cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts pada 7 September 2026. File 397 KB itu berisi nol kemunculan string harness. Class agent adalah declare class ToolLoopAgent, diekspor baik sebagai ToolLoopAgent maupun sebagai Experimental_Agent; declare function isStepCount(stepCount: number) — diekspor sebagai stepCountIs — dikutip verbatim di atas; type StopCondition ditampilkan tanpa parameter tipe keduanya (RUNTIME_CONTEXT extends Context = Context), yang merupakan satu-satunya elision dalam excerpt, seperti juga bentuk stopWhen?: Arrayable<StopCondition<...>> pada generateText dan streamText. File yang sama mendeklarasikan toolApproval, ToolApprovalStatus, prepareStep dan repairToolCall, yang berarti reference implementation secara independen telah sampai pada approval gates, per-step preparation dan error repair. 2

  4. 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 adalah python -m swebench.harness.run_evaluation. Itulah makna lain dari kata itu: scaffold yang menahan agent tetap diam dan menilainya, bukan loop yang menjalankannya.

  5. 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.

  6. 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.

  7. Jumlah unduhan registry npm, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, window eksplisit alih-alih rolling last-month, dan riwayat rilis dari registry.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: ai 945 (terbaru 7.0.93 pada 2026-09-04, dengan major versions 5, 6 dan 7 semuanya muncul di dalam window), langchain 132 (terbaru 1.5.10 pada 2026-08-20), @openai/agents 83 (terbaru 0.17.0 pada 2026-08-19, pertama dipublikasikan 2025-06-03). 2

  8. 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 di code.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.


Dibuat oleh

David Vicente Campos

Pendiri NeuraLIA Labs & salah satu pendiri MyRealFood

Saya seorang insinyur komputer lulusan Universitas León. Saya ikut mendirikan MyRealFood, tempat saya sebagai CTO membangun aplikasi yang telah digunakan jutaan orang untuk makan lebih sehat, dan saya mendirikan NeuraLIA Labs, tempat saya membangun produk AI. Di sini saya menulis tentang hal-hal yang harus saya pahami sepanjang perjalanan, sebagaimana dulu saya berharap ada yang menjelaskannya kepada saya.

Selengkapnya tentang penulis

Diterbitkan oleh NeuraLIA Labs.

Dapatkan postingan baru di inbox kamu

Berita AI, panduan, dan update produk — email singkat saat kami menerbitkan sesuatu yang layak kamu baca.

Indeks kursus

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev11 menit baca

Model AI Jev dibuat untuk keputusan, bukan prosa

Jev dari TypeSafe AI menarik perhatian karena memperlakukan kecerdasan software sebagai persoalan probabilitas: pilih cabang yang tepat, sertakan keyakinan, dan hindari membayar LLM untuk menulis teks saat kode membutuhkan keputusan.

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering11 menit baca

Rekayasa konteks untuk agen AI jangka panjang

Agen yang berjalan lama tidak gagal hanya karena window-nya kecil. Mereka gagal ketika file, output tool, dan riwayat lama menggeser tugas yang seharusnya diselesaikan agen.

Siap membiarkan LIA yang memilih?

Berkarya dengan semua model AI dalam satu tempat — mulai gratis hari ini.