Hoppa till innehållet
23/30Kapitel 23 av 30

Bygg en agent harness: loopen och de fem vägarna ut

En loop på femton rader som fungerar direkt – och sedan bryts sju gånger med flit, från en runaway som kostade 77 gånger mer.

På den här sidan

Börja med den ärliga delen, för ingen annan kommer att säga det: ”harness” är jargong, inte en standard. Det finns ingen specifikation, ingen kommitté, ingen referensdefinition. De fyra artiklar som det här kapitlet citerar — ReAct,1 CoALA,2 SWE-bench och vLLM — använder inte ordet en enda gång i sina abstracts. Den mest nedladdade implementationen av saken, Vercels ai-paket med 89,4 miljoner nedladdningar i månaden, använder det inte heller: strängen harness förekommer noll gånger i de 397 KB typdeklarationer som skickas med version 7.0.93.3 Det enda ställe där ordet faktiskt bär vikt betyder något helt annat. SWE-bench säger ”harness” fem gånger i sin README, alltid som evaluation harness — den containeriserade ställning som applicerar en patch och kör testerna — och dess Python-modul heter bokstavligen swebench.harness.run_evaluation.4

Så två olika saker delar namn. En evaluation harness håller agent stilla och poängsätter den. En agent harness är programmet som kör agent: den anropar modellen, kör det modellen ber om, bestämmer när den ska stoppa och håller tillståndet däremellan. Det här kapitlet bygger den andra, på under tvåhundra rader TypeScript, helt utan framework.

Själva loopen är femton rader och fungerar på första försöket. Allt efter det är ett sätt att lämna den.

Visa detaljer

Det här kapitlet behöver från de tidigare.

  • Kapitel 14 för klienten: deadlines, statustriage, cancellation, idempotency keys och mock provider-tekniken som används igen här.
  • Kapitel 16 för aritmetiken: input tokens växer med kvadraten på konversationen, och priserna nedan är de som lästes där den 6 september 2026.
  • Kapitel 18 för verktygskatalogen: ett schema modellen ser, en endpoint den aldrig ser och regeln att fel är context snarare än undantag.
  • Kapitel 22 för loopen som den här ärver, och för de två publicerade definitionerna av ”agent” som säger emot varandra.

Inga tensorer här. Det här är kursens andra beroendenav: kapitel 24, 25, 29 och 30 kör på filen nedan, och 26 till 28 bygger på det den kan nå.

Kapitel 14 kunde inte skrivas mot en riktig leverantör, eftersom du inte kan be en sådan om en 429 vid ett valt ögonblick. Det här kapitlet har samma problem i en annan form: du kan inte be en riktig modell att skena, eller att begära exakt samma verktyg två gånger i rad, på beställning och reproducerbart.

Så det första programmet är en skriptad leverantör: en endpoint med formen av ett chat completions API vars svar är en funktion av varvets index och av vad verktygen hittills har returnerat. Den räknar tokens med en riktig byte-pair encoder, så pengarna nedan är aritmetik snarare än dekoration.

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);

Två rader bär designen. Varvets index är härlett från konversationen, inte sparat i en variabel, så leverantören är stateless och en körning kan dödas och återupptas mot den. Och recover läser verktygsresultaten innan den bestämmer sig: en skriptad modell som läser sin egen transcript är miniminivån som krävs för att mäta om harness gav den något värt att läsa.

Katalogen är den från kapitel 18, fyra verktyg över tre filer: list_files, read_file, delete_file — markerad needsApproval — och scan_archive, som är långsam med flit.

Här är hela idén, innan någon av delarna som gör den överlevnadsduglig.

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 });
  }
}

Rikta den mot den skriptade leverantören så gör den exakt det den ser ut att göra:

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

Tre varv, två verktygskörningar, en fjärdedels amerikansk cent. Notera sista raden: 204, 269, 342. Varje varv skickar om allt före det, vilket är kapitel 16:s kvadratiska räkning som dyker upp på en plats där ingen skrev något. Resten av kapitlet handlar om vad som händer när den raden inte slutar växa.

Rikta samma loop mot runaway-skriptet — en modell som ber om ett verktyg i varje enskilt varv och aldrig skriver prosa — och den markerade return körs aldrig. Det finns ingen annan utgång. Programmet kör tills processen dör eller kreditkortet gör det.

Fixen är en rad, det är den första kontroll som litteraturen rekommenderar,5 och alla skriver den förr eller senare. Det nästan ingen gör är att mäta vad den är värd:

varvstakmodellanropinput tokenskostnad
883 431$0.009070
202016 259$0.038038
505088 649$0.191098
100100337 299$0.702198

Läs de två sista raderna tillsammans. Att dubbla taket från 50 till 100 dubblade inte kostnaden; det multiplicerade den med 3,7. Input tokens gick från 88 649 till 337 299, en faktor 3,8, eftersom varv nn bär med sig alla tidigare varv och totalen är Θ(n2)\Theta(n^2). Ett varvstak är inte ett linjärt reglage. Det är ett reglage på kvadratroten av ditt värsta fall, och därför är det ett beslut som bör prissättas innan du höjer det från 20 till 100 ”för säkerhets skull”.

Brott två: ett tak för varv är inte ett tak för pengar

Länk till avsnittet: Brott två: ett tak för varv är inte ett tak för pengar

Problemet med ett varvstak är att ett varv inte har ett fast pris. Tjugo varv över en kort transcript kostade $0.038 ovan. Tjugo varv med en katalog på 200 verktyg, en uppsättning hämtade dokument och fyrtio historikmeddelanden kostar hundratals gånger mer, och taket vet inte det. Det operatören vill begränsa är räkningen.

Så loopen räknar pengar, med kapitel 16:s computeCost mot priserna som lästes där — $2.00 per miljon input tokens och $12.00 per miljon output, för modellen som prissätts genom hela kursen:

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);

Samma runaway-skript, inget varvstak alls, tre budgetar:

budgetuppnådda varvfaktiskt spenderat
$0.019$0.010780
$0.0524$0.051790
$0.2052$0.205398

Två saker är värda att namnge. För det första köper budgeten ett annat antal varv varje gång, vilket är poängen: den begränsar det operatören bryr sig om och låter varvantalet hamna där transcript placerar det. För det andra överskrider varje rad budgeten. Budgeten var $0.010 och $0.010780 spenderades, eftersom kontrollen körs före ett varv och priset för ett varv inte är känt förrän det är över. Du kan inte begränsa kostnaden exakt; du kan begränsa den till inom kostnaden för ett varv. Säg det i gränssnittet i stället för att låtsas, och lägg kontrollen före anropet så att överskridandet blir ett varv och inte två.

Vid det här laget har loopen tre utgångar, och formen på resten av kapitlet syns. En produktionskörning slutar på exakt ett av fem sätt, och de är inte varianter av varandra:

hur den slutarvem bestämdevad anroparen bör göra
modellen slutade frågamodellenläs svaret
varvstakdu, i förväghöj taket eller acceptera ett partiellt resultat
budgeten tog slutdu, i förväggodkänn mer pengar eller acceptera ett partiellt resultat
ett fel du inte kan försöka igenleverantören eller ett verktygfixa driftsättningen; kapitel 14:s triage avgör
en människa ingrepen personvänta på ett beslut och återuppta sedan

Att pressa ihop dessa till en boolean är det vanligaste designmisstaget i den här filen, och det är dyrt på ett specifikt sätt: tre av fem är återupptagbara och två är det inte. En agent som nådde sitt varvstak har en giltig transcript, ett riktigt partiellt resultat och ett nästa steg; en agent som fick en 401 har inget av detta. Därför sparar harness orsaken som 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 };

Kapitel 18 slutade med ett påstående utan en siffra: ge ett verktygs fel tillbaka till modellen som ett verktygsresultat i stället för att kasta det, och modellen fixar sig oftast själv. Här är siffran.

Ett fel, tre policyer. Den skriptade modellen gissar en fil som inte finns; verktyget kastar no such file: timeout.log. Call list_files to see what exists.

vad harness gör med feletvarvverktygskörningarkostnadvad användaren fick
kastar ut det ur loopen11$0.000756en stack trace
returnerar Error: the tool failed.21$0.001462”Jag kunde inte läsa filen, så jag vet inte.”
returnerar vad som faktiskt hände43$0.003550”errors.log nämner en timeout.”

Den tredje raden kostar 4,7 gånger den första och är den enda som besvarar frågan. Och den andra raden är den intressanta, eftersom det är vad de flesta kodbaser faktiskt gör: felet fångades, loopen överlevde, modellen fick veta att något misslyckades men inte vad, och den gav upp artigt. Skillnaden mellan rad två och tre är inte felhantering. Det är en mening skriven för en läsare.

Därför behandlar harness ett kastat verktyg som data och gör formuleringen till en 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);   
}

Kapitel 18 varnade också för andra sidan, och den har också ett pris. Rikta loopen mot ett verktyg som misslyckas av en anledning som inget meddelande kan fixa — en läsning som processen inte har rätt att utföra — och modellen försöker igen för alltid:

TEXT
read a file the process may not open   turns=12  toolruns=11  in=7,079  cost=$0.018622
                                      status=max_turns_exceeded   answer=""

Elva identiska körningar av ett anrop som inte kan lyckas, 5,2 gånger kostnaden för körningen som återhämtade sig från ett fixbart fel, och ingenting i slutet. Fel är context; ett permanent fel är context som förgiftar resten av körningen. Skillnaden är kapitel 14:s statustriage flyttad ett lager upp: ett fel modellen kan agera på går tillbaka in i transcript, och ett fel den inte kan agera på bör stoppa körningen med en orsak. Varvstaket är det som står mellan dig och det andra fallet i dag, vilket är ett golv och inte en fix.

Nu kommer felet de flesta antar inte kan hända. Modeller upprepar sig. Be vilken loop som helst köra tillräckligt länge och du kommer att se samma verktyg med samma argument i två varv i rad.

Mätt mot baseline för samma uppgift utan upprepningen:

varvverktygskörningarkostnad
uppgiften, ingen upprepning21$0.001396
samma uppgift, ett anrop upprepat32$0.002446
upprepat, med result cache på read-only-verktyg31$0.002446

Det duplicerade anropet kostade $0.001050 extra, en ökning på 75 %, och här är delen som överraskar folk: caching av resultatet tog tillbaka inget av det. Deduplication sparade verktygskörningen och inte varvet, eftersom modellen redan har fått betalt för att fråga när din kod upptäcker upprepningen. Besparingen är verklig när verktyget är långsamt, rate-limited eller faktureras per anrop — och den är noll på raden som växte.

Det finns en värre version. Använd samma cache på ett verktyg som skriver, och det andra anropet sker tyst inte:

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"]

Vilket av dessa är rätt? Inget, på ett vetbart sätt. Protokollet säger att detta är två anrop: de bär två olika tool_call_id-värden. Argumenten säger att de kanske är ett. En harness som avgör genom att jämföra argumentsträngar kommer en dag att svälja det andra av två identiska, avsedda debiteringar — och kapitel 14 har redan namngivit den enda mekanism som löser detta ärligt, nämligen en idempotency key som genereras per logisk operation av lagret som vet vad operationen är. Tills verktyget bär en sådan är den försvarbara standarden read-only-grinden ovan: cacha läsningar, kör skrivningar och låt skrivningens egen idempotency hantera resten.

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;
}

destructive-skriptet listar filerna och ber sedan om att radera en som uppgiften aldrig nämnde. Ingenting i loopen hittills skulle stoppa det.

Ett verktyg markerat needsApproval misslyckas inte och fortsätter inte. Det stoppar körningen och lämnar tillbaka kontrollen, med allt en person behöver för att bestämma:

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

Det är hela mekanismen, och skälet till att den är en return snarare än en callback är nästa avsnitt: mellan stoppet och beslutet kanske processen inte finns längre.

Men först mätningen ingen väntar sig. Ett avslag är inte frånvaron av ett resultat — transcript har en slot nycklad av tool_call_id och något måste ligga i den. Kör samma avslag två gånger och ändra bara vad detta något säger:

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

Ingenting raderades i någon av körningarna, och i den andra får användaren höra att det raderades. Behörighetssystemet fungerade perfekt; rapporten är en lögn. Det är samma mekanism som i verktygsfelstabellen, nu på en plats där det spelar mycket större roll — en människa sa nej, åtgärden blockerades korrekt, och agent:s sammanfattning motsäger verkligheten eftersom vägran aldrig skrevs ner där modellen läser. Regeln som faller ut är kort: vad din kod än beslutar om ett verktygsanrop, skriv beslutet i transcript med ord. Kapitel 30 återkommer till detta från säkerhetssidan, där det är skillnaden mellan en audit trail och fiktion.

Ett godkännande tar minuter eller timmar. En deploy tar sekunder. Om körningen lever i en lokal variabel inuti en HTTP-request är varje omstart en förlorad körning och varje godkännande en race.

Så körningen är inte en closure. Den är ett vanligt serialiserbart objekt — meddelanden, varvantal, kostnad, status, avbrott, listan över godkända call ids — och loopen är en pure function över den. Den enda begränsningen är det som gör persistence till en enradsfråga:

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"));

Korrekthetsfrågan är inte att spara. Den är vad som händer på vägen tillbaka in, och det naiva svaret debiterar dig två gånger. Om processen dog efter att modellen bad om ett verktyg men innan resultatet skrevs, betalar en resume som börjar med att anropa modellen igen för ett varv den redan har — och om den börjar med att köra om verktygen utför den en skrivning två gånger.

Fixen är att låta loopen börja med att fråga transcript vad som är utestående:

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));    
}

Varje iteration tömmer pending först och frågar modellen bara när inget är utestående. Resume blir samma kodväg som den normala, och det blir approval också — ett godkänt anrop är helt enkelt ett väntande anrop som nu får köras. Döda processen mitt i uppgiften och starta om den:

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

Två verktygskörningar över två processer för en uppgift som behöver två, och slutkostnaden är identisk med körningen som aldrig kraschade. Kostnaden ackumuleras över omstarten eftersom den låg i state, inte i en variabel.

scan_archive tar tre sekunder här och står för verktyget som tar tre minuter i produktion. Två saker saknas medan det kör: användaren har ingen aning om att något händer, och Stopp-knappen gör ingenting.

Båda har samma fix, och det är kapitel 14:s AbortSignal nedtryckt ett lager djupare. Signalen är inte bara för fetch — den skickas in i verktyget, och ett välskrivet verktyg respekterar den:

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"

Två millisekunder från klicket till stoppet, eftersom sleep inuti verktyget lyssnar på samma signal som fetch gör. Trä den bara in i fetch och samma Stopp-knapp väntar tre sekunder — verktygets längd — och körningen ”avbryts” efter att arbetet den avbröt redan har avslutats. Cancellation som inte kopplas hela vägen ner är en spinner som säger rätt ord.

Harness skickar ut en rad per händelse, och vokabulären är liten nog att memorera: 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}

Tre egenskaper gör detta till en trace snarare än logging. Varje rad bär run id, så en körning som spänner över tre processer och två dagar är en fråga. Varje turn-rad bär sina egna token-räkningar och den löpande kostnaden, så ”varför kostade den här körningen fyrtio dollar” går att besvara i efterhand i stället för att bara kunna återskapas i teorin. Och run_stopped bär orsaken, vilket är fältet som gör en support ticket till ett enradsvar: en agent som stoppade vid budgeten och en agent som kraschade ser identiska ut utifrån och behöver motsatta svar.

Kapitel 13 mätte time to first token på hårdvara du äger. Kapitel 14 mätte det genom en socket. En agent multiplicerar det, och multiplikatorn är ett tal ingen valde:

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

Samma trevarvsuppgift, med endast leverantörens latens ändrad:

leverantörslatens per varvväggtid, 3 varv
0 ms15 ms
200 ms615 ms
800 ms2 413 ms

Harness själv bidrar med femton millisekunder till en trevarvskörning. Allt annat är NN multiplicerat med ett tal du inte kontrollerar — satt inuti en serving scheduler som batchar din request med främlingars requests6 — och NN väljs av modellen. Det är därför streaming från kapitel 14 spelar större roll här än i en chat och hjälper mindre: du kan streama sista varvet, och de fyra varven före det är tystnad om inte harness skickar progress. Det är också hela argumentet för tool_progress-händelsen ovan — i en agent är den ärliga enheten för feedback inte token, utan steget.

Allt ovan kördes mot en skriptad leverantör, vilket bevisar harness och inte bevisar något om modeller. Så ändra en rad — sömmen från kapitel 14, LLM_BASE_URL — och rikta identisk kod mot en lokal Qwen2.5-0.5B-Instruct med samma fyra verktyg. Sex uppgifter över samma tre filer:

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

Tre fynd, och det tredje är skälet till att det här avsnittet finns.

Varenda uppgift avslutades på exakt två varv. Varvstaket utlöstes aldrig, budgeten utlöstes aldrig, och loopens enda utgång var att modellen producerade prosa. En modell med en halv miljard parametrar itererar inte; den svarar på sitt andra andetag oavsett om den har det den behöver. Varvantalet är en egenskap hos modellen, inte hos din loop.

Medelvarvet tog 6 908 millisekunder, så latenstabellen ovan är inte en leksak: vid den här storleken är en hypotetisk åttavarvskörning nästan en minut väggtid utan något på skärmen.

Och svaren är fel. Den största filen är errors.log; modellen listade filerna, läste dem aldrig och namngav ändå en. Den första uppgiften gissade ett filnamn, fick veta att det inte fanns och drog en slutsats. Harness körde felfritt i alla sex körningar. En harness gör en agent styrbar, inte korrekt — kapitel 29 är hur du tar reda på vilket, och kapitel 30 är vad det kostar när ingen gjorde det.

Subagents, namngivna här och debiterade senare

Länk till avsnittet: Subagents, namngivna här och debiterade senare

Ett verktyg i katalogen kan ha en annan körning bakom sig. Gränssnittet är kapitel 18:s — ett schema och en endpoint — och en hel agent får plats bakom det eftersom gränssnittet är smalt:

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";
  },
};

Tre saker är redan rätt i de där tio raderna och alla tre är konsekvenser av beslut ovan: barnet har sitt eget fönster, så förälderns transcript får en sammanfattning i stället för allt barnet läste; det har sina egna gränser, så ett skenande barn kan inte spendera förälderns budget; och det ärver signalen, så ett Stopp avbryter trädet. Varför ett rent fönster är poängen snarare än en bieffekt är kapitel 24; de fem orkestreringsmönstren — prompt chaining, routing, parallelisering, orchestrator-workers, evaluator-optimiser — och handoff är kapitel 25.

Var frameworksen finns, och varför den här kursen inte använde ett

Länk till avsnittet: Var frameworksen finns, och varför den här kursen inte använde ett

Inget ovan ska läsas som ett argument mot bibliotek. Mätt den 7 september 2026, för månaden som slutade 29 augusti:7

paketnedladdningar den månadenvad det ger dig
ai (Vercel AI SDK)89 385 860ToolLoopAgent, stopWhen, tool approval, step hooks
@anthropic-ai/claude-agent-sdk41 558 352Claude Code harness som bibliotek: loop, sessions, hooks, behörigheter, subagents8
@langchain/langgraph12 812 815loopen som en explicit state graph
langchain11 359 058chains, agents, integrationer
@openai/agents6 093 155agents, handoffs, guardrails
@mastra/core5 914 502agents, workflows, memory

Skälet till att den här kursen skriver loopen för hand i stället för att lära ut ett av dem är uttalat snarare än underförstått, och det är mätbart. Under de tolv månaderna fram till 7 september 2026 publicerade ai 945 versioner och gick från major 5 till major 7, och dess agent-klass exporteras fortfarande som Experimental_Agent; langchain publicerade 132 versioner under samma fönster; @openai/agents publicerade 83 och ligger fortfarande på 0.x, femton månader efter sin första release.7 Ett kapitel skrivet mot något av dessa API:er blir inaktuellt inom en säsong, och det här publiceras på trettiotre språk, så varje nyutgåva kostar hela översättningen. Det som ligger under dem alla rör sig inte: en loop, en stoppregel, en katalog, en executor, lite state.

Och referensimplementationen håller med det här kapitlet om delen som spelar roll. I ai version 7.0.93 är loopens exit inte ett tal — den är stopWhen, en lista med predikat där ett stegantal bara är ett av dem: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 är plural i den mest använda implementationen av den här loopen, av samma skäl som den är plural i de hundranittiosex raderna ovan.

Du har nu en harness: en loop, en katalog, en executor, fem vägar ut, en persisterad körning, en signal som når verktygen och en trace med ett run id på varje rad. Kapitel 24, 25, 29 och 30 bygger på den här filen, och 26 till 28 på det den kan nå.

Den har ett problem kvar, och mätningarna ovan har pekat på det hela vägen. Titta på runaway-tabellen en gång till: 3 431 input tokens vid åtta varv, 337 299 vid hundra. Titta på körningen som fungerar: 204, 269, 342. Varje varv skickar om hela transcript, så en agent:s context fylls med dess egen historik — och modellen är sämre på att använda den bortre änden av ett långt fönster än den nära änden, vilket är varför en bra agent vid varv fem är en förvirrad vid varv fyrtio.

Ett varvstak fixar inte det. Det stoppar dig bara från att betala för att se det hända. Det som fixar det är att i varje enskilt varv bestämma vilka tokens som förtjänar fönstret: vad som ska kompakteras, vad som ska flyttas ut till en anteckning som agent kan hämta, vad som ska lämnas till en subagent med ett rent fönster och vilka verktygsdefinitioner som är värda sin permanenta skatt. Kapitel 24 mäter vart fönstret faktiskt tar vägen — och överraskningen är att det inte är konversationen.


Varje siffra i det här kapitlet kom från de två servrarna som beskrivs ovan, på Node 22 över ett loopback-interface: en skriptad leverantör som räknar tokens med o200k_base-kodningen, och Qwen/Qwen2.5-0.5B-Instruct bakom en endpoint med samma form, greedy decoding, på CPU. Kostnader beräknas från uppmätta token-antal till priserna som kapitel 16 läste den 6 september 2026 — $2.00 per miljon input tokens och $12.00 per miljon output — och ingen request i det här kapitlet gick till en betald endpoint. Den lokala modellens svar är en liten modells svar; läs dem som evidens om loopen, som är identisk åt båda håll, och inte som ett benchmark av vad dagens modeller gör.

  1. Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. och Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Sammanflätningen av reasoning traces och actions som loopen implementerar, och källan till observationen att acting låter en modell ”handle exceptions” — vilket är exakt vad verktygsfelstabellen ovan mäter.

  2. Sumers, T. R., Yao, S., Narasimhan, K. och Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Den formella behandlingen av det loopen ovan gör informellt: modulära minneskomponenter, ett strukturerat action space som spänner över internt minne och externa miljöer, och ”a generalized decision-making process to choose actions”. Läs den för vokabulären som branschtermen saknar — särskilt separationen mellan working, episodic, semantic och procedural memory, vars praktiska skugga är kapitel 24:s tabell med tre stores.

  3. ai (Vercel AI SDK) version 7.0.93, publicerad 4 september 2026; typdeklarationer lästa från cdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.ts den 7 september 2026. Filen på 397 KB innehåller noll förekomster av strängen harness. Agent-klassen är declare class ToolLoopAgent, exporterad både som ToolLoopAgent och som Experimental_Agent; declare function isStepCount(stepCount: number) — exporterad som stepCountIs — citeras ordagrant ovan; type StopCondition visas utan sin andra typparameter (RUNTIME_CONTEXT extends Context = Context), vilket är den enda utelämningen i utdraget, liksom formen för stopWhen?: Arrayable<StopCondition<...>>generateText och streamText. Samma fil deklarerar toolApproval, ToolApprovalStatus, prepareStep och repairToolCall, vilket betyder att referensimplementationen oberoende har landat i approval gates, per-step preparation och error repair. 2

  4. Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. och Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Abstract kallar artefakten ett ”evaluation framework” med 2 294 problem och använder aldrig ordet ”harness”; projektets egen README (github.com/SWE-bench/SWE-bench, läst 7 september 2026) använder det fem gånger, alltid som ”evaluation harness”, och entry point är python -m swebench.harness.run_evaluation. Det är den andra betydelsen av ordet: en ställning som håller agent stilla och poängsätter den, inte loopen som kör den.

  5. Anthropic, Building effective agents, 19 december 2024, anthropic.com/engineering/building-effective-agents, läst 7 september 2026. Den augmented model som byggsten, agent som en LLM som ”using tools based on environmental feedback in a loop”, och rekommendationen om stopping conditions ”such as a maximum number of iterations” för att behålla kontroll. Kapitel 22 citerar dess definition i sin helhet.

  6. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. och Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Den andra loopen — serving scheduler som batchar din request med främlingars requests och hanterar KV cache från kapitel 13. Det är värt att veta att den finns just för att den inte är din: latensen som din harness multiplicerar sätts inuti den, och inget arbete på din loop flyttar den.

  7. Nedladdningssiffror från npm-registret, api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, ett explicit fönster snarare än det rullande last-month, och releasehistorik från registry.npmjs.org/<package>; båda hämtade 7 september 2026. Releaseantal är antalet versioner publicerade under de tolv månaderna fram till detta datum, inklusive canary builds: ai 945 (senaste 7.0.93 den 2026-09-04, med major-versionerna 5, 6 och 7 alla synliga i fönstret), langchain 132 (senaste 1.5.10 den 2026-08-20), @openai/agents 83 (senaste 0.17.0 den 2026-08-19, först publicerad 2025-06-03). 2

  8. Claude Agent SDK (@anthropic-ai/claude-agent-sdk) är Claude Code harness paketerad som ett bibliotek — agent loop, inbyggda fil- och shell-verktyg, context management, sessions, hooks, behörigheter och subagents — dokumenterad på code.claude.com/docs/en/agent-sdk. Den är det närmaste som finns en publicerad redogörelse för varje mekanism som kapitlet bygger för hand, och värd att läsa bredvid din egen implementation för delarna den namnger som kapitlet bara antyder.

Redo att låta LIA välja åt dig?

Bygg med alla AI-modeller på ett ställe – kom igång gratis i dag.