Agent harness építése: a loop és öt kiútja
Egy 15 soros loop elsőre működik, majd szándékosan hétszer eltörjük — egy runaway példával, ami 77-szer többe került.
Ezen az oldalon
Kezdjük az őszinte résszel, mert más úgysem mondja ki: a „harness” zsargon, nem szabvány. Nincs specifikáció, nincs bizottság, nincs referencia-definíció. A négy tanulmány, amelyre ez a fejezet hivatkozik — ReAct,1 CoALA,2 SWE-bench és vLLM — az absztraktjaikban egyszer sem használja ezt a szót. A dolog legtöbbet letöltött implementációja, a Vercel ai csomagja havi 89,4 millió letöltéssel, szintén nem használja: a harness sztring nulla alkalommal jelenik meg a 7.0.93-as verzióval szállított 397 KB-nyi típusdeklarációban.3 Az egyetlen hely, ahol a szó tényleg jelentést hordoz, egészen mást jelent. A SWE-bench ötször írja le a README-ben, mindig evaluation harness értelemben — azt a konténerizált vázat, amely alkalmaz egy patchet és lefuttatja a teszteket —, a Python modulja pedig szó szerint swebench.harness.run_evaluation.4
Tehát két különböző dolog ugyanazt a nevet viseli. Egy evaluation harness mozdulatlanul tartja az agentet és pontozza. Egy agent harness az a program, amely futtatja az agentet: meghívja a modellt, végrehajtja, amit a modell kér, eldönti, mikor kell megállni, és közben megtartja az állapotot. Ez a fejezet a másodikat építi fel, kétszáz sornál kevesebb TypeScriptben, mindenféle framework nélkül.
Maga a loop tizenöt sor, és első próbálkozásra működik. Minden ezután következő rész egy mód arra, hogy kilépj belőle.
Részletek megjelenítése
Amire ennek a fejezetnek szüksége van a korábbiakból.
- 14. fejezet a klienshez: határidők, státusz-triázs, megszakítás, idempotenciakulcsok és az itt újra használt mock provider technika.
- 16. fejezet a számoláshoz: az input tokenek a beszélgetés négyzetével nőnek, és az alábbi díjak azok, amelyeket ott 2026. szeptember 6-án olvastunk.
- 18. fejezet a tool katalógushoz: egy schema, amelyet a modell lát, egy endpoint, amelyet sosem lát, és a szabály, hogy a hibák kontextusok, nem kivételek.
- 22. fejezet ahhoz a loophoz, amelyet ez megörököl, valamint az „agent” két publikált definíciójához, amelyek nem értenek egyet egymással.
Itt nincsenek tenzorok. Ez a kurzus második függőségi csomópontja: a 24., 25., 29. és 30. fejezet az alábbi fájlon fut, a 26–28. fejezet pedig arra épít, amit ez elér.
Egy provider, amelyet scriptelhetsz
Link a szakaszhoz: Egy provider, amelyet scriptelhetszA 14. fejezetet nem lehetett valódi provider ellen megírni, mert nem kérhetsz tőle 429-et egy kiválasztott pillanatban. Ennek a fejezetnek ugyanez a problémája más alakban: nem kérhetsz meg egy valódi modellt arra, hogy runaway legyen, vagy hogy reprodukálhatóan, kérésre kétszer egymás után ugyanazt a toolt kérje ugyanazokkal az argumentumokkal.
Ezért az első program egy scriptelt provider: egy chat completions API alakú endpoint, amelynek válasza a turn indexétől és attól függ, hogy a toolok eddig mit adtak vissza. Valódi byte-pair encoderrel számolja a tokeneket, így az alábbi pénzösszegek számítások, nem díszletek.
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);Két sor hordozza a dizájnt. A turn indexe a beszélgetésből van levezetve, nem egy változóban tároljuk, ezért a provider állapotmentes, és egy futtatás megölhető, majd folytatható vele szemben. A recover pedig elolvassa a tool eredményeit, mielőtt dönt: egy scriptelt modell, amely elolvassa a saját átiratát, a minimum ahhoz, hogy mérni lehessen, adott-e neki a harness bármit, amit érdemes elolvasni.
A katalógus a 18. fejezeté, négy tool három fájlon keresztül: list_files, read_file, delete_file — needsApproval jelöléssel — és scan_archive, amely szándékosan lassú.
A működő loop
Link a szakaszhoz: A működő loopItt van az egész ötlet, még azelőtt, hogy bejönnének azok a részek, amelyek túlélhetővé teszik.
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 });
}
}Irányítsd a scriptelt providerre, és pontosan azt teszi, aminek látszik:
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, 342Három turn, két tool-végrehajtás, negyed amerikai cent. Figyeld az utolsó sort: 204, 269, 342. Minden turn újraküld mindent, ami előtte volt; ez a 16. fejezet kvadratikus számlája, csak itt úgy érkezik meg, hogy senki sem gépelt semmit. A fejezet többi része arról szól, mi történik, amikor ez a sor nem hagyja abba a növekedést.
Első törés: a feladat, amely sosem ér véget
Link a szakaszhoz: Első törés: a feladat, amely sosem ér végetIrányítsd ugyanezt a loopot a runaway scriptre — egy modellre, amely minden egyes turnben toolt kér, és sosem bocsát ki prózát —, és a megjelölt return sosem fut le. Nincs más kijárat. A program addig fut, amíg a process meg nem hal, vagy a bankkártya fel nem adja.
A javítás egy sor, ez az első kontroll, amelyet az irodalom ajánl,5 és végül mindenki megírja. Amit szinte senki sem tesz meg: megméri, mennyit ér:
| 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 |
Olvasd együtt az utolsó két sort. A cap 50-ről 100-ra duplázása nem duplázta a költséget; 3,7-szeresére szorozta. Az input tokenek 88,649-ről 337,299-re nőttek, 3,8-szorosára, mert a turn minden korábbi turnt visz magával, az összeg pedig . A turn cap nem lineáris tekerő. A legrosszabb eset négyzetgyökén van a tekerő, ezért a 20-ról 100-ra emelés „csak a biztonság kedvéért” olyan döntés, amelyet érdemes beárazni, mielőtt meghozod.
Második törés: a turn cap nem költségplafon
Link a szakaszhoz: Második törés: a turn cap nem költségplafonA turn cap baja az, hogy egy turnnek nincs fix ára. Húsz turn egy rövid átiraton fent $0.038-ba került. Húsz turn egy 200 toolból álló katalógussal, egy retrieved dokumentumkészlettel és negyven előzményüzenettel ennek százszorosaiba kerülhet, a cap pedig erről nem tud. Az operátor azt akarja korlátozni, ami számlaként megjelenik.
Ezért a loop pénzt számol, a 16. fejezet computeCost kódját használva az ott olvasott díjak ellenében — $2.00 egymillió input tokenenként és $12.00 egymillió output tokenenként ahhoz a modellhez, amelyet a kurzus végig áraz:
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);Ugyanaz a runaway script, turn cap nélkül, három büdzsével:
| budget | turns reached | actually spent |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
Két dolgot érdemes néven nevezni. Először: a budget minden alkalommal más számú turnt vásárol, és ez a lényeg: azt korlátozza, ami az operátort érdekli, a turnszámot pedig hagyja oda esni, ahová az átirat teszi. Másodszor: minden sor túllő. A budget $0.010 volt, és $0.010780 ment el, mert az ellenőrzés a turn előtt fut, a turn ára pedig csak utána ismert. A költést nem tudod pontosan korlátozni; legfeljebb egy turn költségén belül. Mondd ezt ki a felületen ahelyett, hogy úgy tennél, mintha nem így lenne, és tedd az ellenőrzést a hívás elé, hogy a túllövés egy turn legyen, ne kettő.
Öt mód a loop elhagyására, nem egy
Link a szakaszhoz: Öt mód a loop elhagyására, nem egyMostanra a loopnak három kijárata van, és látszik a fejezet hátralévő része. Egy production futtatás pontosan ötféleképpen ér véget, és ezek nem egymás variációi:
| how it ends | who decided | what the caller should do |
|---|---|---|
| the model stopped asking | the model | read the answer |
| turn cap | you, in advance | raise the cap, or accept a partial result |
| budget exhausted | you, in advance | approve more money, or accept a partial result |
| an error you cannot retry | the provider or a tool | fix the deployment; Chapter 14's triage decides |
| a human intervened | a person | wait for a verdict, then resume |
Ezeket egyetlen boolean értékbe összevonni a leggyakoribb tervezési hiba ebben a fájlban, és nagyon konkrét módon drága: az ötből három folytatható, kettő nem. Egy agent, amely elérte a turn capet, érvényes átirattal, valódi részleges eredménnyel és következő lépéssel rendelkezik; egy agent, amely 401-et kapott, ezek egyikével sem. Ezért a harness adatként rögzíti az okot:
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 };Harmadik törés: egy tool elhasal
Link a szakaszhoz: Harmadik törés: egy tool elhasalA 18. fejezet számszerűsítés nélküli állítással zárult: add vissza a tool hibáját a modellnek tool resultként ahelyett, hogy kivételt dobnál, és a modell általában kijavítja magát. Itt a szám.
Egy hiba, három policy. A scriptelt modell olyan fájlt tippel, amely nem létezik; a tool no such file: timeout.log. Call list_files to see what exists. kivételt dob
| what the harness does with the error | turns | tool runs | cost | what the user got |
|---|---|---|---|---|
| throws it out of the loop | 1 | 1 | $0.000756 | a stack trace |
returns Error: the tool failed. | 2 | 1 | $0.001462 | „Nem tudtam elolvasni a fájlt, ezért nem tudom.” |
| returns what actually happened | 4 | 3 | $0.003550 | „az errors.log timeoutot említ.” |
A harmadik sor 4,7-szer annyiba kerül, mint az első, és ez az egyetlen, amely megválaszolja a kérdést. A második sor az érdekes, mert a legtöbb codebase valójában ezt csinálja: a hibát elkapta, a loop túlélte, a modell megtudta, hogy valami elromlott, de azt nem, mi, és udvariasan feladta. A második és harmadik sor közötti különbség nem error handling. Hanem egy mondat, amelyet egy olvasónak írtak.
A harness ezért adatként kezeli a kidobott toolt, és a megfogalmazást policyvé teszi:
} 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);
}A 18. fejezet a másik oldalra is figyelmeztetett, és annak is ára van. Irányítsd a loopot egy olyan toolra, amely olyan okból hibázik, amelyet semmilyen üzenet nem tud kijavítani — egy olvasásra, amelyet a process nem végezhet el —, és a modell örökké újrapróbálja:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Tizenegy azonos végrehajtása egy hívásnak, amely nem sikerülhet, 5,2-szeres költség a javítható hibából felépülő futtatáshoz képest, és a végén semmi. A hibák kontextusok; a permanens hiba olyan kontextus, amely megmérgezi a futtatás többi részét. A különbség a 14. fejezet státusz-triázsa egy réteggel feljebb: az a hiba, amelyre a modell cselekedhet, visszamegy az átiratba, amelyre nem, annak okkal meg kell állítania a futtatást. Ma a turn cap áll közted és a második eset között; ez padló, nem javítás.
Negyedik törés: ugyanaz a hívás, kétszer
Link a szakaszhoz: Negyedik törés: ugyanaz a hívás, kétszerMost jön az a hiba, amelyről a legtöbben azt hiszik, nem történhet meg. A modellek ismétlik magukat. Kérj meg bármelyik loopot, hogy elég sokáig fusson, és látni fogod ugyanazt a toolt ugyanazokkal az argumentumokkal két egymást követő turnben.
Az ismétlés nélküli ugyanazon feladat baseline-jához mérve:
| turns | tool runs | cost | |
|---|---|---|---|
| the task, no repeat | 2 | 1 | $0.001396 |
| the same task, one call repeated | 3 | 2 | $0.002446 |
| repeated, with a result cache on read-only tools | 3 | 1 | $0.002446 |
A duplikált hívás $0.001050 extra költség volt, 75%-os növekedés, és itt jön az a rész, amely meglepi az embereket: az eredmény cache-elése semmit nem hozott vissza ebből. A deduplikáció megspórolta a tool-végrehajtást, de nem a turnt, mert mire a kódod észreveszi az ismétlést, a modellért már fizettél, amiért kérte. A megtakarítás valós, ha a tool lassú, rate-limited vagy hívásonként számlázott — és nulla azon a költségsoron, amely megnőtt.
Van rosszabb verzió. Alkalmazd ugyanazt a cache-t egy író toolra, és a második hívás csendben nem történik meg:
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"]Melyik a helyes? Tudhatóan egyik sem. A protokoll szerint ez két hívás: két különböző tool_call_id értéket hordoznak. Az argumentumok szerint lehet, hogy egy. Egy harness, amely argumentumsztringek összehasonlításával dönt, egy nap el fogja nyelni két azonos, szándékolt terhelés közül a másodikat — a 14. fejezet pedig már megnevezte az egyetlen mechanizmust, amely ezt tisztességesen feloldja: az idempotenciakulcsot, amelyet logikai műveletenként az a réteg generál, amely tudja, mi a művelet. Amíg a tool nem hordoz ilyet, a védhető alapértelmezés a fenti read-only kapu: cache-eld az olvasásokat, hajtsd végre az írásokat, a maradékot pedig bízd az írás saját idempotenciájára.
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;
}Ötödik törés: töröl valamit
Link a szakaszhoz: Ötödik törés: töröl valamitA destructive script listázza a fájlokat, majd egy olyat kér törölni, amelyet a feladat sosem említett. Az eddigi loopban semmi sem állítaná meg.
Egy needsApproval jelölésű tool nem hibázik és nem is halad tovább. Megállítja a futtatást és visszaadja az irányítást, mindennel együtt, amire egy embernek szüksége van a döntéshez:
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."Ez az egész mechanizmus, és azért return, nem callback, mert a következő szakasz erről szól: a megállás és az ítélet között lehet, hogy a process már nem is létezik.
De előbb a mérés, amelyre senki sem számít. Az elutasítás nem eredmény hiánya — az átiratban van egy tool_call_id kulcsú slot, és valaminek bele kell kerülnie. Futtasd ugyanazt az elutasítást kétszer, csak azt változtatva, hogy ez a valami mit mond:
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."Egyik futtatásban sem törlődött semmi, a másodikban mégis azt mondják a usernek, hogy törlődött. A jogosultsági rendszer tökéletesen működött; a riport hazugság. Ugyanaz a mechanizmus, mint a tool-hiba táblában, csak sokkal fontosabb helyre érkezve — egy ember nemet mondott, a művelet helyesen blokkolva lett, az agent összefoglalója pedig ellentmond a valóságnak, mert az elutasítás sosem lett leírva oda, ahol a modell olvas. Az ebből következő szabály rövid: bármit is dönt a kódod egy tool callról, írd be a döntést az átiratba, szavakkal. A 30. fejezet a biztonsági oldalról tér vissza ehhez, ahol ez a különbség audit trail és fikció között.
Hatodik törés: meghal a process
Link a szakaszhoz: Hatodik törés: meghal a processEgy approval percekig vagy órákig tart. Egy deploy másodpercekig. Ha a futtatás egy HTTP requesten belüli lokális változóban él, minden restart elveszett futtatás, és minden approval versenyhelyzet.
Ezért a futtatás nem closure. Hanem egy egyszerűen szerializálható objektum — üzenetek, turnszám, költség, státusz, megszakítás, a jóváhagyott call id-k listája —, a loop pedig tiszta függvény rajta. Ez az egyetlen megkötés teszi a perzisztenciát egysoros üggyé:
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"));A helyességi kérdés nem a mentés. Hanem az, mi történik visszatéréskor, és a naiv válasz duplán terhel. Ha a process azután halt meg, hogy a modell toolt kért, de az eredmény még nem lett beírva, egy olyan resume, amely azzal kezdi, hogy újra meghívja a modellt, fizet egy turnért, amely már megvan — ha pedig azzal kezdi, hogy újrafuttatja a toolokat, kétszer végez el egy írást.
A javítás az, hogy a loop azzal kezdődjön: megkérdezi az átiratot, mi van még függőben:
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));
}Minden iteráció először kiüríti a pending listát, és csak akkor kérdezi a modellt, amikor nincs semmi függőben. A resume ugyanaz a kódút lesz, mint a normál futás, és ugyanígy az approval is — egy jóváhagyott call egyszerűen egy pending call, amelyet most már szabad futtatni. Öld meg a processt feladat közben, majd indítsd újra:
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.logKét tool-végrehajtás két processen át egy olyan feladathoz, amelynek kettő kell, a végső költség pedig azonos azzal a futtatással, amely sosem omlott össze. A költség azért gyűlik tovább a restarton át, mert az állapotban volt, nem egy változóban.
Hetedik törés: három perc csend
Link a szakaszhoz: Hetedik törés: három perc csendA scan_archive itt három másodpercig tart, és azt a toolt helyettesíti, amely productionben három percig fut. Két dolog hiányzik, amíg fut: a usernek fogalma sincs, hogy bármi történik, és a Stop gomb semmit sem csinál.
Mindkettőnek ugyanaz a javítása, és ez a 14. fejezet AbortSignal eleme egy szinttel mélyebbre tolva. A signal nem csak a fetchnek szól — bekerül a toolba is, és egy jól megírt tool tiszteletben tartja:
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"Két ezredmásodperc a kattintástól a megállásig, mert a tool belsejében lévő sleep ugyanarra a signalra figyel, mint a fetch. Fűzd csak a fetch hívásba, és ugyanaz a Stop gomb három másodpercet vár — a tool hosszát —, a futtatás pedig azután „szakad meg”, hogy a munka, amelyet megszakított, már befejeződött. A cancellation, amely nincs végig levezetve, csak egy spinner, amely a megfelelő szót írja ki.
A trace, és miért nem log
Link a szakaszhoz: A trace, és miért nem logA harness eseményenként egy sort bocsát ki, és a szókészlet elég kicsi ahhoz, hogy megjegyezd: 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}Három tulajdonság teszi ezt trace-szé, nem logginggá. Minden sor hordozza a run id-t, így egy három processen és két napon átívelő futtatás egyetlen query. Minden turn sor hordozza a saját token számait és a futó költséget, így a „miért került ez a futtatás negyven dollárba” utólag megválaszolható, nem csak elméletben reprodukálható. A run_stopped pedig hordozza az okot, vagyis azt a mezőt, amely egy support ticketből egysoros választ csinál: egy budgetnél megállt agent és egy összeomlott agent kívülről ugyanúgy néz ki, de ellentétes reakciót igényel.
A latency aritmetikája
Link a szakaszhoz: A latency aritmetikájaA 13. fejezet az általad birtokolt hardveren mérte a time to first tokent. A 14. fejezet socketen keresztül mérte. Egy agent megszorozza, és a szorzó olyan szám, amelyet senki sem választott:
Ugyanaz a három turnös feladat, csak a provider latency változik:
| provider latency per turn | wall clock, 3 turns |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2,413 ms |
Maga a harness tizenöt ezredmásodpercet ad hozzá egy három turnös futtatáshoz. Minden más szorozva egy olyan számmal, amelyet nem te kontrollálsz — egy serving scheduler belsejében állítják be, amely a kérésedet idegenek kéréseivel batch-eli6 —, a -et pedig a modell választja. Ezért számít itt jobban a 14. fejezet streamingje, mint egy chatben, és ezért segít kevesebbet: streamelheted az utolsó turnt, az előtte lévő négy turn pedig csend, hacsak a harness nem bocsát ki progresszt. Ez a teljes érv a fenti tool_progress event mellett is — egy agentben a visszajelzés őszinte egysége nem a token, hanem a lépés.
Ugyanez a harness, valódi modellel a port mögött
Link a szakaszhoz: Ugyanez a harness, valódi modellel a port mögöttFent minden scriptelt provider ellen futott, ami bizonyítja a harnesst, de semmit sem bizonyít a modellekről. Változtass meg tehát egy sort — a 14. fejezet seamjét, a LLM_BASE_URL elemet —, és irányítsd ugyanazt a kódot egy lokális Qwen2.5-0.5B-Instruct modellre ugyanazzal a négy toollal. Hat feladat ugyanazon a három fájlon:
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,908msHárom megállapítás, és a harmadik miatt létezik ez a szakasz.
Minden egyes feladat pontosan két turnben fejeződött be. A turn cap sosem sült el, a budget sosem sült el, és a loop egyetlen kijárata az volt, hogy a modell prózát adott. Egy félmilliárd paraméteres modell nem iterál; a második levegővételére válaszol, függetlenül attól, megvan-e neki, amire szüksége van. A turnszám a modell tulajdonsága, nem a loopodé.
Az átlagos turn 6,908 ezredmásodpercig tartott, tehát a fenti latency táblázat nem játék: ekkora méretnél egy hipotetikus nyolc turnös futtatás majdnem egy perc wall clock, semmivel a képernyőn.
És a válaszok rosszak. A legnagyobb fájl errors.log; a modell listázta a fájlokat, sosem olvasta el őket, mégis megnevezett egyet. Az első feladat tippelt egy fájlnevet, megtudta, hogy nem létezik, majd levonta a következtetést. A harness mind a hat futtatásban hibátlanul végrehajtott. A harness az agentet irányíthatóvá teszi, nem helyessé — a 29. fejezet szól arról, hogyan deríted ki, melyikről van szó, a 30. fejezet pedig arról, mennyibe kerül, ha senki sem tette meg.
Subagentek, itt megnevezve, később kiszámlázva
Link a szakaszhoz: Subagentek, itt megnevezve, később kiszámlázvaA katalógus egyik toolja mögött egy másik futtatás állhat. Az interface a 18. fejezeté — egy schema és egy endpoint —, és egy teljes agent befér mögé, mert ez az interface szűk:
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";
},
};Három dolog már ebben a tíz sorban is helyes, és mindhárom a fent meghozott döntések következménye: a childnak saját window-ja van, így a parent átirata összefoglalót kap, nem mindent, amit a child olvasott; saját limitjei vannak, így egy runaway child nem költheti el a parent budgetjét; és örökli a signalt, így egy Stop leállítja a fát. Hogy miért a tiszta window a lényeg, nem mellékhatás, az a 24. fejezet; az öt orchestration pattern — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — és a handoff pedig a 25. fejezet.
Hol vannak a frameworkök, és ez a kurzus miért nem használt egyet sem
Link a szakaszhoz: Hol vannak a frameworkök, és ez a kurzus miért nem használt egyet semA fentiek közül semmit sem szabad libraryk elleni érvként olvasni. 2026. szeptember 7-én mérve, az augusztus 29-ével záruló hónapra: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 | a Claude Code harness libraryként: loop, sessionök, hookok, jogosultságok, subagentek8 |
@langchain/langgraph | 12,812,815 | a loop explicit állapotgráfként |
langchain | 11,359,058 | chains, agentek, integrációk |
@openai/agents | 6,093,155 | agentek, handoffok, guardrailek |
@mastra/core | 5,914,502 | agentek, workflow-k, memória |
Az ok, amiért ez a kurzus kézzel írja meg a loopot, nem pedig ezek egyikét tanítja, kimondott, nem sugallt, és mérhető. A 2026. szeptember 7-ig tartó tizenkét hónapban a ai 945 verziót publikált, és major 5-ről major 7-re lépett, az agent class pedig még mindig Experimental_Agent néven exportálódik; a langchain ugyanebben az időablakban 132 verziót publikált; a @openai/agents 83-at publikált, és tizenöt hónappal az első release után még mindig 0.x-en áll.7 Egy olyan fejezet, amely ezek bármelyikének API-jára épül, egy évszakon belül elavul, ez pedig harminchárom nyelven jelenik meg, így minden újrakiadás az egész fordításba kerül. Ami mindegyik alatt van, nem mozog: egy loop, egy stopping rule, egy katalógus, egy executor, némi állapot.
A referencia-implementáció pedig egyetért ezzel a fejezettel abban a részben, amely számít. A ai 7.0.93-as verziójában a loop kijárata nem szám — hanem stopWhen, predikátumok listája, amelyek közül a lépésszám csak az egyik:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsA megállás többes számú ennek a loopnak a legtöbbet használt implementációjában, ugyanazért, amiért a fenti százkilencvenhat sorban is többes számú.
Merre tovább
Link a szakaszhoz: Merre továbbMost már van harnessed: egy loop, egy katalógus, egy executor, öt kiút, perzisztált futtatás, egy signal, amely eléri a toolokat, és egy trace, amelynek minden sorában run id van. A 24., 25., 29. és 30. fejezet erre a fájlra épít, a 26–28. pedig arra, amit elér.
Egyetlen problémája maradt, és a fenti mérések végig erre mutattak. Nézd meg újra a runaway táblát: 3,431 input token nyolc turnnél, 337,299 száznál. Nézd meg a működő futtatást: 204, 269, 342. Minden turn újraküldi a teljes átiratot, így egy agent contextje a saját történetével telik meg — a modell pedig rosszabbul használja egy hosszú window távoli végét, mint a közeli végét, ezért a turn ötben jó agent turn negyvenben zavarodott agent lesz.
A turn cap ezt nem javítja. Csak megállítja, hogy fizess azért, hogy végignézd. Az javítja, ha minden egyes turnben eldöntöd, mely tokenek érdemlik meg a window-t: mit tömörítesz, mit mozgatsz ki egy jegyzetbe, amelyet az agent lekérhet, mit adsz át egy subagentnek tiszta window-val, és mely tool definíciók érik meg az állandó adójukat. A 24. fejezet megméri, valójában hová megy a window — és a meglepetés az, hogy nem a beszélgetésbe.
Források és módszer
Link a szakaszhoz: Források és módszerEbben a fejezetben minden szám a fent leírt két szerverből származott, Node 22 alatt, loopback interface-en: egy scriptelt provider, amely a o200k_base encodinggal számolja a tokeneket, és Qwen/Qwen2.5-0.5B-Instruct ugyanilyen alakú endpoint mögött, greedy decodinggal, CPU-n. A költségeket mért token counts alapján számoltuk azokkal a díjakkal, amelyeket a 16. fejezet 2026. szeptember 6-án olvasott — $2.00 egymillió input tokenenként és $12.00 egymillió output tokenenként —, és ebben a fejezetben egyetlen request sem ment fizetős endpointnak. A lokális modell válaszai egy kis modell válaszai; a loopról szóló bizonyítékként olvasd őket, amely mindkét esetben azonos, ne pedig benchmarkként arról, mire képesek a jelenlegi modellek.
Hivatkozások
Link a szakaszhoz: Hivatkozások-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. és Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). A reasoning trace-ek és actionök váltakozása, amelyet a loop implementál, valamint annak a megfigyelésnek a forrása, hogy a cselekvés lehetővé teszi a modellnek az „exceptions” kezelését — pontosan ezt méri a fenti tool-hiba tábla. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. és Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Annak formális tárgyalása, amit a fenti loop informálisan végez: moduláris memóriakomponensek, strukturált cselekvési tér belső memória és külső környezetek között, valamint „a generalized decision-making process to choose actions”. Olvasd a szókészletért, amely az iparági kifejezésből hiányzik — különösen a working, episodic, semantic és procedural memory szétválasztásáért, amelynek gyakorlati árnyéka a 24. fejezet háromtárolós táblája. ↩
-
ai(Vercel AI SDK) 7.0.93-as verzió, publikálva 2026. szeptember 4-én; a típusdeklarációkat acdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tsfájlból olvastuk 2026. szeptember 7-én. A 397 KB-os fájl nulla előfordulását tartalmazza aharnesssztringnek. Az agent classdeclare class ToolLoopAgent, exportálvaToolLoopAgentésExperimental_Agentnéven is; adeclare function isStepCount(stepCount: number)—stepCountIsnéven exportálva — fent szó szerint idézve; atype StopConditiona második típusparamétere (RUNTIME_CONTEXT extends Context = Context) nélkül szerepel, ez az egyetlen kihagyás a részletben, ahogy astopWhen?: Arrayable<StopCondition<...>>alakja is agenerateTextésstreamTextelemen. Ugyanez a fájl deklarálja atoolApproval,ToolApprovalStatus,prepareStepésrepairToolCallelemeket is, vagyis a referencia-implementáció önállóan is eljutott az approval kapukhoz, a lépésenkénti előkészítéshez és a hibajavításhoz. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. és Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Az absztrakt az artefaktumot 2,294 problémából álló „evaluation frameworkként” nevezi meg, és sosem használja a „harness” szót; a projekt saját README-je (
github.com/SWE-bench/SWE-bench, olvasva 2026. szeptember 7-én) ötször használja, mindig „evaluation harness” értelemben, a belépési pont pedigpython -m swebench.harness.run_evaluation. Ez a szó másik jelentése: egy váz, amely mozdulatlanul tartja az agentet és pontozza, nem az a loop, amely futtatja. ↩ -
Anthropic, Building effective agents, 2024. december 19.,
anthropic.com/engineering/building-effective-agents, olvasva 2026. szeptember 7-én. Az augmented model mint építőelem, az agent mint olyan LLM, amely „using tools based on environmental feedback in a loop”, valamint a kontroll fenntartására ajánlott stopping conditionök „such as a maximum number of iterations”. A 22. fejezet teljes egészében idézi a definícióját. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. és Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). A másik loop — az a serving scheduler, amely a kérésedet idegenek kéréseivel batch-eli, és kezeli a 13. fejezet KV cache-ét. Pont azért érdemes tudni a létezéséről, mert nem a tiéd: a latency, amelyet a harnessed megszoroz, benne van beállítva, és a loopodon végzett bármennyi munka sem mozdítja el. ↩
-
npm registry letöltésszámok,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, explicit időablakkal, nem a gördülőlast-monthablakkal, valamint release historyk aregistry.npmjs.org/<package>alapján; mindkettő lekérdezve 2026. szeptember 7-én. A release-számok az adott dátumig tartó tizenkét hónapban publikált verziók számai, canary buildekkel együtt:ai945 (legfrissebb 7.0.93, 2026-09-04, major 5, 6 és 7 verziók is megjelentek az ablakon belül),langchain132 (legfrissebb 1.5.10, 2026-08-20),@openai/agents83 (legfrissebb 0.17.0, 2026-08-19, első publikálás 2025-06-03). ↩ ↩2 -
A Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) a Claude Code harness libraryként csomagolva — agent loop, beépített file és shell toolok, context management, sessionök, hookok, jogosultságok és subagentek — dokumentálva itt:code.claude.com/docs/en/agent-sdk. Ez áll a legközelebb egy publikált beszámolóhoz mindazokról a mechanizmusokról, amelyeket ez a fejezet kézzel épít fel, és érdemes a saját implementációd mellett olvasni azokért a részekért, amelyeket megnevez, ez a fejezet pedig csak jelez. ↩