Reading the Source: How a Single File Read Uncovered the Root Cause of Agent Over-Provisioning
In the middle of a complex debugging session spanning autonomous fleet management, production crashes, and LLM-driven decision-making, there is a message that appears deceptively simple. Message [msg 4491] contains nothing more than a single read tool call — the assistant opens a Go source file and examines a specific block of code. There is no grand pronouncement, no triumphant fix, no verbose reasoning block. Just lines 645 through 661 of /tmp/czk/cmd/vast-manager/agent_api.go. Yet within those seventeen lines lies the entire explanation for why an autonomous agent had been over-provisioning GPU instances, burning through budget, and wasting LLM iterations on futile retries. This article examines that single message in depth: why it was written, what it reveals, and how it exemplifies the kind of focused, evidence-driven debugging that characterizes effective systems engineering.
The Context: An Autonomous Agent Gone Wrong
The broader conversation leading up to this message tells a story of rapid iteration and operational discovery. The assistant had built a fully autonomous LLM-driven fleet management agent for a distributed GPU proving infrastructure running on vast.ai. The agent's job was to monitor Curio SNARK demand and scale the fleet up or down accordingly — launching new GPU instances when proofs were queued, shutting them down when demand subsided. The system had evolved through multiple iterations: demand sensing, performance tracking, rate-limit awareness, and a sophisticated context management system with SQLite-backed conversation memory.
But by message [msg 4486], the user had noticed something was off. They asked the assistant to "look at how the agent is doing, if what it's doing makes sense." What followed was a sobering reality check. The assistant checked the agent logs and fleet status in messages [msg 4487] and [msg 4488], and discovered a clear pattern of over-provisioning. The agent had launched eight instances total. Nine vast instances existed on the platform. But only one was actually running and producing proofs at 40 proofs per hour. Six were still loading — bootstrapping, downloading models, syncing data — and would not be ready for another 1-2 hours. The total spend was $3.32 per hour, with most of that money going to machines that were not yet contributing any proving capacity.
The root cause was subtle but devastating. The agent's decision-making was driven by a fleet summary that reported CapacityProofsHr — the total proofs-per-hour capacity of all running instances. With only one machine running, that number was 40 p/h. The target was 500 p/h. The agent saw a gap of 460 p/h and kept launching more instances to fill it, completely ignoring the six machines already in the loading pipeline that would collectively add roughly 300 p/h once they came online. It was like a factory manager who sees empty production lines and hires more workers, ignoring the fact that a busload of workers is already pulling into the parking lot.## What the Message Actually Contains
The subject message itself is remarkably sparse. It reads:
[assistant] [read] /tmp/czk/cmd/vast-manager/agent_api.go
<path>/tmp/czk/cmd/vast-manager/agent_api.go</path>
<type>file</type>
<content>645:
646: var totals FleetTotals
647: for _, inst := range instances {
648: totals.Total++
649: switch inst.State {
650: case "running":
651: totals.Running++
652: totals.CapacityProofsHr += inst.BenchRate
653: totals.TotalDPH += inst.DPH
654: case "registered", "params_done":
655: totals.Loading++
656: case "bench_done":
657: totals.Benchmarking++
658: }
659: }
660:
661: budget := Bud...
This is a read tool call — the assistant is reading a specific region of a Go source file. The file is agent_api.go, which contains the API handlers for the fleet management agent. The region shown is the loop that computes fleet totals: it iterates over all instances, categorizes them by state, and accumulates CapacityProofsHr (sum of BenchRate for running instances), TotalDPH (dollars per hour), and counts for running, loading, and benchmarking instances.
The critical detail is on line 652: totals.CapacityProofsHr += inst.BenchRate. This line only adds capacity for instances in the "running" state. Instances in "registered", "params_done", or "bench_done" states — which represent machines that are still bootstrapping but will soon be productive — are counted but their future capacity is never projected. The fleet summary that the LLM agent sees reports CapacityProofsHr as 40 p/h (from the single running instance), even though the six loading instances represent roughly 300 p/h of incoming capacity.
The Reasoning Behind the Read
Why did the assistant issue this particular read at this moment? The sequence of events reveals the reasoning. In message [msg 4489], immediately before this read, the assistant had just finished analyzing the agent's behavior and identified the core problem:
"The core issue is that the agent doesn't account for loading instances as future capacity — it sees the 40 p/h gap and keeps launching more, when those 6 loading instances will eventually provide ~300 p/h."
The assistant then stated its plan explicitly:
"I need to make this calculation explicit by adding a projected_capacity_proofs_hr field to the fleet response that combines running instance rates with estimated rates for loading instances, using average benchmarks or defaults."
But before implementing this fix, the assistant did something crucial: it read the actual source code to verify its hypothesis. This is the mark of a disciplined engineer. Rather than assuming the bug was in the agent's Python prompt logic (which would have been the easier, surface-level diagnosis), the assistant traced the problem upstream to the data pipeline. The LLM agent could only make decisions based on the information it was given. If the fleet summary it received was incomplete — showing only current capacity rather than projected capacity — then no amount of prompt engineering would fix the over-provisioning. The fix had to start at the source: the Go API that produced the fleet summary.
The read tool call in message [msg 4491] is thus an act of grounding. The assistant had formed a theory about the root cause, and it was now verifying that theory against the actual code. The region it chose to read — lines 645-661 — is precisely the section where fleet totals are computed. This was not a random scan; it was a targeted investigation of the specific data structure that fed the agent's decision-making.
Assumptions and Input Knowledge
To understand this message, one must understand several layers of context. First, the architecture: the fleet management system consists of a Go backend (vast-manager) that exposes REST APIs, and a Python agent (vast_agent.py) that calls these APIs and uses an LLM to make scaling decisions. The Go backend is the authoritative source of fleet state — it queries vast.ai's API, maintains a local SQLite database of instances, and computes summary statistics. The Python agent receives this summary and passes it to the LLM as part of its prompt.
Second, one must understand the instance lifecycle on vast.ai. When a new instance is launched, it goes through several states: registered (just created), params_done (parameters configured), bench_done (benchmarking complete), and finally running (actively proving). The loading process can take 1-2 hours due to model downloads, data synchronization, and configuration. Instances in registered or params_done are not yet contributing proofs, but they will contribute once they finish bootstrapping.
The key assumption embedded in the original code was that CapacityProofsHr should only reflect currently productive capacity. This is a reasonable assumption for many monitoring contexts — you want to know what your fleet is actually producing right now. But for an autonomous scaling agent making provisioning decisions, this assumption is dangerously incomplete. The agent needs to know not just what the fleet is doing now, but what it will be doing in the near future, because launching new instances takes 1-2 hours to become productive. If the agent doesn't account for incoming capacity, it will consistently over-provision.
There was also an implicit assumption in the prompt design: that the LLM would be smart enough to mentally add up the projected capacity from loading instances. The fleet summary textually mentioned "6 loading (will add capacity in 1-2h)", but this natural-language hint was insufficient. The LLM, faced with a numeric target of 500 p/h and a numeric current capacity of 40 p/h, consistently chose to launch more instances rather than waiting. The numbers spoke louder than the text.## Output Knowledge: The Fix That Followed
The read in message [msg 4491] did not itself produce a fix, but it created the knowledge necessary for the fix. By confirming that CapacityProofsHr was computed exclusively from running instances, the assistant established the precise location and nature of the change needed. The subsequent edit (in the following message, [msg 4492]) added a ProjectedCapacityProofsHr field to the fleet totals, computed as:
projectedCapacity := totals.CapacityProofsHr + float64(totals.Loading)*avgBenchRate + float64(totals.Benchmarking)*avgBenchRate
This single change transformed the agent's decision-making. Instead of seeing "40 p/h capacity, target 500 p/h, gap 460 p/h," the agent would see something like "current 40 p/h, projected 340 p/h (including 6 loading instances), target 500 p/h, gap 160 p/h." The difference is dramatic: a gap of 160 p/h might justify launching one more instance, whereas a gap of 460 p/h had been triggering a cascade of launches.
The output knowledge created by this message is thus twofold. First, it produced a precise, verified understanding of a specific code path — the fleet totals computation in agent_api.go. Second, it established the causal chain: the code structure → the incomplete data → the LLM's flawed decision → the over-provisioning. This causal chain became the blueprint for the fix.
The Thinking Process: Evidence Before Action
What is most striking about message [msg 4491] is what it doesn't contain. There is no reasoning block, no explicit analysis, no "I think the problem is X, let me check." The assistant does not narrate its thought process. Yet the thinking is visible in the action itself — in the precise selection of what to read.
The assistant could have taken several alternative approaches. It could have jumped straight to editing the Python agent prompt, adding more explicit instructions about loading instances. It could have added a quick band-aid in the fleet summary text. It could have asked the user for clarification. Instead, it chose to read the Go source — the foundational data layer. This choice reveals a particular engineering philosophy: fix the data, not the interpretation. If the fleet summary is incomplete, the LLM will make bad decisions regardless of how well the prompt is written. By fixing the data at the source, the assistant ensures that any downstream consumer — whether an LLM agent or a human operator — has the information needed to make sound judgments.
The read also reveals a sophisticated understanding of where the bottleneck actually was. The agent's LLM calls cost money and take time. Each wasted iteration — each attempt to launch an instance that gets rate-limited — consumes tokens and delays real work. The assistant recognized that the root cause wasn't the LLM's reasoning capability but the information it was fed. This is a crucial insight for anyone building LLM-powered autonomous systems: the quality of the agent's decisions is bounded by the quality of the data it receives. No amount of prompt engineering can compensate for missing information.
Broader Implications
This message, for all its brevity, encapsulates several important lessons about building reliable autonomous systems. The first is the importance of grounding LLM decisions in accurate, forward-looking data. An agent that only sees the present will make decisions optimized for the present, which may be catastrophically wrong for the future. The second lesson is about tracing the causal chain before fixing. The assistant could have blamed the LLM, rewritten the prompt, or added hard-coded rules. Instead, it traced the problem upstream to the API that produced the agent's input data. This upstream-first debugging approach is far more efficient than patching symptoms downstream.
The third lesson is about the value of reading code as a diagnostic tool. In an era of AI-assisted development, it is tempting to let the AI generate and modify code without deeply understanding it. But message [msg 4491] shows the assistant doing something that looks almost old-fashioned: reading a source file to verify a hypothesis. The read tool is not glamorous. It produces no output visible to the user beyond a code snippet. Yet it is one of the most powerful tools in the assistant's repertoire, because it transforms speculation into knowledge.
Conclusion
Message [msg 4491] is a quiet moment of diagnostic clarity in a noisy conversation about autonomous agents, production crashes, and fleet management. It contains no fireworks, no dramatic revelations. Just a Go source file, a loop over instances, and a single line that adds bench rates to capacity only for running machines. But in that single line, the entire story of over-provisioning is visible. The assistant read that line, understood its implications, and used that understanding to design a fix that would permanently solve the problem.
The message exemplifies a principle that applies far beyond this specific context: when an autonomous system makes consistently bad decisions, do not ask "how do I make the AI smarter?" Ask instead: "what information is the AI missing?" The answer, more often than not, lies not in the prompt but in the data pipeline that feeds it. And finding that answer requires the oldest debugging technique in the book: reading the code.