Rakenna agent harness: loop ja sen viisi poistumistietä
15 rivin loop toimii heti — ja rikotaan tahallaan 7 kertaa, alkaen karkurista, joka maksoi 77× tiukasti rajatun ajon.
Tällä sivulla
Aloita rehellisestä osasta, koska kukaan muu ei sano sitä: "harness" on jargonia, ei standardi. Sillä ei ole spesifikaatiota, komiteaa eikä viitemääritelmää. Neljä paperia, joihin tämä luku viittaa — ReAct,1 CoALA,2 SWE-bench ja vLLM — eivät käytä sanaa kertaakaan abstrakteissaan. Yleisimmin ladattu toteutus tästä asiasta, Vercelin ai-paketti 89,4 miljoonalla kuukausilatauksella, ei käytä sitä myöskään: merkkijono harness esiintyy nolla kertaa version 7.0.93 mukana toimitetuissa 397 kt:n tyyppimäärityksissä.3 Se yksi paikka, jossa sana on rakenteen kannalta kantava, tarkoittaa aivan muuta. SWE-bench sanoo README-tiedostossaan "harness" viisi kertaa, aina muodossa evaluation harness — kontitettu teline, joka soveltaa patchin ja ajaa testit — ja sen Python-moduuli on kirjaimellisesti swebench.harness.run_evaluation.4
Kaksi eri asiaa siis jakaa nimen. Evaluation harness pitää agentin paikallaan ja pisteyttää sen. Agent harness on ohjelma, joka ajaa agentia: se kutsuu mallia, suorittaa sen pyytämät asiat, päättää milloin pysähtyä ja pitää tilan välissä. Tämä luku rakentaa jälkimmäisen alle kahdessasadassa TypeScript-rivissä, ilman frameworkia.
Loop itsessään on viisitoista riviä ja toimii ensimmäisellä yrityksellä. Kaikki sen jälkeen on tapa poistua siitä.
Näytä lisätiedot
Mitä tämä luku tarvitsee aiemmista luvuista.
- Luku 14 clientiä varten: määräajat, statuksen triage, peruutus, idempotency keyt ja mock provider -tekniikka, jota käytetään tässä uudelleen.
- Luku 16 aritmetiikkaa varten: input tokens kasvavat keskustelun neliön mukana, ja alla käytetyt hinnat ovat ne, jotka siellä luettiin 6. syyskuuta 2026.
- Luku 18 työkalukatalogia varten: schema, jonka malli näkee, endpoint, jota se ei koskaan näe, ja sääntö, että virheet ovat contextia eivätkä poikkeuksia.
- Luku 22 loopia varten, jonka tämä perii, sekä kahta julkaistua "agentin" määritelmää varten, jotka ovat eri mieltä keskenään.
Ei tensoreita täällä. Tämä on kurssin toinen riippuvuuskeskittymä: luvut 24, 25, 29 ja 30 ajavat alla olevalla tiedostolla, ja luvut 26–28 rakentuvat sen varaan, mihin se ylettyy.
Provider, jota voi skriptata
Linkki osioon: Provider, jota voi skriptataLukua 14 ei voinut kirjoittaa oikeaa provideria vasten, koska et voi pyytää siltä 429-vastausta valitulla hetkellä. Tässä luvussa on sama ongelma eri muodossa: et voi pyytää oikeaa mallia karkaamaan käsistä tai pyytämään samaa työkalua kahdesti peräkkäin pyynnöstä ja toistettavasti.
Ensimmäinen ohjelma on siis skriptattu provider: endpoint, joka näyttää chat completions API:lta ja jonka vastaus riippuu vuoron indeksistä sekä siitä, mitä työkalut ovat tähän mennessä palauttaneet. Se laskee tokens oikealla byte-pair encoderilla, joten alla oleva raha on aritmetiikkaa eikä koristelua.
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);Kaksi riviä kantaa designin. Vuoron indeksi johdetaan keskustelusta, sitä ei pidetä muuttujassa, joten provider on tilaton ja ajo voidaan tappaa ja jatkaa sitä vasten. Ja recover lukee työkalujen tulokset ennen päätöstä: skriptattu malli, joka lukee oman transkriptinsa, on vähimmäisvaatimus sille, että voidaan mitata, antoiko harness sille mitään lukemisen arvoista.
Katalogi on luvusta 18: neljä työkalua kolmessa tiedostossa: list_files, read_file, delete_file — merkitty needsApproval — ja scan_archive, joka on tarkoituksella hidas.
Loop, joka toimii
Linkki osioon: Loop, joka toimiiTässä on koko idea ennen osia, jotka tekevät siitä selviytymiskelpoisen.
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 });
}
}Osoita se skriptattuun provideriin, ja se tekee täsmälleen sitä miltä näyttää:
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, 342Kolme vuoroa, kaksi työkalusuoritusta, neljännes Yhdysvaltain senttiä. Huomaa viimeinen rivi: 204, 269, 342. Jokainen vuoro lähettää kaiken sitä edeltävän uudelleen, mikä on luvun 16 neliöllinen lasku paikassa, jossa kukaan ei kirjoittanut mitään. Loppu tästä luvusta kertoo, mitä tapahtuu, kun tuo rivi ei lakkaa kasvamasta.
Rikko yksi: tehtävä, joka ei lopu koskaan
Linkki osioon: Rikko yksi: tehtävä, joka ei lopu koskaanOsoita sama loop runaway-skriptiin — malliin, joka pyytää työkalua joka ikisellä vuorolla eikä koskaan tuota proosaa — ja merkitty return ei koskaan laukea. Muuta poistumistietä ei ole. Ohjelma pyörii, kunnes prosessi kuolee tai luottokortti kuolee.
Korjaus on yksi rivi, se on ensimmäinen kontrolli jota kirjallisuus suosittelee,5 ja jokainen kirjoittaa sen lopulta. Se, mitä lähes kukaan ei tee, on mitata mitä se on arvokas:
| vuorokatto | mallikutsut | input tokens | kustannus |
|---|---|---|---|
| 8 | 8 | 3 431 | $0.009070 |
| 20 | 20 | 16 259 | $0.038038 |
| 50 | 50 | 88 649 | $0.191098 |
| 100 | 100 | 337 299 | $0.702198 |
Lue kaksi viimeistä riviä yhdessä. Katon tuplaaminen 50:stä 100:aan ei tuplannut kustannusta; se kertoi sen 3,7:llä. Input tokens nousivat 88 649:stä 337 299:ään, kertoimella 3,8, koska vuoro kantaa mukanaan kaikki aiemmat vuorot ja kokonaisuus on . Vuorokatto ei ole lineaarinen säädin. Se on säädin pahimman tapauksen neliöjuuressa, minkä vuoksi sen nostaminen 20:stä 100:aan "varmuuden vuoksi" on päätös, joka kannattaa hinnoitella ennen kuin teet sen.
Rikko kaksi: vuorokatto ei ole rahakatto
Linkki osioon: Rikko kaksi: vuorokatto ei ole rahakattoVuorokaton ongelma on, ettei vuorolla ole kiinteää hintaa. Kaksikymmentä vuoroa lyhyellä transkriptilla maksoi yllä $0.038. Kaksikymmentä vuoroa 200 työkalun katalogilla, noudetulla dokumenttijoukolla ja neljälläkymmenellä historiaviestillä maksaa satoja kertoja enemmän, eikä katto tiedä sitä. Operaattori haluaa rajata laskun.
Loop laskee siis rahaa käyttäen luvun 16 computeCost-koodia siellä luettuja hintoja vasten — $2.00 miljoonalta input tokenilta ja $12.00 miljoonalta output tokenilta mallille, jota hinnoitellaan läpi tämän kurssin:
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);Sama karkuriscripti, ei vuorokattoa lainkaan, kolme budjettia:
| budjetti | saavutetut vuorot | oikeasti käytetty |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
Kaksi asiaa kannattaa nimetä. Ensinnäkin budjetti ostaa joka kerta eri määrän vuoroja, mikä on tarkoituskin: se rajaa asian, josta operaattori välittää, ja antaa vuoromäärän asettua siihen, mihin transkripti sen vie. Toiseksi jokainen rivi ylittää budjetin. Budjetti oli $0.010 ja rahaa käytettiin $0.010780, koska tarkistus tehdään ennen vuoroa eikä vuoron hintaa tiedetä ennen kuin se on ohi. Et voi rajata kulua täsmälleen; voit rajata sen yhden vuoron kustannuksen tarkkuuteen. Sano se käyttöliittymässä sen sijaan, että teeskentelet, ja tee tarkistus ennen kutsua, jotta ylitys on yksi vuoro eikä kaksi.
Viisi tapaa poistua loopista, ei yksi
Linkki osioon: Viisi tapaa poistua loopista, ei yksiTähän mennessä loopilla on kolme poistumistietä, ja jäljellä olevan luvun muoto näkyy. Tuotantoajo päättyy täsmälleen yhdellä viidestä tavasta, eivätkä ne ole toistensa variaatioita:
| miten se päättyy | kuka päätti | mitä kutsujan pitäisi tehdä |
|---|---|---|
| malli lakkasi pyytämästä | malli | lue vastaus |
| vuorokatto | sinä, etukäteen | nosta kattoa tai hyväksy osittainen tulos |
| budjetti käytetty | sinä, etukäteen | hyväksy lisää rahaa tai hyväksy osittainen tulos |
| virhe, jota et voi yrittää uudelleen | provider tai työkalu | korjaa deployment; luvun 14 triage päättää |
| ihminen puuttui väliin | ihminen | odota verdict, jatka sitten |
Näiden litistäminen yhdeksi booleaniksi on tämän tiedoston yleisin design-virhe, ja se tulee kalliiksi tietyllä tavalla: viidestä kolme on jatkettavissa ja kaksi ei. Agent, joka osui vuorokattoonsa, sisältää validin transkriptin, todellisen osittaisen tuloksen ja seuraavan askeleen; agent, joka sai 401:n, ei sisällä mitään näistä. Siksi harness tallentaa syyn datana:
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 };Rikko kolme: työkalu epäonnistuu
Linkki osioon: Rikko kolme: työkalu epäonnistuuLuku 18 päättyi väitteeseen ilman lukua: palauta työkalun virhe mallille työkalutuloksena sen sijaan, että nostat sen, ja malli yleensä korjaa itse itsensä. Tässä on luku.
Yksi epäonnistuminen, kolme policyä. Skriptattu malli arvaa tiedoston, jota ei ole olemassa; työkalu heittää no such file: timeout.log. Call list_files to see what exists.
| mitä harness tekee virheellä | vuorot | työkalun ajot | kustannus | mitä käyttäjä sai |
|---|---|---|---|---|
| heittää sen ulos loopista | 1 | 1 | $0.000756 | stack trace |
palauttaa Error: the tool failed. | 2 | 1 | $0.001462 | "En voinut lukea tiedostoa, joten en tiedä." |
| palauttaa sen, mitä oikeasti tapahtui | 4 | 3 | $0.003550 | "errors.log mainitsee timeoutin." |
Kolmas rivi maksaa 4,7 kertaa ensimmäisen ja on ainoa, joka vastaa kysymykseen. Ja toinen rivi on kiinnostava, koska sitä useimmat codebaset oikeasti tekevät: virhe napattiin, loop selvisi, mallille kerrottiin että jokin epäonnistui muttei mikä, ja se luovutti kohteliaasti. Rivien kaksi ja kolme ero ei ole virheenkäsittelyä. Se on lukijalle kirjoitettu lause.
Harness kohtelee siksi heitettyä työkalua datana ja tekee sanamuodosta policyn:
} 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);
}Luku 18 varoitti myös toisesta puolesta, ja silläkin on hinta. Osoita loop työkaluun, joka epäonnistuu syystä, jota mikään viesti ei voi korjata — lukuun, jota prosessilla ei ole oikeutta tehdä — ja malli yrittää sitä ikuisesti uudelleen:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Yksitoista identtistä suoritusta kutsusta, joka ei voi onnistua, 5,2 kertaa korjattavasta virheestä toipuneen ajon kustannus, eikä lopussa mitään. Virheet ovat contextia; pysyvä virhe on contextia, joka myrkyttää lopun ajon. Ero on luvun 14 statustriage siirrettynä yhtä kerrosta ylemmäs: virhe, jonka perusteella malli voi toimia, menee takaisin transkriptiin, ja virheen, jonka perusteella se ei voi toimia, pitäisi pysäyttää ajo syyllä. Vuorokatto seisoo tänään sinun ja toisen tapauksen välissä, mikä on lattia eikä korjaus.
Rikko neljä: sama kutsu kahdesti
Linkki osioon: Rikko neljä: sama kutsu kahdestiNyt epäonnistuminen, jonka useimmat olettavat mahdottomaksi. Mallit toistavat itseään. Pyydä mitä tahansa loopia ajamaan tarpeeksi pitkään, ja näet identtisen työkalun identtisillä argumenteilla kahdella peräkkäisellä vuorolla.
Mitattuna saman tehtävän baselinea vasten ilman toistoa:
| vuorot | työkalun ajot | kustannus | |
|---|---|---|---|
| tehtävä, ei toistoa | 2 | 1 | $0.001396 |
| sama tehtävä, yksi kutsu toistettu | 3 | 2 | $0.002446 |
| toistettu, read-only-työkalujen tuloscachella | 3 | 1 | $0.002446 |
Duplikoitu kutsu maksoi $0.001050 lisää, 75 % kasvun, ja tässä on osa, joka yllättää ihmiset: cachen käyttö palautti siitä ei mitään. Deduplication säästi työkalusuorituksen eikä vuoroa, koska siinä vaiheessa kun koodisi huomaa toiston, mallille on jo maksettu pyytämisestä. Säästö on todellinen, kun työkalu on hidas, rate-limited tai laskutetaan kutsukohtaisesti — ja se on nolla sillä rivillä, joka kasvoi.
On olemassa pahempi versio. Sovella samaa cachea kirjoittavaan työkaluun, ja toinen kutsu ei hiljaisesti tapahdu:
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"]Kumpi näistä on oikein? Ei kumpikaan, tiedettävästi. Protokolla sanoo, että nämä ovat kaksi kutsua: niillä on kaksi eri tool_call_id-arvoa. Argumentit sanovat, että ne saattavat olla yksi. Harness, joka päättää vertaamalla argumenttijonoja, nielaisee jonain päivänä kahdesta identtisestä, tarkoitetusta veloituksesta toisen — ja luku 14 nimesi jo ainoan mekanismin, joka ratkaisee tämän rehellisesti: idempotency key, jonka loogisen operaation tunteva kerros luo per looginen operaatio. Kunnes työkalulla on sellainen, puolustettava oletus on yllä oleva read-only-portti: cacheta luennat, suorita kirjoitukset ja anna kirjoituksen oman idempotency-mekanismin hoitaa loput.
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;
}Rikko viisi: se poistaa jotain
Linkki osioon: Rikko viisi: se poistaa jotaindestructive-skripti listaa tiedostot ja pyytää sitten poistamaan yhden, jota tehtävä ei koskaan maininnut. Mikään loopissa tähän mennessä ei pysäyttäisi sitä.
Työkalu, joka on merkitty needsApproval, ei epäonnistu eikä etene. Se pysäyttää ajon ja palauttaa kontrollin, mukana kaikki mitä ihminen tarvitsee päätökseen:
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."Siinä on koko mekanismi, ja syy siihen, että se on return eikä callback, on seuraava osio: pysäytyksen ja verdictin välillä prosessia ei ehkä enää ole.
Mutta ensin mittaus, jota kukaan ei odota. Hylkäys ei ole tuloksen puuttuminen — transkriptissa on tool_call_id-avaimella paikka, ja siihen on laitettava jotain. Aja sama hylkäys kahdesti, muuttaen vain sitä mitä tuo jokin sanoo:
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."Kummassakaan ajossa mitään ei poistettu, ja toisessa käyttäjälle kerrotaan että poistettiin. Oikeusjärjestelmä toimi täydellisesti; raportti on valhe. Se on sama mekanismi kuin työkalun virhetaulukossa, mutta paikassa, jossa sillä on paljon enemmän merkitystä — ihminen sanoi ei, toiminto estettiin oikein, ja agentin yhteenveto on ristiriidassa todellisuuden kanssa, koska kieltäytymistä ei koskaan kirjoitettu sinne, mistä malli lukee. Tästä seuraava sääntö on lyhyt: mitä tahansa koodisi päättää työkalukutsusta, kirjoita päätös transkriptiin sanoin. Luku 30 palaa tähän tietoturvan puolelta, missä se on ero audit trailin ja fiktion välillä.
Rikko kuusi: prosessi kuolee
Linkki osioon: Rikko kuusi: prosessi kuoleeApproval kestää minuutteja tai tunteja. Deploy kestää sekunteja. Jos ajo elää paikallisessa muuttujassa HTTP-pyynnön sisällä, jokainen uudelleenkäynnistys on menetetty ajo ja jokainen approval on race.
Ajo ei siis ole closure. Se on tavallinen serialisoitava objekti — viestit, vuoromäärä, kustannus, status, keskeytys, approved call id -lista — ja loop on puhdas funktio sen yli. Juuri tuo yksi rajoite tekee persistenssistä yhden rivin asian:
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"));Oikeellisuuskysymys ei ole tallentaminen. Se on se, mitä tapahtuu paluumatkalla, ja naiivi vastaus laskuttaa sinua kahdesti. Jos prosessi kuoli sen jälkeen, kun malli pyysi työkalua mutta ennen kuin tulos kirjoitettiin, jatko joka aloittaa kutsumalla mallia uudelleen maksaa vuorosta, joka sillä jo on — ja jos se aloittaa ajamalla työkalut uudelleen, se tekee kirjoituksen kahdesti.
Korjaus on saada loop aloittamaan kysymällä transkriptilta, mikä on kesken:
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));
}Jokainen iteraatio tyhjentää pending ensin ja kysyy mallilta vasta kun mitään kesken olevaa ei ole. Jatkamisesta tulee sama code path kuin normaalista ajosta, ja samoin approvalista — hyväksytty kutsu on yksinkertaisesti pending call, jonka saa nyt ajaa. Tapa prosessi kesken tehtävän ja käynnistä se uudelleen:
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.logKaksi työkalusuoritusta kahden prosessin yli tehtävälle, joka tarvitsee kaksi, ja lopullinen kustannus on identtinen ajon kanssa, joka ei koskaan kaatunut. Kustannus kertyy uudelleenkäynnistyksen yli, koska se oli tilassa, ei muuttujassa.
Rikko seitsemän: kolme minuuttia hiljaisuutta
Linkki osioon: Rikko seitsemän: kolme minuuttia hiljaisuuttascan_archive kestää tässä kolme sekuntia ja edustaa työkalua, joka vie tuotannossa kolme minuuttia. Kaksi asiaa puuttuu sen pyöriessä: käyttäjä ei tiedä, että jotain tapahtuu, ja Stop-painike ei tee mitään.
Molemmat korjataan samalla tavalla, ja se on luvun 14 AbortSignal työnnettynä yhden tason syvemmälle. Signal ei ole vain fetchiä varten — se välitetään työkalun sisään, ja hyvin kirjoitettu työkalu kunnioittaa sitä:
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"Kaksi millisekuntia klikkauksesta pysäytykseen, koska työkalun sisällä oleva sleep kuuntelee samaa signalia kuin fetch. Pujota se vain fetch-kohtaan, ja identtinen Stop-painike odottaa kolme sekuntia — työkalun keston — ja ajo "peruuntuu" sen jälkeen kun työ, jota se perui, on jo valmis. Cancellation, jota ei viedä aivan alas asti, on spinneri, joka sanoo oikean sanan.
Trace, ja miksi se ei ole log
Linkki osioon: Trace, ja miksi se ei ole logHarness emittoi yhden rivin per event, ja sanasto on tarpeeksi pieni muistettavaksi: 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}Kolme ominaisuutta tekee tästä tracen eikä lokitusta. Jokaisella rivillä on run id, joten ajo, joka ulottuu kolmeen prosessiin ja kahteen päivään, on yksi kysely. Jokaisella turn-rivillä on omat token-määränsä ja juokseva kustannus, joten kysymykseen "miksi tämä ajo maksoi neljäkymmentä dollaria" voi vastata jälkikäteen sen sijaan, että se olisi vain teoriassa toistettavissa. Ja run_stopped kantaa syyn, joka on kenttä, joka muuttaa tukipyynnön yhden rivin vastaukseksi: budgettiin pysähtynyt agent ja kaatunut agent näyttävät ulkoa samalta ja tarvitsevat vastakkaiset vastaukset.
Latenssin aritmetiikka
Linkki osioon: Latenssin aritmetiikkaLuku 13 mittasi time to first tokenin omistamallasi raudalla. Luku 14 mittasi sen socketin läpi. Agent kertoo sen, ja kerroin on numero, jota kukaan ei valinnut:
Sama kolmen vuoron tehtävä, vain providerin latenssia muuttaen:
| providerin latenssi per vuoro | wall clock, 3 vuoroa |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2 413 ms |
Harness itse lisää viisitoista millisekuntia kolmen vuoron ajoon. Kaikki muu on kerrottuna luvulla, jota et hallitse — asetettuna serving schedulerissa, joka batchaa pyyntösi tuntemattomien pyyntöjen kanssa6 — ja on mallin valitsema. Siksi luvun 14 streaming merkitsee täällä enemmän kuin chatissa ja auttaa vähemmän: voit streamata viimeisen vuoron, ja neljä sitä edeltävää vuoroa ovat hiljaisuutta, ellei harness emittoi edistystä. Se on myös koko argumentti yllä olevalle tool_progress-eventille — agentissa rehellinen palauteyksikkö ei ole token, vaan askel.
Sama harness, oikea malli portin takana
Linkki osioon: Sama harness, oikea malli portin takanaKaikki yllä ajettu käytti skriptattua provideria, mikä todistaa harnessin eikä todista mitään malleista. Muuta siis yksi rivi — luvun 14 seam, LLM_BASE_URL — ja osoita identtinen koodi paikalliseen Qwen2.5-0.5B-Instructiin samoilla neljällä työkalulla. Kuusi tehtävää samojen kolmen tiedoston yli:
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,908msKolme havaintoa, ja kolmas on syy siihen, että tämä osio on olemassa.
Jokainen tehtävä valmistui täsmälleen kahdessa vuorossa. Vuorokatto ei koskaan lauennut, budjetti ei koskaan lauennut, ja loopin ainoa poistumistie oli mallin tuottama proosa. Puolen miljardin parametrin malli ei iterioi; se vastaa toisella hengityksellään riippumatta siitä, onko sillä mitä se tarvitsee. Vuoromäärä on mallin ominaisuus, ei loopisi.
Keskimääräinen vuoro kesti 6 908 millisekuntia, joten yllä oleva latenssitaulukko ei ole lelu: tässä koossa hypoteettinen kahdeksan vuoron ajo on lähes minuutti wall clockia ilman mitään ruudulla.
Ja vastaukset ovat väärin. Suurin tiedosto on errors.log; malli listasi tiedostot, ei koskaan lukenut niitä ja nimesi silti yhden. Ensimmäinen tehtävä arvasi tiedostonimen, sille kerrottiin ettei sitä ole, ja se päätteli. Harness suoritti moitteettomasti kaikissa kuudessa ajossa. Harness tekee agentista hallittavan, ei oikeaa — luku 29 kertoo miten selvität kumpaa se on, ja luku 30 mitä se maksaa, kun kukaan ei tehnyt sitä.
Subagents, nimetty tässä ja laskutettu myöhemmin
Linkki osioon: Subagents, nimetty tässä ja laskutettu myöhemminYhdellä katalogin työkalulla voi olla toinen ajo takanaan. Rajapinta on luvusta 18 — schema ja endpoint — ja kokonainen agent mahtuu sen taakse, koska rajapinta on kapea:
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";
},
};Kolme asiaa on jo oikein noissa kymmenessä rivissä, ja kaikki kolme seuraavat yllä tehdyistä päätöksistä: lapsella on oma context window, joten vanhemman transkripti saa yhteenvedon eikä kaikkea mitä lapsi luki; sillä on omat rajat, joten karannut lapsi ei voi käyttää vanhemman budjettia; ja se perii signalin, joten yksi Stop peruuttaa puun. Miksi puhdas context window on pääasia eikä sivuvaikutus, on luku 24; viisi orchestration patternia — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — ja handoff ovat luvussa 25.
Missä frameworkit ovat, ja miksi tämä kurssi ei käyttänyt niitä
Linkki osioon: Missä frameworkit ovat, ja miksi tämä kurssi ei käyttänyt niitäMitään yllä olevaa ei pidä lukea argumenttina kirjastoja vastaan. Mitattuna 7. syyskuuta 2026, 29. elokuuta päättyneelle kuukaudelle:7
| paketti | lataukset tuona kuukautena | mitä se antaa sinulle |
|---|---|---|
ai (Vercel AI SDK) | 89 385 860 | ToolLoopAgent, stopWhen, työkalun approval, step hooks |
@anthropic-ai/claude-agent-sdk | 41 558 352 | Claude Code harness kirjastona: loop, sessiot, hookit, oikeudet, subagents8 |
@langchain/langgraph | 12 812 815 | loop eksplisiittisenä tilagraafina |
langchain | 11 359 058 | ketjut, agents, integraatiot |
@openai/agents | 6 093 155 | agents, handoffs, guardrails |
@mastra/core | 5 914 502 | agents, workflowt, muisti |
Syy siihen, että tämä kurssi kirjoittaa loopin käsin sen sijaan, että opettaisi jonkin näistä, sanotaan ääneen eikä jätetä vihjailuksi, ja se on mitattavissa. Kahdentoista kuukauden aikana 7. syyskuuta 2026 mennessä ai julkaisi 945 versiota ja siirtyi major-versiosta 5 major-versioon 7, ja sen agent-luokka viedään edelleen nimellä Experimental_Agent; langchain julkaisi samassa ikkunassa 132 versiota; @openai/agents julkaisi 83 ja on yhä 0.x:ssä, viisitoista kuukautta ensimmäisen julkaisunsa jälkeen.7 Luku, joka kirjoitetaan mitä tahansa noista API:sta vasten, vanhenee yhden kauden sisällä, ja tämä julkaistaan kolmessakymmenessäkolmessa kielessä, joten jokainen uusi editio maksaa koko käännöksen. Niiden kaikkien alla oleva asia ei liiku: loop, pysäytyssääntö, katalogi, executor, jonkin verran tilaa.
Ja reference implementation on tämän luvun kanssa samaa mieltä sillä osalla, jolla on merkitystä. ai-version 7.0.93 loopin poistuminen ei ole numero — se on stopWhen, predikaattien lista, jossa askelmäärä on vain yksi:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsPysähtyminen on monikollista tämän loopin käytetyimmässä toteutuksessa samasta syystä kuin se on monikollista yllä olevissa sadassayhdeksässäkymmenessäkuudessa rivissä.
Minne tästä mennään seuraavaksi
Linkki osioon: Minne tästä mennään seuraavaksiSinulla on nyt harness: loop, katalogi, executor, viisi poistumistietä, persistetty ajo, signal joka ulottuu työkaluihin, ja trace, jossa on run id jokaisella rivillä. Luvut 24, 25, 29 ja 30 rakentuvat tämän tiedoston päälle, ja luvut 26–28 sen päälle, mihin se ylettyy.
Sillä on yksi ongelma jäljellä, ja yllä olevat mittaukset ovat osoittaneet siihen koko matkan. Katso karkuritaulukkoa vielä kerran: 3 431 input tokenia kahdeksalla vuorolla, 337 299 sadalla. Katso toimivaa ajoa: 204, 269, 342. Jokainen vuoro lähettää koko transkriptin uudelleen, joten agentin context täyttyy sen omasta historiasta — ja malli on huonompi käyttämään pitkän ikkunan kaukaista päätä kuin läheistä päätä, minkä vuoksi hyvä agent vuorolla viisi on hämmentynyt agent vuorolla neljäkymmentä.
Vuorokatto ei korjaa sitä. Se vain estää sinua maksamasta siitä, että katsot sen tapahtuvan. Sen korjaa päätös jokaisella vuorolla siitä, mitkä tokens ansaitsevat ikkunan: mitä tiivistää, mitä siirtää muistiinpanoon, jonka agent voi noutaa, mitä antaa subagentille puhtaalla context windowlla, ja mitkä työkalumääritykset ovat pysyvän veronsa arvoisia. Luku 24 mittaa, minne ikkuna oikeasti menee — ja yllätys on, ettei se ole keskustelu.
Lähteet ja menetelmä
Linkki osioon: Lähteet ja menetelmäJokainen tämän luvun luku tuli ulos kahdesta yllä kuvatusta serveristä Node 22:lla loopback-rajapinnan yli: skriptattu provider, joka laskee tokens o200k_base-enkoodauksella, ja Qwen/Qwen2.5-0.5B-Instruct samanmuotoisen endpointin takana, greedy decoding, CPU:lla. Kustannukset lasketaan mitatuista token-määristä hinnoilla, jotka luku 16 luki 6. syyskuuta 2026 — $2.00 miljoonalta input tokenilta ja $12.00 miljoonalta output tokenilta — eikä yksikään tämän luvun pyyntö mennyt maksulliseen endpointiin. Paikallisen mallin vastaukset ovat pienen mallin vastauksia; lue ne evidenssinä loopista, joka on kummassakin tapauksessa identtinen, älä benchmarkina siitä, mitä nykyiset mallit tekevät.
Viitteet
Linkki osioon: Viitteet-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. ja Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Reasoning tracejen ja toimintojen vuorottelu, jonka loop toteuttaa, sekä havainto, että toimiminen antaa mallin "handle exceptions" — mikä on täsmälleen se, mitä yllä oleva työkalun virhetaulukko mittaa. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. ja Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Muodollinen käsittely siitä, mitä yllä oleva loop tekee epämuodollisesti: modulaariset muistikomponentit, strukturoitu action space, joka ulottuu sisäiseen muistiin ja ulkoisiin ympäristöihin, sekä "a generalized decision-making process to choose actions". Lue se sanastosta, joka alan termistä puuttuu — erityisesti working, episodic, semantic ja procedural memory -erottelusta, jonka käytännön varjo on luvun 24 kolmen storagen taulukko. ↩
-
ai(Vercel AI SDK) versio 7.0.93, julkaistu 4. syyskuuta 2026; tyyppimääritykset luettu kohteestacdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts7. syyskuuta 2026. 397 kt:n tiedosto sisältää nolla esiintymää merkkijonostaharness. Agent-luokka ondeclare class ToolLoopAgent, viety sekä nimelläToolLoopAgentettä nimelläExperimental_Agent;declare function isStepCount(stepCount: number)— viety nimellästepCountIs— on lainattu yllä sanatarkasti;type StopConditionon näytetty ilman toista tyyppiparametriaan (RUNTIME_CONTEXT extends Context = Context), mikä on otteen ainoa poisjättö, samoin kuinstopWhen?: Arrayable<StopCondition<...>>-muoto kohdissagenerateTextjastreamText. Sama tiedosto deklaroitoolApproval,ToolApprovalStatus,prepareStepjarepairToolCall, eli reference implementation on itsenäisesti päätynyt approval gateihin, per-step-valmisteluun ja error repairiin. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. ja Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Abstrakti kutsuu artefaktia 2 294 ongelman "evaluation frameworkiksi" eikä käytä sanaa "harness" koskaan; projektin oma README (
github.com/SWE-bench/SWE-bench, luettu 7. syyskuuta 2026) käyttää sitä viisi kertaa, aina muodossa "evaluation harness", ja entry point onpython -m swebench.harness.run_evaluation. Se on sanan toinen merkitys: teline, joka pitää agentin paikallaan ja pisteyttää sen, ei loop joka ajaa sitä. ↩ -
Anthropic, Building effective agents, 19. joulukuuta 2024,
anthropic.com/engineering/building-effective-agents, luettu 7. syyskuuta 2026. Augmented model rakennuspalikkana, agent LLM:nä joka "using tools based on environmental feedback in a loop", ja suositus stopping conditions -ehdoista "such as a maximum number of iterations" kontrollin säilyttämiseksi. Luku 22 lainaa sen määritelmän kokonaan. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. ja Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Toinen loop — serving scheduler, joka batchaa pyyntösi tuntemattomien pyyntöjen kanssa ja hallitsee luvun 13 KV cachea. Sen olemassaolo kannattaa tietää juuri siksi, ettei se ole sinun: latenssi, jonka harness kertoo, asetetaan sen sisällä, eikä mikään työ loopisi parissa siirrä sitä. ↩
-
npm-rekisterin latausmäärät,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, eksplisiittinen ikkuna eikä liukuvalast-month-ikkuna, ja julkaisuhistoriat kohteestaregistry.npmjs.org/<package>; molemmat kysytty 7. syyskuuta 2026. Julkaisumäärät ovat tuohon päivään päättyneen kahdentoista kuukauden aikana julkaistujen versioiden määrä, canary-buildit mukaan lukien:ai945 (uusin 7.0.93 2026-09-04, ja major-versiot 5, 6 ja 7 kaikki näkyvät ikkunan sisällä),langchain132 (uusin 1.5.10 2026-08-20),@openai/agents83 (uusin 0.17.0 2026-08-19, julkaistu ensimmäisen kerran 2025-06-03). ↩ ↩2 -
Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) on Claude Code harness paketoituna kirjastoksi — agent loop, sisäänrakennetut tiedosto- ja shell-työkalut, context management, sessiot, hookit, oikeudet ja subagents — dokumentoitu kohteessacode.claude.com/docs/en/agent-sdk. Se on lähimpänä julkaistua selostusta jokaisesta mekanismista, jonka tämä luku rakentaa käsin, ja se kannattaa lukea oman toteutuksesi rinnalla niiden osien vuoksi, jotka se nimeää ja joihin tämä luku vain viittaa. ↩