Изградете agent harness: цикълът и петте изхода от него
Цикъл от 15 реда, който тръгва от първия път, после нарочно се чупи 7 пъти — започвайки с runaway 77 пъти по-скъп от лимитиран.
На тази страница
Да започнем с честната част, защото никой друг няма да го каже: „harness“ е жаргон, не стандарт. Няма спецификация, няма комитет, няма референтна дефиниция. Четирите статии, които тази глава цитира — ReAct,1 CoALA,2 SWE-bench и vLLM — не използват думата нито веднъж в резюметата си. Най-теглената имплементация на това нещо, пакетът ai на Vercel с 89,4 милиона изтегляния месечно, също не я използва: низът harness се появява нула пъти в 397 KB декларации за типове, доставяни с версия 7.0.93.3 Единственото място, където думата наистина носи смисъл, означава нещо съвсем друго. SWE-bench казва „harness“ пет пъти в своя README, винаги като evaluation harness — containerised scaffold, който прилага patch и пуска тестовете — а Python модулът му е буквално swebench.harness.run_evaluation.4
Така две различни неща споделят едно име. Evaluation harness държи agent неподвижен и го оценява. Agent harness е програмата, която изпълнява agent: тя извиква model, изпълнява това, което model поиска, решава кога да спре и пази state междувременно. Тази глава изгражда второто — под двеста реда TypeScript, без никакъв framework.
Самият цикъл е петнадесет реда и работи от първия опит. Всичко след това е начин да излезете от него.
Покажи подробности
Какво е нужно на тази глава от предишните.
- Глава 14 за клиента: deadlines, status triage, cancellation, idempotency keys и техниката с mock provider, използвана отново тук.
- Глава 16 за аритметиката: input tokens растат с квадрата на разговора, а използваните по-долу тарифи са прочетените там на 6 септември 2026 г.
- Глава 18 за каталога с инструменти: schema, която model вижда, endpoint, който никога не вижда, и правилото, че грешките са контекст, а не exceptions.
- Глава 22 за цикъла, който тази глава наследява, и за двете публикувани дефиниции на „agent“, които не са съгласни една с друга.
Тук няма tensors. Това е вторият dependency hub на курса: глави 24, 25, 29 и 30 стъпват върху файла по-долу, а 26 до 28 градят върху това, до което той може да стигне.
Provider, който може да се скриптира
Връзка към раздела: Provider, който може да се скриптираГлава 14 не можеше да бъде написана срещу реален provider, защото не можете да поискате от него 429 в избран момент. Тази глава има същия проблем в друга форма: не можете да поискате от реален model да се отплесне безкрайно или да поиска един и същ инструмент два пъти поред, по заявка и възпроизводимо.
Затова първата програма е scripted provider: endpoint с формата на chat completions API, чийто отговор е функция от индекса на хода и от това какво са върнали инструментите досега. Той брои tokens с реален byte-pair encoder, така че парите по-долу са аритметика, не декорация.
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);Два реда носят дизайна. Индексът на хода е изведен от разговора, не се държи в променлива, така че provider е stateless и изпълнение може да бъде убито и подновено срещу него. А recover чете резултатите от инструментите, преди да реши: scripted model, който чете собствения си transcript, е минимумът, нужен за измерване дали harness му е дал нещо, което си струва да прочете.
Каталогът е този от глава 18, четири инструмента в три файла: list_files, read_file, delete_file — маркиран needsApproval — и scan_archive, който нарочно е бавен.
Цикълът, който работи
Връзка към раздела: Цикълът, който работиЕто цялата идея, преди всяка от частите, които я правят оцеляваща.
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 });
}
}Насочете го към scripted provider и той прави точно това, което изглежда, че прави:
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Три хода, две изпълнения на инструменти, четвърт от един щатски цент. Обърнете внимание на последния ред: 204, 269, 342. Всеки ход изпраща отново всичко преди него — квадратичната сметка от глава 16 пристига на място, където никой не е написал нищо. Останалата част от тази глава е какво се случва, когато този ред не спира да расте.
Счупване едно: задачата, която никога не свършва
Връзка към раздела: Счупване едно: задачата, която никога не свършваНасочете същия цикъл към скрипта runaway — model, който иска инструмент на всеки ход и никога не излъчва проза — и маркираният return никога не се задейства. Няма друг изход. Програмата работи, докато процесът умре или кредитната карта не издържи.
Поправката е един ред, тя е първият контрол, който литературата препоръчва,5 и всички в крайна сметка го пишат. Това, което почти никой не прави, е да измери колко струва:
| лимит на ходовете | извиквания на model | input tokens | цена |
|---|---|---|---|
| 8 | 8 | 3.431 | $0.009070 |
| 20 | 20 | 16.259 | $0.038038 |
| 50 | 50 | 88.649 | $0.191098 |
| 100 | 100 | 337.299 | $0.702198 |
Прочетете последните два реда заедно. Удвояването на лимита от 50 на 100 не удвои цената; умножи я по 3,7. Input tokens се качиха от 88.649 на 337.299, фактор 3,8, защото ход носи всеки предишен ход със себе си, а общото е . Лимитът на ходовете не е линеен регулатор. Той е регулатор върху квадратния корен на най-лошия ви случай, затова вдигането му от 20 на 100 „за всеки случай“ е решение, което си струва да остойностите, преди да го вземете.
Счупване две: лимит на ходовете не е лимит на парите
Връзка към раздела: Счупване две: лимит на ходовете не е лимит на паритеПроблемът с лимита на ходовете е, че един ход няма фиксирана цена. Двадесет хода върху кратък transcript струваха $0.038 по-горе. Двадесет хода с каталог от 200 инструмента, набор извлечени документи и четиридесет съобщения история струват стотици пъти повече, а лимитът не знае това. Това, което operator иска да ограничи, е сметката.
Затова цикълът брои пари, използвайки computeCost от глава 16 срещу прочетените там тарифи — $2.00 на милион input tokens и $12.00 на милион output за model, чиято цена използваме в целия този курс:
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);Същият runaway скрипт, без никакъв лимит на ходовете, три бюджета:
| бюджет | достигнати ходове | реално похарчено |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
Две неща си струва да бъдат назовани. Първо, бюджетът купува различен брой ходове всеки път, което е целта: той ограничава нещото, за което operator се интересува, и оставя броя ходове да падне там, където transcript го постави. Второ, всеки ред надхвърля бюджета. Бюджетът беше $0.010, а бяха похарчени $0.010780, защото проверката се изпълнява преди ход, а цената на ход не е известна, докато не приключи. Не можете да ограничите разхода точно; можете да го ограничите до цената на един ход. Кажете го в интерфейса, вместо да се преструвате, и поставете проверката преди извикването, така че надхвърлянето да е един ход, а не два.
Пет начина да излезете от цикъла, не един
Връзка към раздела: Пет начина да излезете от цикъла, не единДотук цикълът има три изхода и формата на останалата глава вече се вижда. Production изпълнение приключва по точно един от пет начина и те не са вариации един на друг:
| как приключва | кой реши | какво трябва да направи caller |
|---|---|---|
| model спря да пита | model | прочетете отговора |
| лимит на ходовете | вие, предварително | вдигнете лимита или приемете частичен резултат |
| изчерпан бюджет | вие, предварително | одобрете повече пари или приемете частичен резултат |
| грешка, която не можете да retry | provider или инструмент | поправете deployment; triage от глава 14 решава |
| намеси се човек | човек | изчакайте verdict, после подновете |
Свиването на тези случаи до един boolean е най-честата дизайнерска грешка в този файл и е скъпа по конкретен начин: три от петте са resumable, а два не са. Agent, който е ударил лимита на ходовете си, има валиден transcript, реален частичен резултат и следваща стъпка; agent, който е получил 401, няма нищо от това. Затова harness записва причината като данни:
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 };Счупване три: инструмент се проваля
Връзка към раздела: Счупване три: инструмент се проваляГлава 18 завърши с твърдение без число: върнете грешката на инструмента на model като резултат от инструмент, вместо да я хвърляте, и model обикновено се поправя сам. Ето числото.
Един failure, три политики. Scripted model познава файл, който не съществува; инструментът хвърля no such file: timeout.log. Call list_files to see what exists.
| какво прави harness с грешката | ходове | изпълнения на инструмент | цена | какво получи потребителят |
|---|---|---|---|---|
| хвърля я извън цикъла | 1 | 1 | $0.000756 | stack trace |
връща Error: the tool failed. | 2 | 1 | $0.001462 | „Не успях да прочета файла, затова не знам.“ |
| връща какво реално се случи | 4 | 3 | $0.003550 | „errors.log споменава timeout.“ |
Третият ред струва 4,7 пъти повече от първия и е единственият, който отговаря на въпроса. А вторият ред е интересният, защото това е, което повечето codebases всъщност правят: грешката е хваната, цикълът оцелява, model е уведомен, че нещо се е провалило, но не какво, и се отказва учтиво. Разликата между втори и трети ред не е error handling. Тя е изречение, написано за читател.
Затова harness третира хвърлен инструмент като данни и прави формулировката политика:
} 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);
}Глава 18 предупреди и за другата страна, която също има цена. Насочете цикъла към инструмент, който се проваля по причина, която никое съобщение не може да поправи — read, който процесът няма право да извърши — и model го retry завинаги:
read a file the process may not open turns=12 toolruns=11 in=7,079 cost=$0.018622
status=max_turns_exceeded answer=""Единадесет идентични изпълнения на call, който не може да успее, 5,2 пъти цената на изпълнението, възстановило се от поправима грешка, и нищо накрая. Грешките са контекст; постоянната грешка е контекст, който отравя остатъка от изпълнението. Разграничението е status triage от глава 14, преместен един слой нагоре: грешка, върху която model може да действа, се връща в transcript, а грешка, върху която не може, трябва да спре изпълнението с причина. Днес лимитът на ходовете е това, което стои между вас и втория случай, което е под, а не поправка.
Счупване четири: същият call, два пъти
Връзка към раздела: Счупване четири: същият call, два пътиСега идва провалът, който повечето хора предполагат, че не може да се случи. Models се повтарят. Оставете който и да е цикъл да работи достатъчно дълго и ще видите идентичния инструмент с идентичните аргументи в два последователни хода.
Измерено спрямо baseline на същата задача без повторението:
| ходове | изпълнения на инструмент | цена | |
|---|---|---|---|
| задачата, без повторение | 2 | 1 | $0.001396 |
| същата задача, един call повторен | 3 | 2 | $0.002446 |
| повторена, с result cache върху read-only инструменти | 3 | 1 | $0.002446 |
Дублираният call струва $0.001050 допълнително, увеличение от 75 %, а ето и частта, която изненадва хората: кеширането на резултата не възстанови нищо от това. Deduplication спести изпълнението на инструмента, но не и хода, защото когато кодът ви забележи повторението, model вече е бил платен, за да го поиска. Спестяването е реално, когато инструментът е бавен, rate-limited или таксуван на call — и е нула по реда от сметката, който нарасна.
Има по-лоша версия. Приложете същия cache към инструмент, който пише, и вторият call тихо не се случва:
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"]Кое от тези е правилно? Нито едно, доколкото може да се знае. Протоколът казва, че това са два call-а: те носят две различни стойности tool_call_id. Аргументите казват, че може да са един. Harness, който решава чрез сравняване на низове с аргументи, един ден ще погълне второто от две идентични, умишлени таксувания — а глава 14 вече назова единствения механизъм, който решава това честно: idempotency key, генериран за логическа операция от слоя, който знае какво е операцията. Докато инструментът не носи такъв, защитимият default е read-only gate по-горе: кеширайте reads, изпълнявайте writes и оставете собствената idempotency на write да се погрижи за останалото.
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 изброява файловете и после иска да изтрие един, който задачата никога не е споменавала. Нищо в цикъла досега не би го спряло.
Инструмент, маркиран needsApproval, не се проваля и не продължава. Той спира изпълнението и връща контрола, с всичко, от което човек има нужда, за да реши:
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."Това е целият механизъм, а причината да е return, а не callback, е следващият раздел: между спирането и verdict процесът може вече да не съществува.
Но първо — измерването, което никой не очаква. Отхвърлянето не е липса на резултат — transcript има slot, ключиран по tool_call_id, и нещо трябва да влезе в него. Пуснете същото отхвърляне два пъти, като промените само какво казва това нещо:
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."Нищо не беше изтрито и в двете изпълнения, а във второто на потребителя му се казва, че е било. Permission system работи перфектно; отчетът е лъжа. Това е същият механизъм като таблицата с грешки от инструменти, пристигащ на място, което има много по-голямо значение — човек каза „не“, действието беше правилно блокирано, а summary на agent противоречи на реалността, защото отказът никога не беше записан там, където model чете. Правилото, което следва, е кратко: каквото и да реши кодът ви за tool call, запишете решението в transcript с думи. Глава 30 се връща към това от страната на сигурността, където това е разликата между audit trail и измислица.
Счупване шест: процесът умира
Връзка към раздела: Счупване шест: процесът умираApproval отнема минути или часове. Deploy отнема секунди. Ако изпълнението живее в локална променлива вътре в HTTP request, всеки restart е изгубено изпълнение и всеки approval е race.
Затова изпълнението не е closure. То е обикновен serialisable object — messages, брой ходове, цена, status, interruption, списъкът с approved call ids — а цикълът е pure function върху него. Това единствено ограничение прави persistence едноредов проблем:
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"));Въпросът за correctness не е запазването. Той е какво се случва по пътя обратно, а наивният отговор ви таксува два пъти. Ако процесът е умрял, след като model е поискал инструмент, но преди резултатът да бъде записан, resume, който започва с ново извикване на model, плаща за ход, който вече има — а ако започне с повторно пускане на инструментите, извършва write два пъти.
Поправката е цикълът да започва, като пита transcript какво е outstanding:
export function pending(state: RunState): ToolCall[] {
const answered = new Set(state.messages.filter((m) => m.role === "tool").map((m) => m.tool_call_id));
const last = state.messages.at(-1);
if (last?.role !== "assistant") return [];
return (last.tool_calls ?? []).filter((c) => !answered.has(c.id));
}Всяка итерация първо източва pending и пита model само когато няма нищо outstanding. Resume става същият code path като нормалния, както и approval — approved call е просто pending call, който вече е позволено да се изпълни. Убийте процеса по средата на задачата и го рестартирайте:
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Две изпълнения на инструменти през два процеса за задача, която има нужда от две, а крайната цена е идентична с изпълнението, което никога не се е crash-нало. Цената се натрупва през restart, защото беше в state, не в променлива.
Счупване седем: три минути тишина
Връзка към раздела: Счупване седем: три минути тишинаscan_archive отнема три секунди тук и представя инструмент, който отнема три минути в production. Две неща липсват, докато работи: потребителят няма представа, че нещо се случва, а бутонът Stop не прави нищо.
И двете имат една и съща поправка — AbortSignal от глава 14, избутан един слой по-надолу. Signal не е само за fetch — той се подава в инструмента, а добре написан инструмент го зачита:
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"Две милисекунди от клика до спирането, защото sleep вътре в инструмента слуша същия signal като fetch. Прекарайте го само в fetch и идентичният бутон Stop чака три секунди — дължината на инструмента — а изпълнението „се отменя“, след като работата, която е отменяло, вече е приключила. Cancellation, която не е прокарана докрай надолу, е spinner, който казва правилната дума.
Trace, и защо не е log
Връзка към раздела: Trace, и защо не е logHarness излъчва по един ред на event, а речникът е достатъчно малък, за да се запомни: 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}Три свойства правят това trace, а не logging. Всеки ред носи run id, така че изпълнение, което обхваща три процеса и два дни, е една query. Всеки ред turn носи собствените си token counts и текущата цена, така че „защо това изпълнение струваше четиридесет долара“ има отговор след факта, вместо да е възпроизводимо само на теория. А run_stopped носи причината, което е полето, превръщащо support ticket в едноредов отговор: agent, спрял на бюджета, и agent, който е crash-нал, изглеждат идентични отвън и изискват противоположни реакции.
Аритметиката на latency
Връзка към раздела: Аритметиката на latencyГлава 13 измери time to first token върху hardware, който притежавате. Глава 14 го измери през socket. Agent го умножава, а множителят е число, което никой не е избрал:
Същата задача от три хода, като се променя само latency на provider:
| latency на provider за ход | wall clock, 3 хода |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2.413 ms |
Самият harness допринася петнадесет милисекунди към изпълнение от три хода. Всичко останало е , умножено по число, което не контролирате — зададено вътре в serving scheduler, който batch-ва вашия request с requests на непознати6 — а се избира от model. Затова streaming от глава 14 има по-голямо значение тук, отколкото в chat, и помага по-малко: можете да stream-вате последния ход, а четирите хода преди него са тишина, освен ако harness не излъчва progress. Това е и целият аргумент за event tool_progress по-горе — в agent честната единица feedback не е token, а стъпката.
Същият harness, реален model зад порта
Връзка към раздела: Същият harness, реален model зад портаВсичко по-горе работеше срещу scripted provider, което доказва harness и не доказва нищо за models. Затова сменете един ред — seam от глава 14, LLM_BASE_URL — и насочете идентичния код към локален Qwen2.5-0.5B-Instruct със същите четири инструмента. Шест задачи върху същите три файла:
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Три открития, като третото е причината този раздел да съществува.
Всяка една задача приключи в точно два хода. Лимитът на ходовете никога не се задейства, бюджетът никога не се задейства и единственият изход на цикъла беше model да произведе проза. Model с половин милиард параметри не iterate-ва; той отговаря на втория си дъх, независимо дали има това, което му трябва. Броят ходове е свойство на model, не на вашия цикъл.
Средният ход отне 6.908 милисекунди, така че таблицата с latency по-горе не е играчка: при този размер хипотетично изпълнение от осем хода е почти минута wall clock без нищо на екрана.
И отговорите са грешни. Най-големият файл е errors.log; model изброи файловете, никога не ги прочете и въпреки това посочи един. Първата задача позна име на файл, беше уведомена, че той не съществува, и приключи. Harness изпълни безупречно във всичките шест run-а. Harness прави agent governable, не правилен — глава 29 е как разбирате кое от двете, а глава 30 е какво струва, когато никой не го е направил.
Subagents, назовани тук и таксувани по-късно
Връзка към раздела: Subagents, назовани тук и таксувани по-късноЕдин инструмент в каталога може да има друго изпълнение зад себе си. Интерфейсът е този от глава 18 — schema и endpoint — и цял agent се побира зад него, защото интерфейсът е тесен:
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";
},
};Три неща вече са правилни в тези десет реда и трите са последствия от решенията по-горе: child има собствен window, така че transcript на parent получава summary, а не всичко, което child е прочел; има собствени limits, така че runaway child не може да похарчи бюджета на parent; и наследява signal, така че един Stop отменя дървото. Защо чистият window е целта, а не side effect, е глава 24; петте orchestration patterns — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — и handoff са глава 25.
Къде са frameworks и защо този курс не използва такъв
Връзка към раздела: Къде са frameworks и защо този курс не използва такъвНищо по-горе не трябва да се чете като аргумент срещу libraries. Измерено на 7 септември 2026 г. за месеца, приключващ на 29 август:7
| package | изтегляния през този месец | какво ви дава |
|---|---|---|
ai (Vercel AI SDK) | 89.385.860 | ToolLoopAgent, stopWhen, approval за инструменти, step hooks |
@anthropic-ai/claude-agent-sdk | 41.558.352 | Claude Code harness като library: цикъл, sessions, hooks, permissions, subagents8 |
@langchain/langgraph | 12.812.815 | цикълът като explicit state graph |
langchain | 11.359.058 | chains, agents, integrations |
@openai/agents | 6.093.155 | agents, handoffs, guardrails |
@mastra/core | 5.914.502 | agents, workflows, memory |
Причината този курс да пише цикъла на ръка, вместо да преподава един от тях, е заявена, не подразбрана, и е измерима. За дванадесетте месеца до 7 септември 2026 г. ai публикува 945 версии и се премести от major 5 към major 7, а неговият agent class все още се export-ва като Experimental_Agent; langchain публикува 132 версии в същия прозорец; @openai/agents публикува 83 и все още е на 0.x, петнадесет месеца след първото си release.7 Глава, написана срещу който и да е от тези API, остарява за един сезон, а тази се публикува на тридесет и три езика, така че всяко преиздание струва целия превод. Това, което стои под всички тях, не се движи: цикъл, правило за спиране, каталог, executor, малко state.
И референтната имплементация е съгласна с тази глава за частта, която има значение. В ai версия 7.0.93 изходът на цикъла не е число — той е stopWhen, списък от predicates, от които броят стъпки е само един:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsСпирането е в множествено число в най-използваната имплементация на този цикъл по същата причина, по която е в множествено число и в сто деветдесет и шестте реда по-горе.
Накъде продължава това
Връзка към раздела: Накъде продължава товаВече имате harness: цикъл, каталог, executor, пет изхода, persisted run, signal, който стига до инструментите, и trace с run id на всеки ред. Глави 24, 25, 29 и 30 градят върху този файл, а 26 до 28 — върху това, до което той може да достигне.
Остава му един проблем, а измерванията по-горе сочеха към него през цялото време. Погледнете runaway таблицата още веднъж: 3.431 input tokens при осем хода, 337.299 при сто. Погледнете работещия run: 204, 269, 342. Всеки ход изпраща отново целия transcript, така че context на agent се пълни със собствената му история — а model използва далечния край на дълъг window по-зле от близкия, затова добър agent на ход пет е объркан на ход четиридесет.
Лимитът на ходовете не поправя това. Той само ви спира да плащате, за да го гледате как се случва. Това, което го поправя, е да решавате на всеки отделен ход кои tokens заслужават window: какво да compact-нете, какво да преместите в note, който agent може да fetch-не, какво да дадете на subagent с чист window и кои дефиниции на инструменти си струват постоянния данък. Глава 24 измерва къде window реално отива — и изненадата е, че не е разговорът.
Източници и метод
Връзка към раздела: Източници и методВсяко число в тази глава излезе от двата сървъра, описани по-горе, на Node 22 през loopback interface: scripted provider, броящ tokens с encoding o200k_base, и Qwen/Qwen2.5-0.5B-Instruct зад endpoint със същата форма, greedy decoding, на CPU. Разходите са изчислени от измерените token counts по тарифите, които глава 16 прочете на 6 септември 2026 г. — $2.00 на милион input tokens и $12.00 на милион output — и нито един request в тази глава не отиде към платен endpoint. Отговорите на локалния model са отговори на малък model; четете ги като evidence за цикъла, който е идентичен и в двата случая, а не като benchmark за това какво правят текущите models.
Препратки
Връзка към раздела: Препратки-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. and Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629 (2022). Преплитането на reasoning traces и actions, което цикълът имплементира, и източникът на наблюдението, че acting позволява на model да „handle exceptions“ — което е точно това, което таблицата с tool-error по-горе измерва. ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. and Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA). arXiv:2309.02427 (2023). Формалното третиране на това, което цикълът по-горе прави неформално: modular memory components, structured action space, обхващащ internal memory и external environments, и „a generalized decision-making process to choose actions“. Прочетете го за речника, който липсва на индустриалния термин — особено разделянето на working, episodic, semantic и procedural memory, чиято практическа сянка е таблицата с три store-а в глава 24. ↩
-
ai(Vercel AI SDK) версия 7.0.93, публикувана на 4 септември 2026 г.; декларациите за типове са прочетени отcdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tsна 7 септември 2026 г. Файлът от 397 KB съдържа нула срещания на низаharness. Agent class еdeclare class ToolLoopAgent, export-нат и катоToolLoopAgent, и катоExperimental_Agent;declare function isStepCount(stepCount: number)— export-нат катоstepCountIs— е цитиран дословно по-горе;type StopConditionе показан без втория си type parameter (RUNTIME_CONTEXT extends Context = Context), което е единственото съкращение в excerpt-а, както и shape наstopWhen?: Arrayable<StopCondition<...>>върхуgenerateTextиstreamText. Същият файл декларираtoolApproval,ToolApprovalStatus,prepareStepиrepairToolCall, тоест референтната имплементация независимо е стигнала до approval gates, per-step preparation и error repair. ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. and Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023). Резюмето нарича артефакта „evaluation framework“ от 2.294 проблема и никога не използва думата „harness“; собственото README на проекта (
github.com/SWE-bench/SWE-bench, прочетено на 7 септември 2026 г.) я използва пет пъти, винаги като „evaluation harness“, а entry point еpython -m swebench.harness.run_evaluation. Това е другият смисъл на думата: scaffold, който държи agent неподвижен и го оценява, не цикълът, който го изпълнява. ↩ -
Anthropic, Building effective agents, 19 декември 2024 г.,
anthropic.com/engineering/building-effective-agents, прочетено на 7 септември 2026 г. Augmented model като building block, agent като LLM, „using tools based on environmental feedback in a loop“, и препоръката за stopping conditions „such as a maximum number of iterations“ за запазване на контрол. Глава 22 цитира дефиницията му изцяло. ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. and Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023). Другият цикъл — serving scheduler, който batch-ва вашия request с requests на непознати и управлява KV cache от глава 13. Струва си да знаете, че съществува точно защото не е ваш: latency, която вашият harness умножава, се задава вътре в него и никаква работа по вашия цикъл не я премества. ↩
-
Броеве изтегляния от npm registry,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, explicit window вместо rollinglast-month, и release histories отregistry.npmjs.org/<package>; и двете query-нати на 7 септември 2026 г. Release counts са броят версии, публикувани за дванадесетте месеца до тази дата, включително canary builds:ai945 (последна 7.0.93 на 2026-09-04, с major версии 5, 6 и 7, всички появили се в window),langchain132 (последна 1.5.10 на 2026-08-20),@openai/agents83 (последна 0.17.0 на 2026-08-19, първо публикуван на 2025-06-03). ↩ ↩2 -
Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) е Claude Code harness, пакетиран като library — agent loop, built-in file и shell tools, context management, sessions, hooks, permissions и subagents — документиран наcode.claude.com/docs/en/agent-sdk. Това е най-близкото нещо до публикуван разказ за всеки механизъм, който тази глава изгражда на ръка, и си струва да се чете до собствената ви имплементация за частите, които назовава, а тази глава само маркира. ↩