<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agent | kaguc — Writing to understand systems.</title><link>https://kaguc.com/tag/agent/</link><atom:link href="https://kaguc.com/tag/agent/index.xml" rel="self" type="application/rss+xml"/><description>Agent</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 29 Jul 2026 00:00:00 +0000</lastBuildDate><image><url>https://kaguc.com/media/logo.svg</url><title>Agent</title><link>https://kaguc.com/tag/agent/</link></image><item><title>27 days of AI-assisted development: a retrospective in numbers</title><link>https://kaguc.com/blog/ai-assisted-27-days/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/ai-assisted-27-days/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/ai-assisted-27-days-zh/">中文&lt;/a>. Part 14 of the Engineering LLM Applications series — the closing installment. The previous thirteen articles discussed mechanisms; this one returns to the 27 days that produced them.&lt;/em>&lt;/p>
&lt;h2 id="scope-and-accounting-rules">Scope and accounting rules&lt;/h2>
&lt;p>From 2026-06-10 to 07-06, one person plus a coding agent (Claude Code) built, from scratch, the AI writing agent this series has been citing throughout. Because the project enforced a paper-trail rule from day one — every development conversation produces a devlog entry — this retrospective does not rely on memory. The data comes from 94 devlogs, git history, a count of the test directory, and a line-by-line sum of the research experiment&amp;rsquo;s usage.jsonl. The article is organized as four ledgers: output, discipline, debt, and conclusions. Approximate figures state their accounting basis; anything the logs marked as &amp;ldquo;suspected cause&amp;rdquo; stays hedged here.&lt;/p>
&lt;h2 id="the-output-ledger">The output ledger&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimension&lt;/th>
&lt;th>Number&lt;/th>
&lt;th>Basis&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Time span&lt;/td>
&lt;td>27 days&lt;/td>
&lt;td>2026-06-10 → 2026-07-06&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Devlogs&lt;/td>
&lt;td>94 entries&lt;/td>
&lt;td>docs/devlog/, one per conversation; single-day peak of 27 (06-15)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Backend&lt;/td>
&lt;td>28 modules / 8,352 lines&lt;/td>
&lt;td>line count over backend/app/*.py&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Clients&lt;/td>
&lt;td>Web frontend + Tauri desktop build&lt;/td>
&lt;td>PyInstaller-frozen backend + Tauri shell + bundled TinyTeX&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tests&lt;/td>
&lt;td>351&lt;/td>
&lt;td>count of def test in backend/tests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Research experiment&lt;/td>
&lt;td>4 days / 20 commits&lt;/td>
&lt;td>writing-judgment-distill, 06-18 → 06-21&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment LLM usage&lt;/td>
&lt;td>3,511 calls / ~7.94M tokens&lt;/td>
&lt;td>summed from results/usage.jsonl: ~5.22M input + ~2.72M output&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Product pivots&lt;/td>
&lt;td>3&lt;/td>
&lt;td>manual workflow → local web app (06-10) → SaaS (06-12) → standalone desktop (06-22)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two notes. First, output came in bursts: of the 94 devlogs, 27 landed on 06-15 alone and 14 on 06-11 — at the granularity of &amp;ldquo;one conversation advances one feature&amp;rdquo;, parallel sprints became the norm. Second, the third pivot (SaaS → standalone) is worth recording as method: every change was flag-gated and off by default; on landing day, all 47 backend tests passed in the default configuration — zero regression in SaaS behavior. The test baseline then grew to 344 passed (devlog figures), with the repository ultimately counting 351 test functions.&lt;/p>
&lt;h2 id="the-discipline-ledger-which-conventions-paid-for-themselves">The discipline ledger: which conventions paid for themselves&lt;/h2>
&lt;p>Of the working conventions written into CLAUDE.md on day one, four were repeatedly proven worth their cost by day 27.&lt;/p>
&lt;p>&lt;strong>A devlog for every conversation.&lt;/strong> Fixed format: goal / what was done / key decisions / files changed / leftovers. The direct payoff is this article — a retrospective that needs no recollection. The larger payoff is attribution: the venv-pollution debt described below can be reconstructed as a complete causal chain from two log entries one hour apart. The research experiment ran the same regime — journal entries are append-only and machine-readable results go into runs.tsv — which is what allowed the summary document to state that &amp;ldquo;all numbers come from recorded experiments.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Git as the safety net: commit every state that runs.&lt;/strong> The very first conversation ended with git init and a commit (f705076, 115 files). None of the three pivots ever lost work: the standalone pivot branched from the current HEAD with zero disturbance to the working tree, preserving the experiment branch&amp;rsquo;s 20 commits intact. &amp;ldquo;If the AI breaks it, roll back&amp;rdquo; went from slogan to daily practice over the 27 days.&lt;/p>
&lt;p>&lt;strong>base_docs is read-only.&lt;/strong> Domain material, few-shot samples, and test data live in a read-only zone; generated artifacts go into separate directories. Through three pivots and dozens of generation rounds, the evidence base was never contaminated.&lt;/p>
&lt;p>&lt;strong>One feature per change.&lt;/strong> Twenty-seven devlogs in one day is not 27× productivity; it is 27 scope-narrowed conversations, paired with &amp;ldquo;one commit plus tests per feature.&amp;rdquo; This is the &lt;a href="https://kaguc.com/blog/deterministic-boundary/">deterministic boundary&lt;/a> applied isomorphically to the development process itself: narrow the scope of every change, and defend existing assets with deterministic gates — tests and git.&lt;/p>
&lt;h2 id="the-debt-ledger-verification-debt-and-environment-debt">The debt ledger: verification debt and environment debt&lt;/h2>
&lt;p>&lt;strong>Verification debt, case one: a batch verification that failed wholesale.&lt;/strong> On 07-05, to benchmark the agent pipeline against the state of the art, a 105-agent web-research workflow was run. Its adversarial verification stage failed as a whole batch when the subscription&amp;rsquo;s session quota was exhausted: all 25 claims came back unverified. The only remedy was manual: pick the 3 papers carrying the most weight — and most likely to have been fabricated by a retrieval agent — and check them line by line against the arXiv originals. One had its core numbers confirmed (revisions regress 16–27% of already-covered content); one was genuine but required two corrections to how it had been cited; one was architecturally real, with its specific numbers absent from the abstract and therefore left marked unverified. The machine produced 25 conclusions in one evening; human verification covered 3. That gap is the shape of verification debt.&lt;/p>
&lt;p>&lt;strong>Verification debt, case two: a conclusion frozen for want of a human anchor.&lt;/strong> The 4-day controlled experiment obtained highly direction-consistent results under LLM-judge scoring (see &lt;a href="https://kaguc.com/blog/distill-judgment-experiment/">part 12&lt;/a>); the 30-pair anonymized blind-evaluation sheet was exported, and the machine&amp;rsquo;s verdict on the same items was B=20 / A=3 / tie=7. But human scores were never collected. The summary document therefore had to settle for: H1 is &amp;ldquo;a strong candidate with method-level evidentiary support — not established.&amp;rdquo; The one step that 3,511 calls and ~7.94M tokens could not buy was precisely the only step independent of the machine.&lt;/p>
&lt;p>&lt;strong>Environment debt: one experiment&amp;rsquo;s contamination, detonating an hour later.&lt;/strong> On 06-23, minted code highlighting was trialed for the standalone build; the conclusion was that it is unworkable under frozen distribution, so the build reverted to the safe default. But the experiment&amp;rsquo;s pip installs had touched the shared venv. One hour later (by log timestamps), the packaged desktop app failed to start; one layer of the four-layer root-cause analysis was a missing pydantic_core — the log annotates it as &amp;ldquo;suspected cause: the minted detour&amp;rsquo;s pip installs overrode the pydantic version.&amp;rdquo; The recorded lesson, verbatim: clean up the venv after temporary experiments.&lt;/p>
&lt;h2 id="the-methodological-conclusion-the-bottleneck-moved-to-verification-and-judgment">The methodological conclusion: the bottleneck moved to verification and judgment&lt;/h2>
&lt;p>The output ledger shows AI assistance expanding the engineering scale one person can drive by an order of magnitude — a full-stack application, desktop distribution, 351 tests, and a controlled experiment advancing simultaneously within 27 days. That is a case judgment against the author&amp;rsquo;s own manual pace, not a controlled measurement. But the three debts point to a single conclusion: &lt;strong>once scale expands, the bottleneck moves from production to verification and judgment.&lt;/strong> Twenty-five claims needed a human reading the originals; H1 needed 30 human blind evaluations; the venv contamination needed a human-driven layer-by-layer diagnosis. Every conclusion the machine produces needs an anchor independent of the machine — deterministic tests, git history, arXiv originals, human scores. The mechanisms discussed across the first thirteen articles of this series are, in the end, ways of manufacturing such anchors at scale.&lt;/p>
&lt;h2 id="where-this-retrospective-does-not-apply">Where this retrospective does not apply&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>N=1.&lt;/strong> One developer, a greenfield project, no collaboration friction, no legacy code. These numbers are a record, not a statistic; extrapolating to teams or brownfield systems has no basis here.&lt;/li>
&lt;li>&lt;strong>Output metrics are not outcome metrics.&lt;/strong> Lines, tests, and devlogs measure production volume; the 27 days include no user data, so product value went unmeasured.&lt;/li>
&lt;li>&lt;strong>Devlog count is not workload.&lt;/strong> Devlogs count conversations; a 27-entry day reflects short, scope-narrowed sessions, not 27× throughput.&lt;/li>
&lt;li>&lt;strong>The token figures cover only the experiment&amp;rsquo;s metering.&lt;/strong> usage.jsonl records the research experiment&amp;rsquo;s API calls only; the coding agent&amp;rsquo;s own usage during main-app development is not metered there, so total cost cannot be extrapolated from these numbers.&lt;/li>
&lt;li>&lt;strong>&amp;ldquo;Zero regression&amp;rdquo; was purchased in advance.&lt;/strong> Three pivots without incident rested on flag-gating plus a test baseline; without that baseline, the same speed produces silent breakage.&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>The three papers manually re-verified after the 105-agent workflow: &lt;em>Beyond Single-shot Writing: Deep Research Agents are Unreliable at Multi-turn Report Revision&lt;/em> (arXiv:2601.13217; core numbers checked against the original); &lt;em>PaperOrchestra: A Multi-Agent Framework for Automated AI Research Paper Writing&lt;/em> (arXiv:2604.05018; genuine, with two citation corrections); RP-ReAct (arXiv:2512.03560; architecture genuine, specific numbers not independently verified).&lt;/li>
&lt;li>Experiment data and statistical basis: the writing-judgment-distill SUBMISSION_SUMMARY and results/usage.jsonl.&lt;/li>
&lt;/ul></description></item><item><title>Controlled revision in reflection loops</title><link>https://kaguc.com/blog/reflection-gate/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/reflection-gate/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/reflection-gate-zh/">中文&lt;/a>. Part 10 of the Engineering LLM Applications series.&lt;/em>&lt;/p>
&lt;h2 id="the-problem-why-draft--self-revise--n-often-makes-things-worse">The problem: why &amp;ldquo;draft → self-revise × N&amp;rdquo; often makes things worse&lt;/h2>
&lt;p>The standard closing act of an autonomous generation pipeline is a reflection loop: once the first draft exists, let the model critique and revise it for a few rounds. Intuition says more rounds can&amp;rsquo;t hurt. Our first implementation was a direct transcription of that intuition — critique produced a prose commentary, revise rewrote the whole document accordingly, and the new draft replaced the previous one unconditionally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">msgs&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">draft&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">yield&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;draft&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">draft&lt;/span>&lt;span class="p">}&lt;/span> &lt;span class="c1"># keep this version unconditionally&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Empirical measurement says the intuition is wrong. arXiv:2601.13217 (&lt;em>Beyond Single-shot Writing: Deep Research Agents are Unreliable at Multi-turn Report Revision&lt;/em> — we read the arXiv original to verify it) measured multi-turn revision in deep research agents: when handling feedback, full-rewrite revision &lt;strong>regresses 16–27% of already-covered content on average&lt;/strong>, with citation quality degrading alongside. More alarming: &lt;strong>unguided &amp;ldquo;please reflect and revise yourself&amp;rdquo; is ineffective for almost every agent tested&lt;/strong> — only 1 of 5 improved (+3.6%) — and it causes &lt;strong>more collateral damage&lt;/strong> than targeted feedback. On the mechanism side, Self-Refine (arXiv:2303.17651, retrieval excerpt, not adversarially verified) is consistent: feedback must be specific and actionable, generic feedback measurably hurts, gains concentrate in the first 1–2 rounds, and tasks where errors are hard to self-detect see almost no benefit. The positive contrast is PaperOrchestra (arXiv:2604.05018): its refinement loop carries an explicit &amp;ldquo;accept only if better, revert and stop if worse&amp;rdquo; gate and reports a 79–81% win rate with 0% degradation (a body-text detail, not independently verified).&lt;/p>
&lt;p>Held against that evidence, our first implementation stepped on all three mines:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Failure mode&lt;/th>
&lt;th>Evidence&lt;/th>
&lt;th>Corresponding rework&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Full-document rewrite&lt;/td>
&lt;td>Regresses 16–27% of covered content, citation degradation (2601.13217, manually verified)&lt;/td>
&lt;td>Rework 1 + Rework 2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Open-ended prose feedback&lt;/td>
&lt;td>Generic feedback measurably hurts; unguided self-reflection nearly useless with more collateral damage&lt;/td>
&lt;td>Rework 1: locate to section + actionable step&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No accept/revert gate&lt;/td>
&lt;td>Only gated refinement achieves 79–81% / 0% degradation (PaperOrchestra, not independently verified)&lt;/td>
&lt;td>Rework 3: zero-token regression gate&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The three sections below walk through the rework in order of increasing invasiveness. The context is a LaTeX report-generation agent (scan → explore → figures → write → reflect × N → bib → compile self-repair), but the mechanisms are format-agnostic.&lt;/p>
&lt;h2 id="rework-1-reflection-outputs-a-structured-edit-plan">Rework 1: reflection outputs a structured edit plan&lt;/h2>
&lt;p>The first cut lands on the shape of the feedback. The critique is no longer prose; it is forced into a JSON edit plan (prompt translated here — the production prompt is Chinese):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">_REFLECT_SYS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;You are a reviewer of lab reports / course notes. Check the current draft &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;against the source material and identify only problems that genuinely need &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;fixing, locating each one to a specific section (give its anchor, e.g. n2); &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;use &lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">whole&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2"> for cross-section / overall-structure / abstract-and-keywords &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;issues. Each item carries an issue and an actionable action. Do not mention &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;what is already fine; no generic polishing. Output strictly JSON: &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;{&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">changes&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">:[{&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">section&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">n2 or whole&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">,&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">issue&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="se">\&amp;#34;\&amp;#34;&lt;/span>&lt;span class="s2">,&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">action&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="se">\&amp;#34;\&amp;#34;&lt;/span>&lt;span class="s2">}]}; &amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;if the draft is acceptable overall, output {&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">changes&lt;/span>&lt;span class="se">\&amp;#34;&lt;/span>&lt;span class="s2">:[]} and nothing else.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four design points:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Every issue must be located to a specific section&lt;/strong> (the anchor assigned by the sectionizer, e.g. &lt;code>n2&lt;/code>); cross-section, overall-structure, and abstract/keywords issues are explicitly marked &lt;code>&amp;quot;whole&amp;quot;&lt;/code>. Location is not decoration — it decides which application path the item takes next.&lt;/li>
&lt;li>&lt;strong>Issue and action come as a pair.&lt;/strong> &amp;ldquo;The error analysis is shallow&amp;rdquo; alone is not enough; an executable action is required — the direct application of Self-Refine&amp;rsquo;s &amp;ldquo;feedback must be specific and actionable&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>The empty array is allowed.&lt;/strong> &lt;code>{&amp;quot;changes&amp;quot;:[]}&lt;/code> means &amp;ldquo;acceptable overall&amp;rdquo; and terminates the reflection loop early. Revision flips from &amp;ldquo;change by default&amp;rdquo; to &amp;ldquo;no change by default, change only with cause&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Parse failure never breaks the flow.&lt;/strong> &lt;code>_parse_reflect_plan&lt;/code> returns an empty list for non-JSON or missing &lt;code>changes&lt;/code>, which the caller treats as &amp;ldquo;no changes needed&amp;rdquo;; plans are capped at 8 items; the title fallback when an anchor misses is &amp;ldquo;exact match first, then the longest contained title&amp;rdquo; — so &amp;ldquo;Results&amp;rdquo; cannot steal a match meant for &amp;ldquo;Results and Discussion&amp;rdquo;.&lt;/li>
&lt;/ol>
&lt;p>A companion change: reflection now runs on a fresh minimal context (system + source material + current draft) instead of dragging along the full exploration transcript; the material sits as a stable prefix and hits the cache across rounds.&lt;/p>
&lt;h2 id="rework-2-pure-section-level-issues-go-through-scoped-per-section-splicing">Rework 2: pure section-level issues go through scoped per-section splicing&lt;/h2>
&lt;p>Each plan item is routed by its location. Pure section-level issues no longer trigger a full rewrite; they reuse the mature machinery of the interactive editing side (sectionizer, scoped prompts, fragment health check, deterministic splice — the toolkit of &lt;a href="https://kaguc.com/blog/versioning-scoped-edit/">Part 07&lt;/a>), applied section by section. For each item, &lt;code>_scoped_reflect_apply&lt;/code> re-parses and re-resolves the target on the &lt;strong>current working text&lt;/strong> (offsets have moved after the previous splice), sends the model only the target section plus that item&amp;rsquo;s issue/action plus relevant material, and health-checks the returned fragment (non-empty, balanced braces, balanced environments, no &lt;code>\documentclass&lt;/code>). A fragment that fails the check causes that section to be skipped — &lt;strong>a bad fragment is never spliced back&lt;/strong>. A fragment that passes is spliced in, with every other section byte-identical. Only cross-section issues, abstract/keywords issues, or drafts that cannot be sectionized fall back to a whole-document minimal rewrite.&lt;/p>
&lt;p>This step has one implementation pitfall, found by an internal adversarial audit (rated [medium]):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># The fragment must preserve the count of top-level \section commands within the&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># replaced span (usually =1; =0 for a subsection) — otherwise the splice changes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># the document&amp;#39;s section count, the positional anchors of subsequent scoped items&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># in the same round drift and edit the wrong section, and the &amp;#34;added sections&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># direction is not caught by regression_check (it only reports decreases).&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Count mismatch → skip this section; never splice a fragment that would shift things.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">re&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">findall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">section\{&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">new_text&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">re&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">findall&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">r&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\\&lt;/span>&lt;span class="s2">section\{&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">section_src&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The model occasionally gets &amp;ldquo;helpful&amp;rdquo; and splits one section into two: the fragment itself is healthy and passes every check, but after splicing, the document&amp;rsquo;s section count goes up by one, the positional anchors of all subsequent plan items in the same round drift, and the wrong sections get edited — while the regression gate only reports section &lt;em>decreases&lt;/em>, leaving this path with no backstop. The conservation check closes it off before the splice.&lt;/p>
&lt;h2 id="rework-3-the-zero-token-regression-gate">Rework 3: the zero-token regression gate&lt;/h2>
&lt;p>Whether a round went through scoped splicing or the whole-document fallback, its output passes &lt;code>regression_check&lt;/code> before landing — six criteria, all deterministic, zero API calls:&lt;/p>
&lt;ul>
&lt;li>loss of the compilable skeleton (&lt;code>\documentclass&lt;/code> / &lt;code>\end{document}&lt;/code>);&lt;/li>
&lt;li>length collapse below 60% of the previous version (truncation, or &amp;ldquo;rest unchanged&amp;rdquo;-style elision);&lt;/li>
&lt;li>structural score decrease;&lt;/li>
&lt;li>shrinkage of the &lt;code>\cite&lt;/code> key set (with the lost keys reported);&lt;/li>
&lt;li>fewer sections;&lt;/li>
&lt;li>fewer figures or data tables.&lt;/li>
&lt;/ul>
&lt;p>Any trigger means regressed: revert to the previous version and stop reflecting. If the two versions are character-identical after whitespace stripping, the result is &lt;code>converged&lt;/code> — also an early stop, so convergence wastes no rounds.&lt;/p>
&lt;p>Why not use an LLM judge as the gate? The reason is written in the module&amp;rsquo;s own comments: this particular failure mode — losing content — &lt;strong>is deterministically detectable&lt;/strong>, whereas the pairwise judge&amp;rsquo;s reliability in the Chinese domain is unverified (the judge module&amp;rsquo;s own caveat) and would cost one extra real call per round. More correct, and cheaper. The judge&amp;rsquo;s proper place is offline evaluation, not an online gate; the validity question is the subject of &lt;a href="https://kaguc.com/blog/judge-validity/">Part 09&lt;/a>.&lt;/p>
&lt;p>The gate has its own edge case: in the pipeline, bibliography generation runs &lt;strong>after&lt;/strong> reflection, so during reflection the bib is always empty — which makes the &amp;ldquo;citations closed&amp;rdquo; check false for any draft containing &lt;code>\cite&lt;/code>, and a newly added citation would be misread as a structural-score drop, wrongly reverting a good edit. The fix: while the bib is empty, that check is excluded from the gate score, and citation loss is judged separately by &amp;ldquo;the key set must not shrink&amp;rdquo; — pinned by a dedicated regression test (empty bib produces no false positive).&lt;/p>
&lt;p>Gate decisions are surfaced as persisted step events (visible in the log page and across reconnects), with a structured &lt;code>reflect_gate&lt;/code> field attached to the trace so the experiment bench can observe whether reflection is actually improving quality and which round reverted.&lt;/p>
&lt;h2 id="the-reworked-loop-end-to-end">The reworked loop, end to end&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">flowchart TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> A[Current draft vN] --&amp;gt; B[Reviewer on fresh minimal context: JSON edit plan]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B --&amp;gt;|changes empty| S[Judged acceptable, early stop]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B --&amp;gt;|pure section-level| C[Scoped per-section edit + splice, other sections byte-identical]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B --&amp;gt;|contains whole items| D[Whole-document minimal rewrite]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> C --&amp;gt; G{Zero-token regression_check}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> D --&amp;gt; G
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> G --&amp;gt;|degraded| X[Revert to vN, stop reflecting]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> G --&amp;gt;|converged| S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> G --&amp;gt;|pass| V[Commit as vN+1, next round]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tests arrived in batches with the rework: when the gate landed, &lt;code>test_eval_report&lt;/code> +7 (accept / revert / converge / lost section / lost citation / length collapse / empty-bib no false positive) and &lt;code>test_run_agent_mock&lt;/code> +1 (degraded revision → revert to previous version and stop), with 303 backend tests green; 306 after scoped reflection landed; 322 by the end of this round of work (plus 19 on the experiment bench). Generation quality itself cannot be tested offline — but every deterministic component of the loop can: plan parsing, routing, splicing, the gate.&lt;/p>
&lt;h2 id="lessons-from-the-research-process-itself">Lessons from the research process itself&lt;/h2>
&lt;p>The evidence chain behind this rework deserves its own post-mortem. To benchmark against the state of the art, we ran a 105-agent networked research workflow; the adversarial-verification stage &lt;strong>failed wholesale&lt;/strong> when the subscription session quota was exhausted, leaving all 25 claims stuck at unverified. So we manually re-verified the 3 most load-bearing papers — the ones a retrieval agent would most plausibly fabricate — by reading the arXiv originals. 2601.13217: real, core numbers confirmed. PaperOrchestra: real, but with two corrections — it is not a Google paper, and its abstract describes itself as flexible rather than rigidly fixed, so it can only support &amp;ldquo;structured multi-agent division of labor beats autonomous loops&amp;rdquo; (win rate +50–68% / overall quality +14–38%), not &amp;ldquo;the more rigid the better&amp;rdquo;. RP-ReAct (arXiv:2512.03560): the architecture is real, but the specific scores in the retrieval claim about &amp;ldquo;being beaten by vanilla ReAct on simple tasks&amp;rdquo; do not appear in the abstract — plausibly extracted or confabulated from the body text by the retrieval agent, so they are marked unverified. One more calibration correction: the retrieval claim said &amp;ldquo;break rate 21–31%&amp;rdquo;, which is the body-text framing; the abstract says 16–27%, and this article uses the abstract&amp;rsquo;s figure.&lt;/p>
&lt;p>The lesson is isomorphic to the article&amp;rsquo;s thesis: &lt;strong>a retrieval agent&amp;rsquo;s output needs an accept/revert gate of its own&lt;/strong> — manual verification is the regression_check of the research workflow.&lt;/p>
&lt;h2 id="when-not-to-do-this">When not to do this&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>The gate&amp;rsquo;s criteria are proxies: they prevent getting worse, they do not certify getting better.&lt;/strong> Section counts, citation sets, and length all measure structural preservation; content that is wrong but structurally intact passes the gate. Preference-level quality still requires offline evaluation and judges — this gate does not replace them.&lt;/li>
&lt;li>&lt;strong>Scoped reflection presupposes sectionizable structure.&lt;/strong> A draft that cannot be parsed goes straight down the whole-document fallback — that fallback is not a defect, it is an acknowledgment of the boundary.&lt;/li>
&lt;li>&lt;strong>When the structure itself is wrong, scoped editing is powerless.&lt;/strong> Problems requiring global reorganization (moving sections, merging sections, changing the narrative line) must be marked whole and take the full rewrite; forcing them through per-section edits produces drafts that are locally fluent and globally incoherent.&lt;/li>
&lt;li>&lt;strong>Do not bolt this gate onto loops that already have an external signal.&lt;/strong> The compile self-repair loop has the compiler as its referee and stops on no-progress error fingerprints — a different kind of closed loop, and already the right design; adding a structural gate to it is redundant construction.&lt;/li>
&lt;li>&lt;strong>Keep N small.&lt;/strong> Self-Refine reports gains concentrated in the first 1–2 rounds (not adversarially verified), and 2601.13217 shows the break rate is the bottleneck of multi-turn revision — budget spent on a fifth reflection round is most likely paying for a revert.&lt;/li>
&lt;/ol>
&lt;p>This article pairs with &lt;a href="https://kaguc.com/blog/deterministic-boundary/">Part 11, &amp;ldquo;The deterministic boundary&amp;rdquo;&lt;/a>: that article promotes the gate and the scoped machinery into a unified architectural principle; this one is the complete rework record of that principle applied to a single closed loop — the reflection loop.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;em>Beyond Single-shot Writing: Deep Research Agents are Unreliable at Multi-turn Report Revision&lt;/em> (arXiv:2601.13217) — full-rewrite revision regresses 16–27% of covered content on average with citation degradation; unguided self-reflection is nearly useless and causes more collateral damage; its named remedies: structured edit plans and a dedicated Reviser. Verified against the original.&lt;/li>
&lt;li>&lt;em>PaperOrchestra: A Multi-Agent Framework for Automated AI Research Paper Writing&lt;/em> (arXiv:2604.05018) — structured multi-agent pipelines beat autonomous baselines by +50–68% / +14–38% (verified); its accept-or-revert refinement gate reports a 79–81% win rate with 0% degradation (body-text detail, not independently verified).&lt;/li>
&lt;li>&lt;em>Self-Refine: Iterative Refinement with Self-Feedback&lt;/em> (arXiv:2303.17651) — feedback must be specific and actionable, generic feedback measurably hurts; gains concentrate in the first 1–2 rounds; tasks where errors are hard to self-detect need external signals. Retrieval excerpt, not adversarially verified.&lt;/li>
&lt;li>&lt;em>Reason-Plan-ReAct&lt;/em> (arXiv:2512.03560) — plan/execute decoupled architecture (verified); the specific scores for &amp;ldquo;beaten by vanilla ReAct on simple tasks&amp;rdquo; are absent from the abstract and unverified.&lt;/li>
&lt;/ul></description></item><item><title>From chat to agent: tool loops and collapse breakers</title><link>https://kaguc.com/blog/agent-loop/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/agent-loop/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/agent-loop-zh/">中文&lt;/a>. Part 4 of the Engineering LLM Applications series. The series is grounded in the implementation and measurements of a production-grade AI writing agent (FastAPI + React + Tauri, 351 test cases).&lt;/em>&lt;/p>
&lt;h2 id="the-problem-handing-the-loop-to-the-model">The problem: handing the loop to the model&lt;/h2>
&lt;p>The first three articles of this series were about single-call engineering: controlling input, assembling prompts, sanitizing output. The watershed of an agent is that &lt;strong>what to do next&lt;/strong> is also handed to the model: give it a tool set, execute its calls, feed the results back into the message list, and call again — until the task is done.&lt;/p>
&lt;p>The concrete scenario for this article: give the agent a data folder whose file names and contents are unknown in advance, and have it read files on its own, figure out what experiment this is, and write the report. The naive implementation is a &lt;code>while True&lt;/code> tool loop, and in production it fails in three ways:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Untrusted paths.&lt;/strong> The model decides which file to read; a single &lt;code>../&lt;/code> in an argument reads outside the data directory.&lt;/li>
&lt;li>&lt;strong>No natural endpoint.&lt;/strong> &amp;ldquo;How much exploration is enough&amp;rdquo; is the model&amp;rsquo;s judgment call; there is no upper bound on iterations, and therefore none on cost.&lt;/li>
&lt;li>&lt;strong>Loop collapse.&lt;/strong> The most insidious class: in long contexts, the model degenerates into calling the same tool with identical arguments. A public case, qwen-code issue #4695, records 43 consecutive &lt;code>git status&lt;/code> calls consuming 8.9M tokens in one session; the deepseek-v4-pro tier we use is also prone to collapsing into this pattern in long contexts. SDK retries cannot help — retries only cover network errors and rate limits, while each of these calls &amp;ldquo;succeeds&amp;rdquo; at the API layer. The breaker has to live on the client side.&lt;/li>
&lt;/ol>
&lt;p>The rest of this article walks through five mechanisms in build order. The implementation uses standard OpenAI-compatible tool calling (DeepSeek by default; any compatible endpoint can be swapped in); the entry point &lt;code>run_agent&lt;/code> is a generator that yields events one by one for SSE streaming.&lt;/p>
&lt;h2 id="mechanism-1-a-minimal-tool-set-with-a-path-guard">Mechanism 1: a minimal tool set with a path guard&lt;/h2>
&lt;p>The exploration phase has five tools: &lt;code>list_dir&lt;/code>, &lt;code>read_file&lt;/code>, &lt;code>read_image&lt;/code> (a local vision model extracts readings from screenshots and figures), &lt;code>make_figure&lt;/code> (executes model-written matplotlib code to produce plots), and &lt;code>finish_exploration&lt;/code>. Three design points:&lt;/p>
&lt;p>&lt;strong>Segmented continuation instead of stuffing everything in at once.&lt;/strong> &lt;code>read_file&lt;/code> clips text at &lt;code>READ_CHARS = 7000&lt;/code> characters and, when it clips, appends one line: &amp;ldquo;truncated — use offset/limit to continue reading by lines&amp;rdquo;. Truncation is not an error; it is a decision point for the model: there is more, and whether it is worth reading further is the model&amp;rsquo;s trade-off to make. It also imposes a constraint on the loop-breaker fingerprint later — continuation reads with different offsets are legitimate behavior and must not be flagged.&lt;/p>
&lt;p>&lt;strong>Explicit phase transition.&lt;/strong> &lt;code>finish_exploration&lt;/code> turns &amp;ldquo;exploration is done&amp;rdquo; from an implicit signal (the model simply stops calling tools) into an explicit call: the schema requires &lt;code>experiment&lt;/code> / &lt;code>goal&lt;/code> / &lt;code>key_findings&lt;/code>, with three optional slots for assignment requirements, methodology notes, and a reference report. The phase transition leaves a structured record — and gives the breaker a well-defined exit to steer toward.&lt;/p>
&lt;p>&lt;strong>Every path entry goes through &lt;code>_safe&lt;/code>.&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_safe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">root&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">root&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">base_docs_root&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">t&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">normpath&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">rel&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">root&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">t&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">root&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sep&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">root&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">t&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After &lt;code>normpath&lt;/code> joining, the resolved path must still be inside the root; an escape returns &lt;code>(None, None)&lt;/code>, and the tool layer turns that into a &amp;ldquo;path does not exist or is out of bounds&amp;rdquo; string &lt;strong>returned as the tool result&lt;/strong> — the model sees it and can correct itself, and the main flow never raises. The &lt;code>make_figure&lt;/code> tool description also spells out hard sandbox rules (no importing os/sys/subprocess and the like, no open()/eval/exec, no double-underscore attributes); its two-layer sandbox implementation is the subject of &lt;a href="https://kaguc.com/blog/code-sandbox/">the next article&lt;/a>.&lt;/p>
&lt;h2 id="mechanism-2-a-zero-llm-scan-before-exploration-starts">Mechanism 2: a zero-LLM scan before exploration starts&lt;/h2>
&lt;p>Before the model begins exploring, &lt;code>scan_folder&lt;/code> performs a deterministic recursive scan: files are coarsely bucketed by extension into documents / images / data / other, returning the bucketed listing and counts — zero LLM calls, zero tokens. It serves two purposes: an instant whole-folder snapshot for the user (the detail of the first step event), and deterministic input for the later material-gap check. The principle is the same as in &lt;a href="https://kaguc.com/blog/deterministic-boundary/">Part 11&lt;/a>: &amp;ldquo;what is in the folder&amp;rdquo; is a question code can answer, and the model&amp;rsquo;s exploration budget should be spent on &amp;ldquo;which files to read and what to make of them&amp;rdquo;. One cost note in passing: the exploration tool loop runs on the cheap model tier (reading files and deciding what to read is simple work); drafting and reflection use the main model.&lt;/p>
&lt;h2 id="mechanism-3-two-lines-of-defense--a-budget-cap-and-a-collapse-breaker">Mechanism 3: two lines of defense — a budget cap and a collapse breaker&lt;/h2>
&lt;p>&lt;code>MAX_TOOL_ITERS = 20&lt;/code> is a budget cap, not a defense: a collapsed agent will burn all 20 iterations on repeated calls and produce nothing. The real defense is &lt;code>_LoopBreaker&lt;/code>, built around a call fingerprint:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_tool_fingerprint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Tool-call fingerprint = name + full arguments (sorted JSON).&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ensure_ascii&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sort_keys&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s2">&amp;#34;|&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">args&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three design constraints: &lt;strong>full arguments&lt;/strong> — &lt;code>offset=0&lt;/code> and &lt;code>offset=100&lt;/code> are two different fingerprints, so legitimate segmented reads are never flagged (a unit test pins this property); &lt;strong>sorted serialization&lt;/strong> — argument key order does not affect the verdict; &lt;strong>never raises&lt;/strong> — serialization failure falls back to &lt;code>str()&lt;/code>. Wrap-up tools like &lt;code>finish_exploration&lt;/code> are excluded from counting.&lt;/p>
&lt;p>The thresholds come in two tiers. Third occurrence of the same fingerprint (&lt;code>_LOOP_WARN&lt;/code>): inject one wrap-up prompt — &amp;ldquo;You are repeatedly calling the same tool with the same arguments. What you have read is sufficient; call finish_exploration immediately and state your conclusions&amp;rdquo; — with &lt;code>nudge_once&lt;/code> guaranteeing a single injection per run, because the injection itself occupies context and repeated injections would accelerate the collapse. Fifth occurrence (&lt;code>_LOOP_STOP&lt;/code>): hard-break out of the exploration loop. The break is not a crash: the agent yields a &amp;ldquo;repeated identical tool call detected — exploration cut off, writing from what has been read&amp;rdquo; event and proceeds to the drafting phase with the material accumulated so far. A degraded deliverable plus a human reviewer beats zero output after the budget is exhausted.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">flowchart&lt;/span> &lt;span class="n">TD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">A&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Call&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">with&lt;/span> &lt;span class="n">TOOLS&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span> &lt;span class="n">B&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">tool_calls&lt;/span> &lt;span class="n">returned&lt;/span>&lt;span class="err">?&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">B&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="n">none&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">N&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Inject&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">keep&lt;/span> &lt;span class="n">exploring&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">call&lt;/span> &lt;span class="n">finish_exploration&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span> &lt;span class="n">A&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">B&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="n">finish_exploration&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">W&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Record&lt;/span> &lt;span class="n">understanding&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">enter&lt;/span> &lt;span class="n">drafting&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">B&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="n">other&lt;/span> &lt;span class="n">tools&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">F&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">LoopBreaker&lt;/span> &lt;span class="n">fingerprint&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">F&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="n">ok&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">E&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Execute&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">feed&lt;/span> &lt;span class="n">result&lt;/span> &lt;span class="n">back&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span> &lt;span class="n">A&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">F&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="n">rd&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">once&lt;/span> &lt;span class="n">only&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">G&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Execute&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">inject&lt;/span> &lt;span class="n">wrap&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">up&lt;/span> &lt;span class="n">prompt&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span> &lt;span class="n">A&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">F&lt;/span> &lt;span class="o">--&amp;gt;|&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="n">th&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">|&lt;/span> &lt;span class="n">X&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">Hard&lt;/span> &lt;span class="k">break&lt;/span> &lt;span class="n">after&lt;/span> &lt;span class="n">this&lt;/span> &lt;span class="nb">round&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">--&amp;gt;&lt;/span> &lt;span class="n">W&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The whole loop still sits inside &lt;code>for _ in range(MAX_TOOL_ITERS)&lt;/code> — the two defenses hold independently. The breaker has an end-to-end test: a mock client that, whenever tools are passed, always returns the same &lt;code>read_file(a.txt)&lt;/code> call (simulating total collapse); the test asserts that the break event must appear and that the number of create calls is ≤ 6 — far below the cap of 20. The threshold sequence (first two ok, third nudge, fifth stop) and the fingerprint properties are pinned by separate unit tests.&lt;/p>
&lt;h2 id="mechanism-4-the-inverse-failure-in-drafting--tool-markup-without-tools">Mechanism 4: the inverse failure in drafting — tool markup without tools&lt;/h2>
&lt;p>After exploration, drafting begins, and this phase passes no tools. But the model may still emit its internal tool-call markup (DeepSeek&amp;rsquo;s DSML) as body text — the generation &amp;ldquo;succeeds&amp;rdquo; on the surface, and the resulting report is garbage. &lt;code>_complete_report&lt;/code> validates output with three deterministic criteria: contains &lt;code>\documentclass&lt;/code>, contains no &lt;code>DSML&lt;/code>, contains no &lt;code>tool_calls&lt;/code>. On failure it injects a reinforcement instruction (&amp;ldquo;You have no tools available right now… output the complete LaTeX source directly&amp;rdquo;) and retries, up to 3 times. This is the output-sanitizing approach of &lt;a href="https://kaguc.com/blog/output-sanitizing/">Part 3&lt;/a> extended into the agent context: a structurally invalid output is not patched textually — the constraints are tightened and the call is retried.&lt;/p>
&lt;h2 id="mechanism-5-deterministic-progress--pre-enumerated-milestones-and-explicit-stages">Mechanism 5: deterministic progress — pre-enumerated milestones and explicit stages&lt;/h2>
&lt;p>A streaming agent&amp;rsquo;s progress bar cannot run on guesswork. This pipeline is &amp;ldquo;process as configuration&amp;rdquo;: scan → explore → [figures] → [gap] → draft → reflect×N → bib → [compile], with the optional steps switched by the template. So &lt;code>_plan_pipeline&lt;/code> enumerates the milestone sequence before the run starts, &lt;strong>from configuration alone&lt;/strong>; M = len(plan), every milestone event carries index/total, and the progress bar has a fixed denominator from the first second. Two consistency details:&lt;/p>
&lt;ul>
&lt;li>The gates for optional steps must match the runtime decisions verbatim: &lt;code>has_xelatex&lt;/code> is probed exactly once, and the plan and the runtime compile gate reuse the same result, so the two can never diverge;&lt;/li>
&lt;li>the stage name is passed explicitly rather than looked up from &lt;code>_plan[_pi-1]&lt;/code>: the reflection loop may break early on convergence, and a skipped milestone would make the positional lookup mislabel the subsequent bib / compile milestones as reflect.&lt;/li>
&lt;/ul>
&lt;p>One known small deviation is documented honestly in the code comments: the gap step also depends at runtime on whether material exists and may actually be skipped — the plan counts it anyway, and the completion event backfills to 100%.&lt;/p>
&lt;h2 id="measurements">Measurements&lt;/h2>
&lt;p>Real-run validation (devlog record, DeepSeek v4-pro, an electromagnetics field-scan dataset): the agent ran &lt;code>list_dir&lt;/code> automatically and made multiple rounds of &lt;code>read_file&lt;/code> (a PDF, band.csv, scan parameter files, and more), and its autonomous identification of the experiment was correct; the draft was 16,773 characters, and after one reflection-revision round the final report was 20,742 characters across 9 sections, with the full run taking 745 seconds. That validation ran on the initial version whose tool set was only list_dir / read_file / finish_exploration; read_image and make_figure were added later.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mechanism&lt;/th>
&lt;th>Alternative&lt;/th>
&lt;th>Effect&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>_safe&lt;/code> path guard&lt;/td>
&lt;td>Trust model-supplied paths&lt;/td>
&lt;td>Escapes become tool-level errors the model can self-correct; nothing raises&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>scan_folder&lt;/code> pre-scan&lt;/td>
&lt;td>Let the model map the folder itself&lt;/td>
&lt;td>Zero-token bucketed snapshot of every file&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>MAX_TOOL_ITERS = 20&lt;/code>&lt;/td>
&lt;td>Unbounded loop&lt;/td>
&lt;td>Hard cost ceiling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>_LoopBreaker&lt;/code>&lt;/td>
&lt;td>Iteration cap / SDK retries alone&lt;/td>
&lt;td>≤ 6 create calls in the collapse scenario (end-to-end assertion), saving 14+ wasted rounds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DSML detection and retry&lt;/td>
&lt;td>Accept the &amp;ldquo;successful&amp;rdquo; output&lt;/td>
&lt;td>Valid LaTeX within at most 3 retries&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>_plan_pipeline&lt;/code> + explicit stage&lt;/td>
&lt;td>Guess progress from event counts&lt;/td>
&lt;td>Denominator known before the run; early convergence never mislabels stages&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="where-not-to-do-this">Where not to do this&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>An exact fingerprint catches total collapse, not wandering.&lt;/strong> Low-value calls to the same tool with varying arguments (reading irrelevant files one after another) never trip the breaker; that class is bounded by the budget cap and prompt quality. Fuzzing the fingerprint (ignoring some arguments) would widen coverage but misfire on legitimate continuation reads — we chose narrow and precise.&lt;/li>
&lt;li>&lt;strong>The nudge works only while the model still follows instructions.&lt;/strong> The wrap-up prompt helps with mild circling; once the context has collapsed severely, the model no longer responds to injections and only the hard break matters — which is exactly the rationale for the two-tier thresholds.&lt;/li>
&lt;li>&lt;strong>A degraded deliverable requires a human in the loop.&lt;/strong> A report written from partial material is only worth something if someone reviews it downstream. If the agent&amp;rsquo;s output is executed automatically and partial information can cause harm, the breaker policy should be fail-the-whole-run rather than degrade.&lt;/li>
&lt;li>&lt;strong>Pre-enumerated progress requires a fixed pipeline.&lt;/strong> &lt;code>_plan_pipeline&lt;/code> works because the pipeline structure is settled before the run starts; an open-ended agent whose plan is generated dynamically by the model has no a-priori milestone sequence, and progress degrades to event counting.&lt;/li>
&lt;li>&lt;strong>The thresholds are not universal constants.&lt;/strong> 20 / 3 / 5 were tuned to the scale of &amp;ldquo;read a folder, write a report&amp;rdquo; and have not been validated on other task shapes.&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>qwen-code issue #4695 — a public case of tool-loop collapse (43 repeated &lt;code>git status&lt;/code> calls / 8.9M tokens in one session).&lt;/li>
&lt;/ul></description></item><item><title>Multi-agent adversarial review as an engineering practice</title><link>https://kaguc.com/blog/adversarial-review/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/adversarial-review/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/adversarial-review-zh/">中文&lt;/a>. Part 13 of the Engineering LLM Applications series.&lt;/em>&lt;/p>
&lt;h2 id="the-problem-the-typical-failure-of-an-llm-finding-is-that-it-looks-real">The problem: the typical failure of an LLM finding is that it looks real&lt;/h2>
&lt;p>When you use an LLM to review code, volume is never the bottleneck — it will happily list fifty &amp;ldquo;potential issues&amp;rdquo; against any codebase. The bottleneck is at the other end: the typical failure mode of an LLM finding is &lt;strong>plausible-but-wrong&lt;/strong>. The line number is specific, the failure narrative is complete, the suggested fix is fully formed — and then it falls apart the moment you confront it with the code: the guard already exists, the alleged race path is unreachable, the &amp;ldquo;leaked&amp;rdquo; resource is reclaimed somewhere else.&lt;/p>
&lt;p>A review pipeline without a verification stage drowns in fake findings, and fake findings cost you both ways: either you falsify each one by hand, paying back every hour the review saved, or you start ignoring the report wholesale — and the real problems die with the fake ones. The second outcome is the more common and the more fatal: a report that mixes true and false findings carries close to zero engineering information.&lt;/p>
&lt;p>A separate, independent problem is breadth: a single-perspective review — whether a human or a single agent — cannot stay simultaneously alert across security, concurrency, billing, and cross-platform consistency.&lt;/p>
&lt;p>Both problems point at the same structural remedy: split &amp;ldquo;find problems&amp;rdquo; and &amp;ldquo;verify problems&amp;rdquo; into two stages with opposite objectives — the finding side optimizes for coverage, the verifying side optimizes for truth. This article walks through the structure, grounded in two production runs (one full-project audit, one session-level review) plus one set of overturn data.&lt;/p>
&lt;h2 id="the-structure-parallel-discovery-by-dimension-adversarial-verification-per-finding">The structure: parallel discovery by dimension, adversarial verification per finding&lt;/h2>
&lt;p>&lt;strong>Stage one: N auditors in parallel, each guarding exactly one dimension.&lt;/strong> The full-project audit used 6 (backend core, API-DB-security, LaTeX-verify-distill, frontend, test blind spots, documentation drift); the session-level review used 4 (concurrency/runs, security, frontend regression, data integrity). The point of slicing by dimension is not parallel speed-up but &lt;strong>forced perspective&lt;/strong>: an auditor that only looks at concurrency does not have its attention taxed by functional correctness.&lt;/p>
&lt;p>Findings obey a format discipline: every one must land as &lt;code>file:line&lt;/code> + a concrete failure scenario + a proposed fix. This is the precondition for everything downstream — it turns a finding from &amp;ldquo;something feels off&amp;rdquo; into a &lt;strong>falsifiable claim&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Stage two: each finding goes to an independent verifier.&lt;/strong> The verifier is not the agent that produced the finding and does not enter with that agent&amp;rsquo;s reasoning in context; its instructions are &lt;strong>default skepticism, specialize in falsification&lt;/strong> — go into the code and look for evidence that the finding does &lt;em>not&lt;/em> hold. The verdict is three-valued: CONFIRMED / uncertain / dismissed, and only CONFIRMED enters the repair queue.&lt;/p>
&lt;p>Independence is a requirement, not an option. When one agent checks its own work, its finding is sunk cost sitting in its context, and it will defend it; only when &amp;ldquo;successfully overturning the finding&amp;rdquo; is the verifier&amp;rsquo;s success criterion do the incentives align. This is the same issue as &lt;a href="https://kaguc.com/blog/judge-validity/">judge validity&lt;/a> from part 9 of this series, seen from the other side: the verifier is itself a judge, and its validity has to be demonstrated by the fact that it actually overturns things — data below.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">flowchart TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> A[Codebase / change set] --&amp;gt; B1[Auditor: security]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> A --&amp;gt; B2[Auditor: concurrency]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> A --&amp;gt; B3[Auditor: ... N dimensions total]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B1 --&amp;gt; F[Finding: file:line + failure scenario + fix]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B2 --&amp;gt; F
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> B3 --&amp;gt; F
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> F --&amp;gt; V{Independent verifier: default skepticism, falsify}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> V --&amp;gt;|CONFIRMED| T[Triage by severity]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> V --&amp;gt;|dismissed| X[Discard]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> V --&amp;gt;|uncertain| U[File as open question]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> T --&amp;gt; R[Fix, or defer explicitly with a recorded reason]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> R --&amp;gt; P[Regression tests confirm no breakage]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="two-measured-runs">Two measured runs&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Full-project audit&lt;/th>
&lt;th>Session-level review&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Scope&lt;/td>
&lt;td>Entire codebase (backend + frontend + docs + tests), focused on the broader/older parts&lt;/td>
&lt;td>One session&amp;rsquo;s changes: 44 files / 1,467 lines, including a core-path refactor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auditors&lt;/td>
&lt;td>6, by subsystem/dimension&lt;/td>
&lt;td>4, by risk dimension&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CONFIRMED&lt;/td>
&lt;td>27 (5 high / 14 medium / 8 low)&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>uncertain / dismissed&lt;/td>
&lt;td>0 / 2&lt;/td>
&lt;td>all others falsified&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Disposition&lt;/td>
&lt;td>24 fixed, 3 explicitly deferred&lt;/td>
&lt;td>7 fixed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Post-fix verification&lt;/td>
&lt;td>pytest 243 passed (+6 new tests), vite build clean&lt;/td>
&lt;td>pytest 18 passed + targeted live tests&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Beyond the counts, what deserves attention is the &lt;strong>type&lt;/strong> of problem on the confirmed list — a substantial share is the kind a single person reading the diff end-to-end would very likely miss:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TOCTOU race in the 409 concurrency guard&lt;/strong> (session-level): &amp;ldquo;check whether a run is already running&amp;rdquo; and &amp;ldquo;create the run&amp;rdquo; were not atomic, so concurrent requests could double-start. The fix moved the check, the creation, and the handle registration together inside &lt;code>runs._LOCK&lt;/code>; the repair was closed out empirically — two concurrent requests return &lt;code>[200, 409]&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Chat billing used the unresolved provider&lt;/strong> (session-level): with &lt;code>DEFAULT_PROVIDER=anthropic&lt;/code> and the request omitting the provider field, cost was computed at deepseek&amp;rsquo;s unit prices — roughly a 60× underestimate. Provider resolution lives in one place, pricing in another; reviewed module by module, both sides &amp;ldquo;look correct&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Anthropic streaming &lt;code>max_tokens=32000&lt;/code> while DeepSeek&amp;rsquo;s was 64000&lt;/strong> (full audit, medium): long reports silently truncated on one provider; only a cross-provider consistency comparison exposes it. Fixed by unifying on &lt;code>_STREAM_MAX_TOKENS=64000&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Figure sandbox environment hardcoded POSIX&lt;/strong> (full audit, high): on the Windows distribution (the primary platform), the CPython subprocess cannot start for lack of SystemRoot/TEMP, so &lt;code>make_figure&lt;/code> crashes unconditionally.&lt;/li>
&lt;li>&lt;strong>&lt;code>EXPOSE_RESET_TOKEN&lt;/code> defaulting to true&lt;/strong> (full audit, high): any anonymous caller could obtain a reset token and change a password — account takeover; changed to a secure default of false.&lt;/li>
&lt;/ul>
&lt;p>These respectively require concurrency-interleaving reasoning, cross-module dataflow tracing, cross-provider comparison, and cross-platform runtime knowledge — exactly the categories that one dedicated perspective per dimension covers and one sequential read-through misses.&lt;/p>
&lt;p>One disposition detail worth copying: the 3 deferrals are not silent omissions but &lt;strong>explicit decisions with recorded reasons&lt;/strong> — all are SaaS-only issues neutralized in the shipped STANDALONE configuration by &lt;code>_no_cloud&lt;/code>/forced-local, and fixing them outright would have meant disturbing a set of non-STANDALONE test semantics; they are filed for whenever SaaS is revived. The output of a review is not just fixes; it is also this traceable record of &lt;em>why not&lt;/em>.&lt;/p>
&lt;h2 id="verification-is-not-a-rubber-stamp-overturn-data">Verification is not a rubber stamp: overturn data&lt;/h2>
&lt;p>Whether the adversarial verification stage actually works needs evidence of its own. The same find-then-adversarially-recheck structure was applied to a different kind of object: auditing whether the writing rules produced by the &lt;a href="https://kaguc.com/blog/distill-judgment-experiment/">distillation experiment&lt;/a> (part 12 of this series) genuinely learned &amp;ldquo;judgment&amp;rdquo; rather than reskinning structural templates. Eleven agents in total: for each rule pair, an item-by-item initial classification, followed by an adversarial re-review whose instructions were, again, default skepticism — specializing in items whose &amp;ldquo;because&amp;rdquo; is a tautology or whose structural action carries only a thin veneer of justification.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pair&lt;/th>
&lt;th>Initial judgment share&lt;/th>
&lt;th>After adversarial re-review&lt;/th>
&lt;th>Pair verdict&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>87%&lt;/td>
&lt;td>80%&lt;/td>
&lt;td>holds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>82%&lt;/td>
&lt;td>73%&lt;/td>
&lt;td>holds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>73%&lt;/td>
&lt;td>67%&lt;/td>
&lt;td>holds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>82%&lt;/td>
&lt;td>64%&lt;/td>
&lt;td>holds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>100%&lt;/td>
&lt;td>90%&lt;/td>
&lt;td>holds&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The initial numbers of &lt;strong>all five pairs were revised downward&lt;/strong>; the post-adversarial mean is 0.746, and the overturned items follow one highly consistent pattern — the action overlaps a structural template and the stated reason is close to a tautology. The pair-level verdicts held 5/5, but no pair emerged untouched. The original report also flags, unprompted, that pair 5&amp;rsquo;s 0.9 is optimistic (the initial 1.0 was itself suspicious).&lt;/p>
&lt;p>This data establishes two things. First, the verifier really does overturn items case by case — it is not process decoration. Second, the value of the adversarial stance runs in both directions: in code review it kills false positives (2 dismissed; &amp;ldquo;all others falsified&amp;rdquo; at the session level), and in evaluation tasks it deflates optimistic self-assessment (100% → 90%).&lt;/p>
&lt;h2 id="cost-and-applicability">Cost and applicability&lt;/h2>
&lt;p>The cost structure is straightforward: every finding pays for one additional dedicated verification pass, plus orchestration overhead. When it is worth it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Large change surfaces.&lt;/strong> The session-level run covered 44 files / 1,467 lines including a core-path refactor (&amp;ldquo;merging chat into run&amp;rdquo;) — a surface beyond what one person can hold in a single pass.&lt;/li>
&lt;li>&lt;strong>Pre-release / security surfaces.&lt;/strong> Problems like &lt;code>EXPOSE_RESET_TOKEN&lt;/code> or the Stripe webhook skipping validation when no secret is configured (fixed to fail closed: 503 without a secret) have a miss cost wildly out of proportion to the verification cost.&lt;/li>
&lt;li>&lt;strong>Legacy code never systematically audited.&lt;/strong> The full-project audit&amp;rsquo;s stated goal was precisely &amp;ldquo;not just the new code — focus on the broader/older parts&amp;rdquo;, code outside the field of view of any recent review.&lt;/li>
&lt;/ul>
&lt;p>When it is not worth it, the source record already contains the judgment: single-feature implementation is &amp;ldquo;sequential and interleaved, with no fan-out&amp;rdquo; — solo work suffices; review and audit (breadth plus independent verification) are what the multi-agent workflow is actually good at. Extrapolating to small changes: on a few dozen lines of diff, falsifying three findings by hand is far faster than orchestrating a six-auditor pipeline, and the verification overhead simply exceeds the benefit.&lt;/p>
&lt;h2 id="limits">Limits&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>The shared-origin blind spot.&lt;/strong> The verifier and the author of the code under audit are the same (or a sibling) LLM. The adversarial structure removes positional bias — no one defends their own output anymore — but it does not extend the capability boundary: a category of problem invisible to both sides stays invisible to the verification stage too. It is no substitute for signals of a different origin — actually running the code, live concurrency tests, human domain knowledge.&lt;/li>
&lt;li>&lt;strong>&amp;ldquo;0 uncertain&amp;rdquo; does not mean &amp;ldquo;0 escaped&amp;rdquo;.&lt;/strong> 27 CONFIRMED with 0 uncertain measures the precision side: nearly everything raised was real. It says nothing about the recall side — the problems no auditor raised at all. This very audit confirmed three test blind spots: an audit can find the holes in the tests, but there is no instrument measuring the holes in the audit.&lt;/li>
&lt;li>&lt;strong>Verification depth is itself sampled.&lt;/strong> The overturn experiment&amp;rsquo;s report states honestly that only pair 4&amp;rsquo;s source file was empirically spot-checked; the rest relies on sub-agent labeling. Verification can be shallow, and a CONFIRMED stamped by shallow verification deserves a discount — mistaking &amp;ldquo;verification happened&amp;rdquo; for &amp;ldquo;verification was thorough&amp;rdquo; is this pipeline&amp;rsquo;s most seductive self-deception.&lt;/li>
&lt;li>&lt;strong>The structure depends on finding discipline.&lt;/strong> &lt;code>file:line&lt;/code> + failure scenario + fix is what makes a finding a falsifiable claim; relax it to &amp;ldquo;module X may have issues&amp;rdquo; and the verifier has nothing to confront — the whole pipeline degenerates into two rounds of opinion exchange.&lt;/li>
&lt;/ol></description></item><item><title>Wiring external signals into the loop: compile-and-repair</title><link>https://kaguc.com/blog/compile-self-repair/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://kaguc.com/blog/compile-self-repair/</guid><description>&lt;p>&lt;em>Also in &lt;a href="https://kaguc.com/blog/compile-self-repair-zh/">中文&lt;/a>. Part 6 of the Engineering LLM Applications series.&lt;/em>&lt;/p>
&lt;h2 id="the-problem-models-cannot-fix-errors-they-cannot-see">The problem: models cannot fix errors they cannot see&lt;/h2>
&lt;p>Scenario: an autonomous agent generates a LaTeX lab report, and xelatex fails. Who fixes it?&lt;/p>
&lt;p>The reflex is to ask the model to &amp;ldquo;check it again.&amp;rdquo; The evidence against that path is specific. One of Self-Refine&amp;rsquo;s conclusions (arXiv:2303.17651) is that &lt;strong>on tasks where errors are hard to self-detect, pure self-reflection is nearly useless, and gains only return once an external verification signal is wired in&lt;/strong>. The systematic review in &lt;em>When Can LLMs Actually Correct Their Own Mistakes&lt;/em> (TACL 2024) is blunter: prompting-only self-critique does not improve — and can degrade — arithmetic and code tasks, and the flattering self-correction numbers in the literature mostly leaned on an oracle (stopping only when the known-correct answer appears). Compile errors are the canonical hard-to-self-detect case: the model rereads the &lt;code>\def&lt;/code> line it just wrote and cannot see that a &lt;code>%&lt;/code> inside the braces comments out the closing brace — but xelatex sees it, and its error report cannot be faked.&lt;/p>
&lt;p>So the right question is not &amp;ldquo;how do we make the model self-check&amp;rdquo; but &amp;ldquo;how do we wire an objective external verifier into the generation loop.&amp;rdquo; This article dissects the compile-and-repair loop we shipped in an AI writing agent — &lt;strong>really compile after generation; on failure, extract the errors and feed them back for a repair round&lt;/strong> — and the engineering constraints that keep this deceptively simple loop under control in production.&lt;/p>
&lt;h2 id="from-patching-to-really-compiling">From patching to really compiling&lt;/h2>
&lt;p>Before the compiler was wired in, the system already had a deterministic sanitizer layer (&lt;a href="https://kaguc.com/blog/output-sanitizing/">part 3 of this series&lt;/a>): &lt;code>\mathbf&lt;/code>→&lt;code>\symbf&lt;/code>, &lt;code>%&lt;/code> comments inside &lt;code>\def&lt;/code> lines, backfilling template variables the model forgot to set — the known pitfalls that rules can encode. But patches cannot enumerate the long tail. The devlog states the goal of this step plainly: &amp;ldquo;not patching with the sanitizer, but having the agent really compile after generating the report, feed the xelatex errors back for a round of its own fixes on failure, and push the first-pass success rate to the max.&amp;rdquo;&lt;/p>
&lt;p>That fixed the division of labor: &lt;strong>rules handle the known pitfalls (zero tokens); the compiler backstops the long tail (one LLM call per round)&lt;/strong>. In practice the sanitizer has already cured the common traps, so the first compile check usually passes outright; the repair branch only catches what remains. The loop itself is three steps:&lt;/p>
&lt;ol>
&lt;li>&lt;code>compile_tex&lt;/code> — assemble a complete working directory (article.tex + cls + bib + figures), run xelatex → bibtex → xelatex ×2, return &lt;code>{ok, pdf_path, log}&lt;/code>;&lt;/li>
&lt;li>&lt;code>extract_errors&lt;/code> — pull from the log the lines starting with &lt;code>!&lt;/code>, the &lt;code>l.&lt;/code> line-number lines, and Runaway / Emergency stop lines, capped at 1800 characters — never feed back the whole log;&lt;/li>
&lt;li>on failure, feed the error excerpt back to the model, demand the full revised source, at most 2 rounds.&lt;/li>
&lt;/ol>
&lt;p>The cap of 2 is not arbitrary: a survey finding (&lt;em>How Many Tries&lt;/em>, arXiv:2604.10508) across 7 models is that the first two rounds capture 76–95% of the achievable gain, with near-zero return from round three on; Self-Refine likewise plateaus by round 3. Extra rounds only multiply wall-clock and token spend. One further detail: the second round&amp;rsquo;s repair output is not re-verified inside the loop — the repaired draft is kept, and final verification is left to the user-side manual compile/export path.&lt;/p>
&lt;p>A counter-intuitive design point: the repair is a &lt;strong>full-document rewrite&lt;/strong>, not a scoped patch of the kind used in part 7. The reason is a property of LaTeX errors — they are often decoupled from the root cause, and line numbers are unreliable (&lt;em>LaTeX Compilation Challenges&lt;/em>, arXiv:2603.02873), so pinpoint fixes at the reported line easily patch the wrong place. Scoped editing presupposes deterministic targeting, and compile errors are precisely where that presupposition fails.&lt;/p>
&lt;h2 id="termination-fingerprint-round-cap-wall-clock">Termination: fingerprint, round cap, wall clock&lt;/h2>
&lt;p>The biggest production risk of a self-repair loop is not failing to fix — it is &lt;strong>failing to stop&lt;/strong>. The consensus on loop governance is that &amp;ldquo;when to stop&amp;rdquo; must not be left to the model&amp;rsquo;s discretion; an outer controller enforces it with deterministic criteria. There are three gates:&lt;/p>
&lt;p>&lt;strong>The no-progress gate.&lt;/strong> After each failed round, fingerprint the errors — the first 120 characters of the first &lt;code>!&lt;/code> error line (LaTeX&amp;rsquo;s anchor line); if it equals the previous round&amp;rsquo;s fingerprint, declare no progress, stop immediately, keep the current draft:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_error_fingerprint&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">errs&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Compile-error fingerprint: first &amp;#39;! …&amp;#39; error line (LaTeX&amp;#39;s anchor),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> else the first 120 chars. Used for no-progress detection.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">line&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">errs&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">splitlines&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">s&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">line&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">startswith&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;!&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">s&lt;/span>&lt;span class="p">[:&lt;/span>&lt;span class="mi">120&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">errs&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">strip&lt;/span>&lt;span class="p">()[:&lt;/span>&lt;span class="mi">120&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If the same error survived a repair round, this model has no answer for this error; feeding it the same report again will most likely produce the same failure, and burning more tokens has no expected return.&lt;/p>
&lt;p>&lt;strong>The round cap.&lt;/strong> &lt;code>for attempt in range(2)&lt;/code> — a hard limit.&lt;/p>
&lt;p>&lt;strong>The wall-clock gate.&lt;/strong> A single compile runs up to 4 subprocess passes at 180 seconds each — a token budget cannot contain CPU time. &lt;code>COMPILE_WALL_BUDGET&lt;/code> (default 600 seconds) is checked before each round; on expiry, stop and keep the current draft. The config comment states the motive: prevent multi-round self-repair from hogging the customer machine&amp;rsquo;s CPU.&lt;/p>
&lt;p>All three gates are deterministic criteria executed on the Python side; the model has no say in whether the loop stops — the same stance as the tool-loop circuit breaker in &lt;a href="https://kaguc.com/blog/agent-loop/">part 4&lt;/a>.&lt;/p>
&lt;h2 id="minimal-repair-context-8k-tokens-not-4060k">Minimal repair context: ~8k tokens, not 40–60k&lt;/h2>
&lt;p>How the repair call&amp;rsquo;s context is constructed is the single most money-saving decision in this loop. The intuitive approach is to reuse the agent&amp;rsquo;s full exploration history — surely the model &amp;ldquo;fixes better knowing more&amp;rdquo;? The opposite holds. The judgment recorded in the code comment: &lt;strong>95% of compile errors are template-level syntax problems&lt;/strong> (balancing, command misuse, template variables), and fixing them requires exactly three things:&lt;/p>
&lt;ul>
&lt;li>the full current draft;&lt;/li>
&lt;li>the error excerpt (≤1800 characters);&lt;/li>
&lt;li>the formatting hard rules (no &lt;code>%&lt;/code> comments inside &lt;code>\def&lt;/code> braces; bold vectors with &lt;code>\symbf&lt;/code>; no bold inside accents; balance braces/environments/&lt;code>$&lt;/code>; end with a top-level &lt;code>\end{document}&lt;/code>).&lt;/li>
&lt;/ul>
&lt;p>About 8k tokens in total. Reusing the full exploration messages would carry 40–60k tokens of dead weight — source material, tool-call records, multiple draft versions — none of it contributing to fixing one brace; switching to a fresh minimal context saves 30–50k tokens per repair round. The system prompt narrows the mandate to a single sentence:&lt;/p>
&lt;blockquote>
&lt;p>You are a LaTeX compile-repair assistant: fix compile errors only — do not rewrite content, do not add or remove sections, do not touch data.&lt;/p>
&lt;/blockquote>
&lt;p>This is not only about cost: the measurements in &lt;em>Revisit Self-Debugging&lt;/em> (arXiv:2501.12793) show label-only feedback beating verbose detailed feedback — inaccurate long context introduces ambiguity and lowers scores. Commercial products are isomorphic: Overleaf&amp;rsquo;s commercial Error Assist also deliberately minimizes the repair context, sending only the full error, the relevant code lines, and a file-name list (product-page information, not independently verified).&lt;/p>
&lt;h2 id="concurrency-isolation-a-compile-directory-per-session">Concurrency isolation: a compile directory per session&lt;/h2>
&lt;p>A plain constraint you only learn by tripping on it: the compile working directory must be isolated per session. &lt;code>compile_tex&lt;/code> writes article.tex into the directory, copies the cls and figures, and rmtree-rebuilds the figure subdirectory during assembly — two concurrent generation runs sharing one directory will overwrite each other&amp;rsquo;s source, delete each other&amp;rsquo;s figures, and pollute each other&amp;rsquo;s logs. After partitioning the repair directory as &lt;code>_repair/&amp;lt;session_id&amp;gt;&lt;/code>, each run writes its own. The lesson generalizes: &lt;strong>a verifier is a side-effecting process, not a pure function&lt;/strong> — when wiring an external verifier into a loop, isolation is a default, not an optimization.&lt;/p>
&lt;h2 id="the-whole-picture-quantified">The whole picture, quantified&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">flowchart TD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> A[Final draft] --&amp;gt; W{Wall-clock budget exceeded?}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> W --&amp;gt;|yes| S[Stop, keep current draft]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> W --&amp;gt;|no| C[compile_tex: real compile]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> C --&amp;gt;|ok| P[PDF produced]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> C --&amp;gt;|fail| E[extract_errors: pull ! error lines]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> E --&amp;gt; F{Fingerprint same as last round?}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> F --&amp;gt;|same = no progress| S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> F --&amp;gt;|different| R[Minimal-context repair call ≈8k tokens]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> R --&amp;gt; N[2 rounds used?]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> N --&amp;gt;|yes| S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> N --&amp;gt;|no| W
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Constraint&lt;/th>
&lt;th>Failure mode without it&lt;/th>
&lt;th>Implementation and numbers&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Round cap&lt;/td>
&lt;td>Grinding on for rounds with near-zero return from round 3&lt;/td>
&lt;td>K=2; first two rounds capture 76–95% of achievable gain (survey figure)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No-progress gate&lt;/td>
&lt;td>The same error fed back again and again, spinning in place&lt;/td>
&lt;td>Fingerprint = first 120 chars of the first &lt;code>!&lt;/code> line; stop on consecutive match&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wall-clock gate&lt;/td>
&lt;td>4 passes × 180s per compile; a token cap cannot contain CPU&lt;/td>
&lt;td>&lt;code>COMPILE_WALL_BUDGET&lt;/code>, default 600s; on expiry keep the current draft&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Minimal context&lt;/td>
&lt;td>40–60k tokens of exploration-history dead weight per call&lt;/td>
&lt;td>Current draft + error excerpt + hard rules ≈8k; saves 30–50k tokens per round&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Session isolation&lt;/td>
&lt;td>Concurrent runs clobber each other&amp;rsquo;s source/figures/logs&lt;/td>
&lt;td>Dedicated &lt;code>_repair/&amp;lt;session_id&amp;gt;&lt;/code> working directory&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="where-not-to-build-this-loop">Where not to build this loop&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>It presupposes a cheap, objective, unfakeable verifier.&lt;/strong> Compilers, type checkers, and unit tests qualify; &amp;ldquo;is the writing good&amp;rdquo; does not. The stop condition must anchor on an objective signal like &amp;ldquo;compilation succeeded&amp;rdquo; — never on the model&amp;rsquo;s self-assessment. Quality dimensions without a verifier belong to evaluation and judges (parts 8 and 9), not to this loop.&lt;/li>
&lt;li>&lt;strong>Deterministic repair comes first.&lt;/strong> Pitfalls that can be written as rules (known syntax traps, missing-figure placeholders, variable backfill) should be cured by the zero-token sanitizer before any LLM repair; the repair loop only backstops the long tail that rules cannot enumerate. Reversing the order means using a probabilistic component for deterministic work: paying a call every time while adding new uncertainty.&lt;/li>
&lt;li>&lt;strong>Weak models need a non-LLM exit.&lt;/strong> Survey data puts weak models&amp;rsquo; repair rate on deadlocked errors at only about 30% (not independently verified). The loop must ship with a fallback that does not depend on the model — here, export_zip produces a compilable package to hand to Overleaf or a human. Without an exit, the loop degenerates on weak models into &amp;ldquo;burn the budget, then give up.&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>When the verifier is expensive, the budget gate precedes the round cap.&lt;/strong> Here a single compile is wall-clock-bounded at 4 passes × 180 seconds (at most 720s), so K=2 is affordable; if the verifier is a ten-minute integration test suite, the same K=2 may be unacceptable — the wall-clock budget must become the first gate, including the option of &amp;ldquo;give up without repairing at all.&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;em>Self-Refine: Iterative Refinement with Self-Feedback&lt;/em> (arXiv:2303.17651) — self-reflection is nearly useless on tasks where errors are hard to self-detect; gains return with external signals; benefit concentrates in the first 1–2 rounds (its &amp;ldquo;+20% average&amp;rdquo; figure not independently verified).&lt;/li>
&lt;li>&lt;em>When Can LLMs Actually Correct Their Own Mistakes&lt;/em> (TACL 2024) — prompting-only self-critique does not improve, and can degrade, arithmetic/code tasks; flattering self-correction results mostly relied on an oracle.&lt;/li>
&lt;li>&lt;em>How Many Tries&lt;/em> (arXiv:2604.10508) — the first two rounds capture 76–95% of achievable gain; near zero from round 3.&lt;/li>
&lt;li>&lt;em>LaTeX Compilation Challenges&lt;/em> (arXiv:2603.02873) — LaTeX errors are often decoupled from root causes; line numbers are unreliable.&lt;/li>
&lt;li>&lt;em>Revisit Self-Debugging&lt;/em> (arXiv:2501.12793) — label-only feedback beats verbose feedback.&lt;/li>
&lt;li>Overleaf Error Assist — the commercial isomorph of minimal repair context (product-page information, not independently verified).&lt;/li>
&lt;/ul></description></item></channel></rss>