Commit 1 of 9v1-not-an-agent
An LLM Is Not an Agent
An agent is a model, a set of actions, and a loop. Take the loop away and what is left is a chatbot.

Everyone calls things agents now. A prompt with a system message is an agent. A chatbot with a retrieval step is an agent. The word has been stretched until it means “LLM I am excited about.”
Here is the definition I am building this series on:
An agent is an LLM that can take actions in a loop until a task is complete.
Three parts, and all three are load-bearing. A model that reasons. Actions it can actually take. And a loop that keeps going until the work is done rather than until the response is finished.
Drop any one of them and you have something else. Drop the loop specifically, and you have a chatbot — which is exactly what this first commit is.
What I built
Thirty-three lines of agent across two files, comments and blank lines included, plus a CLI that prints the result and writes the trace. One of the two files holds a model name and a two-sentence system instruction. The other makes a single call and returns the text.
export async function runOnce(task: string): Promise<RunResult> {
const result = await generateText({
model: openai(MODEL_NAME),
instructions: INSTRUCTIONS,
prompt: task,
})
return {
text: result.text,
model: MODEL_NAME,
totalTokens: result.usage?.totalTokens ?? 0,
}
}
No tools. No message history. No loop. The whole system instruction is “You are a helpful assistant running inside a developer terminal. Answer concisely.”
I am starting here on purpose. Everything the next eight commits add is easier to argue for once you have watched this version fail.
What broke
I asked it to count the TypeScript files in this project. Step through the run — it is two frames long, and the second one is the whole post.
Recorded run
Count how many TypeScript files are in this project and tell me the number.
Recorded from gpt-5-mini at commit v1-not-an-agent. The run did not finish the task.
Frame 1 of 2
0 tokens in context, against this run's peak of 245.
You
Count how many TypeScript files are in this project and tell me the number.
I expected a shell command delivered with false confidence. That is the failure I had drafted this section around before I recorded anything, and it is not what happened. What came back is stranger and more useful.
It opens with this:
I can’t directly count files from the image alone.
There is no image. Nothing was uploaded and nothing was attached. The prompt is the single sentence in frame one, sent by a command-line program with no concept of a file upload. The model reached for the most common way a question like that arrives — a screenshot pasted into a chat window — and described the situation it was probably in rather than the one it was actually in. It has no way to check, so it guessed, and it guessed a modality this program does not have.
What it asks for next follows from that. First, for the answer to be handed to
it: paste a listing, or give it the repository, or send a screenshot. Then,
failing all three, commands I could run myself — find for Linux and macOS,
Get-ChildItem for Windows PowerShell. Two operating systems, not because it
hedges out of habit, but because it cannot see which machine it is on. Every
fact needed to answer the question — the platform, the filesystem, the list of
files — sits on the other side of a wall with no door in it.
And then the last line, which is the one I keep coming back to:
I’ll tell you the number once you provide the output.
There is no once. runOnce returns, the CLI prints the text, writes the
trace, and the process exits. The model had just proposed a protocol — you run
a command, you paste the result back, I read it and answer — in which every
step after the first is impossible in the program it was running inside. It
described one turn of a loop and assumed a second turn would exist. Then it
asked me to be the loop.
So the reply is two things at once, and it is worth keeping them apart. One is simply wrong: there was no image, and a person on the other end of that message would now be working out which image it meant. The other looks right — the right commands, for the right platforms, in the right order — and when I first wrote this section I called it a correct plan and left it there. Then, writing the short version of this post for somewhere else, I ran it.
$ find . -type f -name "*.ts" | wc -l
4697
4,660 of those are inside node_modules. They are TypeScript that arrived with
the dependencies, not TypeScript anybody here wrote. The question was how many
TypeScript files are in this project, and the command answers it two orders of
magnitude high. The number I was actually after comes from a different
question:
$ git ls-files '*.ts' '*.tsx' | wc -l
37
So the plan was not correct either. It was correctly shaped — find is the
right tool and that is close to how you reach for it — and quietly wrong, which
is the worse kind, because a wrong number does not look like anything. It
arrives formatted exactly like a right one. The failure I had drafted this
section around did turn up in the end. It was one layer down from where I was
looking for it.
Both of those figures drift, which is its own small lesson. find counts
whatever is installed, so at the v1-not-an-agent tag, freshly installed, the
same command returns 4,683, of which 4,650 are dependencies; the source count
there is 35 rather than 37, because this branch has kept moving since the
commit the post is about. A number that changes depending on when you asked is
exactly the sort of number that deserves a second look.
Which is the part worth sitting with. Handed 4,697, the model would have reported 4,697. Nothing in a single completion weighs an answer against the question that produced it — nothing reads “four thousand” and “this project” in the same breath and finds the two implausible together. It cannot doubt a number it did not produce.
An agent can. It runs the command, gets 4,697, notices that a repository with a few dozen source files does not hold four thousand of them, and runs a better one. That second look is the loop, and it is the thing this commit does not have.
So the gap this series is about is not the gap between a right answer and a wrong one. It is the gap between a reply and a result — and under that, the narrower one that does the damage: a model without a loop cannot check its own work, because there is no second pass in which to check anything. Count what the run contains: one question, one answer, 245 tokens, and not one byte read from disk. The project on my machine is exactly as uncounted as it was before I asked.
What is missing
Here is what answering that question would actually take:
- Decide that listing files is the next action
- Call something that lists files
- Read what came back
- Decide whether that was enough
- Repeat until it is
Steps 1 and 3 need tools: a way for the model to name an action, and a way for the result to come back in. Steps 4 and 5 need a loop: somewhere for turn two to happen at all.
The model got through step 2 on its own: it decided that listing files was the next action and it named the call. Then it handed 3 and 4 to me, and never reached 5 at all — it assumed one exchange would settle the matter. That assumption is the tell. Repeating until the work is done is not something a single completion can want; it is something the program around it has to do.
Step 4 is where the 4,697 lives. Deciding whether what came back was any good is a separate act from producing it, and a single completion has nowhere to put it.
The next commit gives it hands.