Agent Harness बनाएँ: Loop और उससे बाहर निकलने के पाँच रास्ते
पहली कोशिश में चलने वाला 15-line loop, फिर जानबूझकर सात बार तोड़ा गया—एक runaway से शुरू, जिसकी लागत capped run से 77 गुना थी।
इस पेज पर
ईमानदार हिस्से से शुरू करें, क्योंकि कोई और यह नहीं कहेगा: “harness” jargon है, standard नहीं। इसकी कोई specification, कोई committee, कोई reference definition नहीं है। यह chapter जिन चार papers का हवाला देता है — ReAct,1 CoALA,2 SWE-bench और vLLM — उनमें से किसी के abstract में यह शब्द एक बार भी नहीं आता। इस चीज़ की सबसे ज़्यादा downloaded implementation, Vercel का ai package, जिसके महीने में 89.4 million downloads हैं, भी इसे इस्तेमाल नहीं करता: version 7.0.93 के साथ shipped 397 KB type declarations में string harness शून्य बार आती है।3 एक जगह जहाँ यह शब्द वाकई load-bearing है, वहाँ इसका मतलब पूरी तरह कुछ और है। SWE-bench अपने README में “harness” पाँच बार कहता है, हर बार evaluation harness के रूप में — containerised scaffold जो patch apply करता है और tests चलाता है — और उसका Python module सचमुच swebench.harness.run_evaluation है।4
तो दो अलग चीज़ें एक नाम साझा करती हैं। Evaluation harness agent को स्थिर रखता है और उसे score करता है। agent harness वह program है जो agent चलाता है: वह model को call करता है, model जो माँगता है उसे execute करता है, कब रुकना है तय करता है, और बीच की state संभालता है। यह chapter दूसरा वाला बनाता है, TypeScript की दो सौ lines से कम में, बिना किसी framework के।
Loop खुद पंद्रह lines का है और पहली कोशिश में काम करता है। उसके बाद की हर चीज़ उससे बाहर निकलने का एक तरीका है।
विवरण दिखाएँ
इस chapter को पहले वालों से क्या चाहिए।
- Chapter 14 client के लिए: deadlines, status triage, cancellation, idempotency keys, और वही mock provider technique जो यहाँ फिर इस्तेमाल होगी।
- Chapter 16 arithmetic के लिए: input tokens conversation के square के साथ बढ़ते हैं, और नीचे इस्तेमाल rates वही हैं जो वहाँ 6 September 2026 को पढ़े गए थे।
- Chapter 18 tool catalogue के लिए: एक schema जो model देखता है, एक endpoint जो वह कभी नहीं देखता, और यह rule कि errors exceptions नहीं बल्कि context हैं।
- Chapter 22 उस loop के लिए जिसे यह inherit करता है, और “agent” की दो published definitions के लिए जो एक-दूसरे से असहमत हैं।
यहाँ tensors नहीं हैं। यह course का दूसरा dependency hub है: Chapters 24, 25, 29 और 30 नीचे वाली file पर चलते हैं, और 26 से 28 उस पर बनते हैं जहाँ तक यह पहुँच सकता है।
A provider you can script
सेक्शन का लिंक: A provider you can scriptChapter 14 को real provider के against नहीं लिखा जा सकता था, क्योंकि आप उससे किसी चुने हुए पल पर 429 नहीं माँग सकते। इस chapter में वही समस्या अलग आकार में है: आप real model से demand पर और reproducibly runaway होने, या लगातार दो बार identical tool request करने को नहीं कह सकते।
इसलिए पहला program एक scripted provider है: chat completions API के shape वाला endpoint, जिसकी reply turn index और tools ने अब तक क्या लौटाया है, इस पर function की तरह निर्भर करती है। यह real byte-pair encoder से tokens count करता है, इसलिए नीचे का पैसा सजावट नहीं बल्कि arithmetic है।
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);Design को दो lines ढोती हैं। Turn index conversation से derived है, किसी variable में held नहीं, इसलिए provider stateless है और run को kill करके उसके against resume किया जा सकता है। और recover decide करने से पहले tool results पढ़ता है: अपने transcript को पढ़ने वाला scripted model यह मापने के लिए न्यूनतम जरूरत है कि harness ने उसे पढ़ने लायक कुछ दिया भी या नहीं।
Catalogue Chapter 18 का है, तीन files पर चार tools: list_files, read_file, delete_file — needsApproval marked — और scan_archive, जो जानबूझकर slow है।
The loop that works
सेक्शन का लिंक: The loop that worksयह पूरी idea है, उन हिस्सों से पहले जो इसे survivable बनाते हैं।
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 पर point करें और यह ठीक वही करता है जैसा दिखता है:
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तीन turns, दो tool executions, US cent का एक चौथाई। आख़िरी line पर ध्यान दें: 204, 269, 342। हर turn उससे पहले की हर चीज़ दोबारा भेजता है, यानी Chapter 16 का quadratic bill वहाँ आ रहा है जहाँ किसी ने कुछ type नहीं किया। इस chapter का बाकी हिस्सा यह है कि जब वह line बढ़ना बंद नहीं करती तो क्या होता है।
Break one: the task that never ends
सेक्शन का लिंक: Break one: the task that never endsउसी loop को runaway script पर point करें — ऐसा model जो हर single turn में tool माँगता है और कभी prose emit नहीं करता — और marked return कभी fire नहीं करता। कोई दूसरा exit नहीं है। Program तब तक चलता है जब तक process मर न जाए या credit card।
Fix एक line है, literature जिस पहले control की recommendation करता है,5 वही है, और अंततः हर कोई इसे लिखता है। जो लगभग कोई नहीं करता, वह है मापना कि इसकी कीमत क्या है:
| turn cap | model calls | input tokens | cost |
|---|---|---|---|
| 8 | 8 | 3,431 | $0.009070 |
| 20 | 20 | 16,259 | $0.038038 |
| 50 | 50 | 88,649 | $0.191098 |
| 100 | 100 | 337,299 | $0.702198 |
आख़िरी दो rows साथ पढ़ें। Cap को 50 से 100 double करने से cost double नहीं हुई; वह 3.7 गुना हुई। Input tokens 88,649 से 337,299 हो गए, 3.8 का factor, क्योंकि turn अपने साथ हर previous turn लेकर चलता है और total है। Turn cap कोई linear dial नहीं है। यह आपके worst case के square root पर dial है, इसलिए 20 से 100 तक “सिर्फ़ safe रहने के लिए” बढ़ाना ऐसा decision है जिसकी price पहले निकालनी चाहिए।
Break two: a cap on turns is not a cap on money
सेक्शन का लिंक: Break two: a cap on turns is not a cap on moneyTurn cap की समस्या यह है कि turn की fixed price नहीं होती। Short transcript पर twenty turns की cost ऊपर $0.038 थी। 200-tool catalogue, retrieved document set और history के forty messages के साथ twenty turns उसकी hundreds of times cost कर सकते हैं, और cap को पता नहीं होता। Operator जिस चीज़ को bound करना चाहता है, वह bill है।
तो loop पैसे count करता है, Chapter 16 के computeCost को वहाँ पढ़े गए rates के against इस्तेमाल करके — इस पूरे course में priced model के लिए $2.00 per million input tokens और $12.00 per million output:
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 script, कोई turn cap नहीं, तीन budgets:
| budget | turns reached | actually spent |
|---|---|---|
| $0.01 | 9 | $0.010780 |
| $0.05 | 24 | $0.051790 |
| $0.20 | 52 | $0.205398 |
दो चीज़ें नाम लेकर कहने लायक हैं। पहली, budget हर बार turns की different संख्या खरीदता है, और यही point है: यह उस चीज़ को bound कर रहा है जिसकी operator को परवाह है, और turn count को transcript जहाँ रखता है वहाँ गिरने देता है। दूसरी, हर row overshoot करती है। Budget $0.010 था और $0.010780 spent हुआ, क्योंकि check turn से पहले चलता है और turn की price उसके खत्म होने तक ज्ञात नहीं होती। आप spend को exactly bound नहीं कर सकते; आप उसे एक turn की cost के भीतर bound कर सकते हैं। Interface में ऐसा ही कहें, pretend न करें, और check call से पहले रखें ताकि overshoot एक turn हो, दो नहीं।
Five ways to leave the loop, not one
सेक्शन का लिंक: Five ways to leave the loop, not oneअब तक loop के तीन exits हैं, और बाकी chapter का shape दिखने लगा है। Production run exactly पाँच तरीकों में से एक से खत्म होता है, और वे एक-दूसरे की variations नहीं हैं:
| how it ends | who decided | what the caller should do |
|---|---|---|
| the model stopped asking | model | answer पढ़ें |
| turn cap | आपने, पहले से | cap बढ़ाएँ, या partial result accept करें |
| budget exhausted | आपने, पहले से | और पैसा approve करें, या partial result accept करें |
| an error you cannot retry | provider या tool | deployment ठीक करें; Chapter 14 का triage decide करता है |
| a human intervened | व्यक्ति | verdict का wait करें, फिर resume करें |
इन सबको एक boolean में collapse करना इस file की सबसे common design mistake है, और यह एक specific तरीके से expensive है: पाँच में से तीन resumable हैं और दो नहीं। Turn cap hit करने वाले agent के पास valid transcript, real partial result और next step है; 401 पाने वाले agent के पास इनमें से कुछ नहीं। इसलिए harness reason को data के रूप में record करता है:
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 };Break three: a tool fails
सेक्शन का लिंक: Break three: a tool failsChapter 18 एक claim पर खत्म हुआ था, number के बिना: tool का error raise करने के बजाय model को tool result की तरह वापस दें, और model आमतौर पर खुद को fix कर लेता है। यहाँ number है।
एक failure, तीन policies। Scripted model ऐसी file guess करता है जो exist नहीं करती; tool no such file: timeout.log. Call list_files to see what exists. throw करता है
| what the harness does with the error | turns | tool runs | cost | what the user got |
|---|---|---|---|---|
| उसे loop से बाहर throw करता है | 1 | 1 | $0.000756 | stack trace |
Error: the tool failed. return करता है | 2 | 1 | $0.001462 | “I could not read the file, so I do not know.” |
| वास्तव में क्या हुआ return करता है | 4 | 3 | $0.003550 | “errors.log mentions a timeout.” |
तीसरी row पहली से 4.7 गुना cost करती है और वही अकेली question का answer देती है। और दूसरी row interesting है, क्योंकि most codebases वास्तव में यही करती हैं: error catch हुआ, loop survive किया, model को बताया गया कि कुछ fail हुआ, पर यह नहीं कि क्या, और उसने politely give up कर दिया। Row two और three का अंतर error handling नहीं है। यह reader के लिए लिखी गई एक sentence है।
इसलिए harness thrown tool को data की तरह treat करता है, और wording को policy बनाता है:
} 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);
}Chapter 18 ने दूसरे side के बारे में भी warn किया था, और उसकी भी price है। Loop को ऐसे tool पर point करें जो ऐसे reason से fail होता है जिसे कोई message fix नहीं कर सकता — एक read जिसे process perform करने की permission नहीं — और model उसे forever 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 के eleven identical executions जो succeed नहीं कर सकती, fixable one से recover करने वाले run की cost से 5.2 गुना, और अंत में कुछ नहीं। Errors context हैं; permanent error ऐसा context है जो बाकी run को poison कर देता है। यह distinction Chapter 14 का status triage एक layer ऊपर move किया हुआ है: जिस error पर model act कर सकता है वह transcript में वापस जाता है, और जिस पर नहीं कर सकता उसे reason के साथ run stop कर देना चाहिए। Turn cap आज आपके और दूसरे case के बीच खड़ा है, जो floor है, fix नहीं।
Break four: the same call, twice
सेक्शन का लिंक: Break four: the same call, twiceअब वह failure जिसे most people मानते हैं कि हो ही नहीं सकता। Models खुद को repeat करते हैं। किसी भी loop को enough long चलाएँ और आप identical tool को identical arguments के साथ दो consecutive turns में देखेंगे।
Same task के no-repeat baseline के against measured:
| turns | tool runs | cost | |
|---|---|---|---|
| task, no repeat | 2 | 1 | $0.001396 |
| वही task, one call repeated | 3 | 2 | $0.002446 |
| repeated, read-only tools पर result cache के साथ | 3 | 1 | $0.002446 |
Duplicated call ने $0.001050 extra, 75 % increase cost किया, और यह हिस्सा लोगों को चौंकाता है: result cache करने से उसका कुछ भी recover नहीं हुआ। Deduplication ने tool execution बचाया, turn नहीं, क्योंकि जब तक आपका code repeat notice करता है, model को पूछने के लिए पहले ही pay किया जा चुका होता है। Saving real है जब tool slow, rate-limited, या per call billed हो — और जिस line item ने growth दिखाया उस पर यह zero है।
इससे worse version है। वही cache write करने वाले tool पर apply करें, और second call silently नहीं होती:
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"]इनमें से कौन सही है? Knowably, कोई नहीं। Protocol कहता है ये दो calls हैं: इनके पास दो अलग tool_call_id values हैं। Arguments कहते हैं वे शायद एक हों। Argument strings compare करके decide करने वाला harness एक दिन दो identical, intended charges में से दूसरा निगल जाएगा — और Chapter 14 ने पहले ही वह एकमात्र mechanism नाम दिया है जो इसे honestly resolve करता है: logical operation per generated idempotency key, उस layer द्वारा जो जानती है operation है क्या। जब तक tool एक carry नहीं करता, defensible default ऊपर वाला read-only gate है: reads cache करें, writes execute करें, और write की अपनी idempotency को बाकी संभालने दें।
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;
}Break five: it deletes something
सेक्शन का लिंक: Break five: it deletes somethingdestructive script files list करता है और फिर task में कभी mention न की गई एक file delete करने को कहता है। अब तक loop में ऐसा कुछ नहीं जो इसे रोक दे।
needsApproval marked tool fail नहीं करता और proceed नहीं करता। यह run stop करता है और control return करता है, decision लेने के लिए व्यक्ति को जो कुछ चाहिए उसके साथ:
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."यही पूरा mechanism है, और यह callback के बजाय return इसलिए है क्योंकि next section: stop और verdict के बीच process शायद exist ही न करे।
लेकिन पहले, वह measurement जिसकी किसी को उम्मीद नहीं होती। Rejection result की absence नहीं है — transcript में tool_call_id keyed slot है और उसमें कुछ जाना ही होगा। वही rejection दो बार run करें, सिर्फ़ यह बदलते हुए कि वह कुछ क्या कहता है:
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."किसी भी run में कुछ delete नहीं हुआ, और दूसरे में user को बताया गया कि हुआ। Permission system ने perfectly काम किया; report झूठ है। यह tool-error table जैसा ही mechanism है, बस कहीं ज़्यादा important जगह पर — human ने no कहा, action correctly blocked हुआ, और agent की summary reality से contradict करती है क्योंकि refusal कभी वहाँ लिखा ही नहीं गया जहाँ model पढ़ता है। इससे निकलने वाला rule छोटा है: आपका code tool call के बारे में जो भी decide करे, decision को transcript में words में लिखें। Chapter 30 security side से इस पर लौटता है, जहाँ यही audit trail और fiction का अंतर है।
Break six: the process dies
सेक्शन का लिंक: Break six: the process diesApproval में minutes या hours लगते हैं। Deploy में seconds। अगर run HTTP request के अंदर local variable में रहता है, तो हर restart lost run है और हर approval race है।
इसलिए run closure नहीं है। यह plain serialisable object है — messages, turn count, cost, status, interruption, approved call ids की list — और loop इस पर pure function है। वही single constraint persistence को one-line concern बनाता है:
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 question saving नहीं है। यह है कि वापस आते समय क्या होता है, और naive answer आपको double-charge करता है। अगर process model के tool माँगने के बाद लेकिन result लिखे जाने से पहले मर गया, तो resume जो model को फिर call करके शुरू होता है, उस turn के लिए pay करता है जो उसके पास पहले से है — और अगर वह tools re-run करके शुरू होता है, तो write दो बार perform करता है।
Fix यह है कि loop शुरू होते ही 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));
}हर iteration पहले pending drain करती है और model से तभी पूछती है जब कुछ outstanding नहीं। Resume normal path जैसा ही code path बन जाता है, और approval भी — approved call बस pending call है जिसे अब run करने की अनुमति है। Process को mid-task kill करें और restart करें:
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ऐसे task के लिए जिसे दो चाहिए, दो processes में कुल दो tool executions, और final cost वही है जो उस run की थी जो कभी crash नहीं हुआ। Cost restart के across accumulate होती है क्योंकि वह state में थी, variable में नहीं।
Break seven: three minutes of silence
सेक्शन का लिंक: Break seven: three minutes of silencescan_archive यहाँ तीन seconds लेता है और production में तीन minutes लेने वाले tool का stand-in है। चलते समय दो चीज़ें missing हैं: user को पता नहीं कि कुछ हो रहा है, और Stop button कुछ नहीं करता।
दोनों की fix same है, और यह Chapter 14 का AbortSignal एक level deeper pushed है। Signal सिर्फ़ fetch के लिए नहीं है — यह tool के अंदर pass होता है, और well-written tool उसे honour करता है:
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"Click से stop तक दो milliseconds, क्योंकि tool के अंदर की sleep उसी signal को सुनती है जिसे fetch सुनता है। इसे सिर्फ़ fetch में thread करें और identical Stop button तीन seconds wait करता है — tool की length — और run “cancel” तब होता है जब जिस work को cancel करना था वह पहले ही finish हो चुका होता है। Cancellation जो all the way down plumbed नहीं है, वह बस ऐसा spinner है जो सही word कहता है।
The trace, and why it is not a log
सेक्शन का लिंक: The trace, and why it is not a logHarness हर event पर एक line emit करता है, और vocabulary याद रखने जितनी छोटी है: 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}तीन properties इसे logging के बजाय trace बनाती हैं। हर line run id carry करती है, इसलिए तीन processes और दो दिनों तक फैला run एक query है। हर turn line अपने token counts और running cost carry करती है, इसलिए “इस run की cost forty dollars क्यों थी” का answer बाद में दिया जा सकता है, न कि केवल theory में reproduce किया जा सकता है। और run_stopped reason carry करता है, वही field जो support ticket को one-line answer बनाता है: budget पर रुका agent और crash हुआ agent बाहर से identical दिखते हैं और opposite responses मांगते हैं।
The arithmetic of latency
सेक्शन का लिंक: The arithmetic of latencyChapter 13 ने आपके own hardware पर time to first token measure किया। Chapter 14 ने socket के through measure किया। Agent उसे multiply करता है, और multiplier ऐसा number है जिसे किसी ने choose नहीं किया:
वही three-turn task, केवल provider की latency बदलते हुए:
| provider latency per turn | wall clock, 3 turns |
|---|---|
| 0 ms | 15 ms |
| 200 ms | 615 ms |
| 800 ms | 2,413 ms |
Harness खुद three-turn run में पंद्रह milliseconds contribute करता है। बाकी सब है, ऐसे number से multiplied जिसे आप control नहीं करते — serving scheduler के अंदर set, जो आपकी request को strangers की requests के साथ batch कर रहा है6 — और model choose करता है। इसी वजह से Chapter 14 की streaming यहाँ chat से ज़्यादा matter करती है और कम help करती है: आप final turn stream कर सकते हैं, और उसके पहले के four turns silence हैं जब तक harness progress emit नहीं करता। यही ऊपर के tool_progress event का पूरा argument भी है — agent में feedback की honest unit token नहीं, step है।
The same harness, a real model behind the port
सेक्शन का लिंक: The same harness, a real model behind the portऊपर की हर चीज़ scripted provider के against चली, जो harness prove करता है और models के बारे में कुछ prove नहीं करता। तो एक line बदलें — Chapter 14 का seam, LLM_BASE_URL — और identical code को same four tools के साथ local Qwen2.5-0.5B-Instruct पर point करें। Same three files पर छह tasks:
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तीन findings, और तीसरा ही इस section के होने की वजह है।
हर single task exactly two turns में finish हुआ। Turn cap कभी fire नहीं हुआ, budget कभी fire नहीं हुआ, और loop का only exit model का prose produce करना था। Half-billion-parameter model iterate नहीं करता; उसके पास जो चाहिए हो या न हो, वह अपने second breath पर answer देता है। Turn count आपके loop की नहीं, model की property है।
Mean turn ने 6,908 milliseconds लिए, इसलिए ऊपर वाली latency table toy नहीं है: इस size पर hypothetical eight-turn run nearly एक minute wall clock है जिसमें screen पर कुछ नहीं।
और answers wrong हैं। Largest file errors.log है; model ने files list कीं, उन्हें कभी read नहीं किया, और फिर भी एक नाम बता दिया। First task ने file name guess किया, उसे बताया गया कि वह exist नहीं करती, और उसने conclude कर दिया। सभी six runs में harness flawlessly execute हुआ। Harness agent को governable बनाता है, correct नहीं — Chapter 29 बताता है कि आप पता कैसे लगाते हैं कि कौन सा है, और Chapter 30 बताता है कि जब किसी ने नहीं किया तो इसकी cost क्या है।
Subagents, named here and charged later
सेक्शन का लिंक: Subagents, named here and charged laterCatalogue में एक tool के पीछे another run हो सकता है। Interface Chapter 18 का है — schema और endpoint — और पूरा agent उसके पीछे fit हो जाता है क्योंकि interface narrow है:
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";
},
};इन ten lines में तीन चीज़ें पहले से सही हैं और तीनों ऊपर लिए decisions के consequences हैं: child के पास अपनी window है, इसलिए parent के transcript को summary मिलती है, child ने जो कुछ read किया वह सब नहीं; उसके पास अपने limits हैं, इसलिए runaway child parent का budget spend नहीं कर सकता; और वह signal inherit करता है, इसलिए एक Stop पूरे tree को cancel करता है। Clean window point क्यों है, side effect नहीं, यह Chapter 24 है; पाँच orchestration patterns — prompt chaining, routing, parallelisation, orchestrator-workers, evaluator-optimiser — और handoff Chapter 25 हैं।
Where the frameworks are, and why this course did not use one
सेक्शन का लिंक: Where the frameworks are, and why this course did not use oneऊपर की किसी बात को libraries के against argument की तरह नहीं पढ़ना चाहिए। 7 September 2026 को measured, 29 August को खत्म होने वाले month के लिए:7
| package | downloads that month | what it gives you |
|---|---|---|
ai (Vercel AI SDK) | 89,385,860 | ToolLoopAgent, stopWhen, tool approval, step hooks |
@anthropic-ai/claude-agent-sdk | 41,558,352 | Claude Code harness as a library: loop, sessions, hooks, permissions, subagents8 |
@langchain/langgraph | 12,812,815 | explicit state graph के रूप में loop |
langchain | 11,359,058 | chains, agents, integrations |
@openai/agents | 6,093,155 | agents, handoffs, guardrails |
@mastra/core | 5,914,502 | agents, workflows, memory |
यह course उनमें से किसी को teach करने के बजाय loop हाथ से क्यों लिखता है, कारण implied नहीं बल्कि declared है, और measurable है। 7 September 2026 तक के बारह महीनों में, ai ने 945 versions publish किए और major 5 से major 7 तक move किया, और उसकी agent class अभी भी Experimental_Agent के रूप में export होती है; langchain ने same window में 132 versions publish किए; @openai/agents ने 83 publish किए और first release के पंद्रह months बाद भी 0.x पर है।7 इनमें से किसी API के against लिखा chapter एक season में stale हो जाता है, और यह one thirty-three languages में publish होता है, इसलिए हर re-edition पूरे translation की cost बनती है। इनके नीचे जो है वह नहीं बदलता: loop, stopping rule, catalogue, executor, कुछ state।
और reference implementation इस chapter से उस part पर agree करती है जो matter करता है। ai version 7.0.93 में loop का exit number नहीं है — वह stopWhen है, predicates की list, जिनमें step count बस एक है:3
type StopCondition<TOOLS extends ToolSet> = (options: { steps: Array<StepResult<TOOLS>> }) => PromiseLike<boolean> | boolean;
declare function isStepCount(stepCount: number): StopCondition<any, any>; // exported as stepCountIsStopping इस loop की most-used implementation में plural है, उसी वजह से जिस वजह से ऊपर की hundred and ninety-six lines में plural है।
Where this goes next
सेक्शन का लिंक: Where this goes nextअब आपके पास harness है: loop, catalogue, executor, बाहर निकलने के पाँच रास्ते, persisted run, tools तक पहुँचने वाला signal, और हर line पर run id वाला trace। Chapters 24, 25, 29 और 30 इस file पर बनते हैं, और 26 से 28 उस पर जहाँ तक यह पहुँच सकता है।
इसमें एक problem बची है, और ऊपर की measurements पूरे समय उसी की ओर इशारा करती रही हैं। Runaway table फिर देखें: eight turns पर 3,431 input tokens, hundred पर 337,299। Working run देखें: 204, 269, 342। हर turn पूरा transcript दोबारा भेजता है, इसलिए agent का context अपनी ही history से भर जाता है — और model long window के far end को near end से worse use करता है, इसलिए turn five पर अच्छा agent turn forty पर confused agent बन जाता है।
Turn cap इसे fix नहीं करता। वह बस आपको इसे होते हुए देखने के लिए pay करने से रोकता है। Fix यह है कि हर single turn पर decide किया जाए कि कौन से tokens window deserve करते हैं: क्या compact करना है, क्या बाहर किसी note में move करना है जिसे agent fetch कर सके, clean window वाले subagent को क्या hand करना है, और कौन सी tool definitions अपने permanent tax के लायक हैं। Chapter 24 measure करता है कि window actually कहाँ जाती है — और surprise यह है कि वह conversation नहीं है।
Sources and method
सेक्शन का लिंक: Sources and methodइस chapter का हर number ऊपर described दो servers से आया, Node 22 पर loopback interface के over: o200k_base encoding से tokens count करने वाला scripted provider, और same shape के endpoint के पीछे Qwen/Qwen2.5-0.5B-Instruct, greedy decoding, CPU पर। Costs measured token counts से compute की गई हैं, उन rates पर जिन्हें Chapter 16 ने 6 September 2026 को पढ़ा — $2.00 per million input tokens और $12.00 per million output — और इस chapter की कोई request paid endpoint पर नहीं गई। Local model के answers small model के answers हैं; उन्हें loop के बारे में evidence की तरह पढ़ें, जो दोनों तरह identical है, current models क्या करते हैं उसके benchmark की तरह नहीं।
संदर्भ
सेक्शन का लिंक: संदर्भ-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K. और Cao, Y. ReAct: Synergizing Reasoning and Acting in Language Models। arXiv:2210.03629 (2022)। Reasoning traces और actions की interleaving जिसे loop implement करता है, और observation का source कि acting model को “handle exceptions” करने देता है — यही ऊपर tool-error table measure करती है। ↩
-
Sumers, T. R., Yao, S., Narasimhan, K. और Griffiths, T. L. Cognitive Architectures for Language Agents (CoALA)। arXiv:2309.02427 (2023)। ऊपर का loop informally जो करता है उसका formal treatment: modular memory components, internal memory और external environments को span करने वाला structured action space, और “a generalized decision-making process to choose actions”। इसे उस vocabulary के लिए पढ़ें जो industry term में missing है — खासकर working, episodic, semantic और procedural memory का separation, जिसका practical shadow Chapter 24 की three-store table है। ↩
-
ai(Vercel AI SDK) version 7.0.93, 4 September 2026 को published; type declarationscdn.jsdelivr.net/npm/ai@7.0.93/dist/index.d.tsसे 7 September 2026 को पढ़े गए। 397 KB file में stringharnessकी zero occurrences हैं। Agent classdeclare class ToolLoopAgentहै,ToolLoopAgentऔरExperimental_Agentदोनों के रूप में exported;declare function isStepCount(stepCount: number)—stepCountIsके रूप में exported — ऊपर verbatim quoted है;type StopConditionअपने second type parameter (RUNTIME_CONTEXT extends Context = Context) के बिना दिखाया गया है, जो excerpt में अकेला elision है, जैसेgenerateTextऔरstreamTextपरstopWhen?: Arrayable<StopCondition<...>>का shape। वही filetoolApproval,ToolApprovalStatus,prepareStepऔरrepairToolCalldeclare करती है, यानी reference implementation independently approval gates, per-step preparation और error repair तक पहुँची है। ↩ ↩2 -
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O. और Narasimhan, K. SWE-bench: Can Language Models Resolve Real-World GitHub Issues? arXiv:2310.06770 (2023)। Abstract artefact को 2,294 problems का “evaluation framework” कहता है और “harness” word कभी use नहीं करता; project का अपना README (
github.com/SWE-bench/SWE-bench, 7 September 2026 को पढ़ा गया) इसे पाँच बार use करता है, हमेशा “evaluation harness” के रूप में, और entry pointpython -m swebench.harness.run_evaluationहै। यह word का दूसरा sense है: agent को स्थिर रखकर score करने वाला scaffold, न कि उसे चलाने वाला loop। ↩ -
Anthropic, Building effective agents, 19 December 2024,
anthropic.com/engineering/building-effective-agents, 7 September 2026 को पढ़ा गया। Building block के रूप में augmented model, agent को LLM के रूप में “using tools based on environmental feedback in a loop”, और control maintain करने के लिए stopping conditions “such as a maximum number of iterations” की recommendation। Chapter 22 इसकी definition full quote करता है। ↩ -
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H. और Stoica, I. Efficient Memory Management for Large Language Model Serving with PagedAttention। arXiv:2309.06180 (2023)। दूसरा loop — serving scheduler जो आपकी request को strangers की requests के साथ batch करता है और Chapter 13 का KV cache manage करता है। यह जानना कि वह exist करता है, ठीक इसलिए important है क्योंकि वह आपका नहीं है: आपका harness जिस latency को multiply करता है वह उसी के अंदर set होती है, और आपके loop पर कोई भी काम उसे move नहीं करता। ↩
-
npm registry download counts,
api.npmjs.org/downloads/point/2026-07-31:2026-08-29/<package>, rollinglast-monthone के बजाय explicit window, और release historiesregistry.npmjs.org/<package>से; दोनों 7 September 2026 को queried। Release counts उस date तक के बारह महीनों में published versions की संख्या हैं, canary builds सहित:ai945 (latest 7.0.93 on 2026-09-04, major versions 5, 6 और 7 सभी window के अंदर आए),langchain132 (latest 1.5.10 on 2026-08-20),@openai/agents83 (latest 0.17.0 on 2026-08-19, first published 2025-06-03)। ↩ ↩2 -
Claude Agent SDK (
@anthropic-ai/claude-agent-sdk) Claude Code harness को library के रूप में packaged करता है — agent loop, built-in file और shell tools, context management, sessions, hooks, permissions और subagents —code.claude.com/docs/en/agent-sdkपर documented। यह इस chapter द्वारा हाथ से बनाए गए हर mechanism के published account के सबसे करीब है, और अपनी implementation के साथ पढ़ने लायक है, खासकर उन parts के लिए जिन्हें यह chapter केवल gesture करता है। ↩