Agents From First Principles 00: What Is an Agent, Really?

Page content

What Is an Agent, Really?

This is the first post in Agents From First Principles.

It follows two earlier series.

In PyTorch: Zero to Hero, we worked upward from tensors, autograd and neural-network building blocks until we could build a small language model ourselves.

In Models From First Principles, we moved one level higher. We looked at how learned components can be composed into scorers, value models, policy heads, recurrent models, hierarchical models and compact recursive systems.

Now we move one level higher again.

We are going to build systems around models.

Not bigger neural networks.

Not another transformer.

Not another architecture hidden behind a model name.

Agents.

But before we build planners, tool users, memory systems, search trees, critics and verifiers, we need to remove a lot of ambiguity from the word itself.

The term agent is currently used for almost everything.

A script that calls an LLM is called an agent.

A chatbot is called an agent.

A workflow is called an agent.

A tool-using loop is called an agent.

A collection of twenty LLMs passing messages to one another is called a multi-agent system.

If the word means all of those things, it stops being useful.

So this post starts with the smallest useful definition and builds upward.

By the end, we will have this progression:

model call
model wrapped in a program
stateful workflow
observe → decide → act
observe the result
choose what to do next
agent loop

The important idea is simple:

A model produces an output. An agent uses outputs to decide what happens next.

That distinction will drive the entire series.


1. Start with a model call

The smallest possible LLM program looks something like this:

answer = llm("Explain gradient descent")
print(answer)

There is nothing wrong with this.

In fact, for many problems this is exactly what we should use.

The program supplies an input.

The model returns an output.

The program ends.

We can describe it as:

input
model
output

There is no loop.

There is no external action.

There is no observation of a changed environment.

There is no decision about what to do next.

Calling this an “agent” does not buy us much conceptually.

It is a model invocation.

That distinction matters because one of the recurring mistakes in agent design is to introduce an elaborate runtime where a single model call would have solved the problem more cheaply, quickly and reliably.

We will return to that point repeatedly throughout this series.


2. Put a program around the model

Now let us add a little structure.

class Assistant:
    def __init__(self, model):
        self.model = model

    def run(self, task):
        prompt = f"Solve this task:\n\n{task}"
        return self.model(prompt)

We now have:

  • configuration;
  • a reusable object;
  • a run() method;
  • a prompt-building step;
  • a model call.

That looks more agent-like.

But the computation is still:

task
build prompt
model
answer

The wrapper did not create agency.

It created software structure.

That software structure is useful. Real agent runtimes usually need things such as:

  • configuration;
  • logging;
  • model selection;
  • prompt loading;
  • context passing;
  • persistence;
  • retries;
  • telemetry;
  • caching.

But those facilities are infrastructure around the computation.

They are not the core mechanism that makes the system agentic.

This is an important distinction:

An agent class is not necessarily an agent loop.

A class called Agent may still just perform one deterministic pipeline of operations and return.


3. What changes when the system can act?

Suppose our model can choose an operation.

result = llm("Should I search the web or answer directly?")

The output might be:

SEARCH

Now the program interprets that output:

if result == "SEARCH":
    observation = search_web(query)
else:
    observation = None

Something important has changed.

The model output is no longer merely the final answer.

It is being used as a control signal.

The program has separated:

reason about action
choose action
execute action

This is much closer to the idea of an agent.

We now have two different kinds of output:

  1. world-facing output — the final answer or result;
  2. control output — an instruction that changes what the program does next.

That second category is fundamental.


4. The smallest useful agent loop

Let us make the structure explicit.

while True:
    observation = observe()
    action = decide(observation)
    result = act(action)

    if finished(result):
        break

That gives us the classic loop:

observe
decide
act
observe
decide
act
...

This is the core structure we will build on throughout the series.

The model might implement decide().

But the agent is the loop that connects decision-making to actions and observations.

The model is one component inside that loop.

This is the same decomposition habit we used throughout Models From First Principles.

There, a “model” turned out to contain smaller models, heads, blocks and operations.

Here, an “agent” turns out to contain:

agent
├── state
├── observations
├── decision mechanism
├── action space
├── executor
├── stopping rule
└── history

No magic is required.


5. Build the smallest agent in Python

Let us create an agent with only two actions:

CALCULATE
FINAL

The agent is given a task.

It may either ask a calculator for help or produce the final answer.

from dataclasses import dataclass, field


@dataclass
class AgentState:
    task: str
    history: list[dict] = field(default_factory=list)
    done: bool = False
    final_answer: str | None = None

The state is deliberately boring.

That is good.

Now define an action:

@dataclass
class Action:
    name: str
    argument: str

Our decision function might ask an LLM for a structured action:

def decide(model, state: AgentState) -> Action:
    prompt = f"""
Task:
{state.task}

History:
{state.history}

Choose exactly one action:

CALCULATE <expression>
FINAL <answer>
"""

    output = model(prompt).strip()

    name, argument = output.split(" ", 1)
    return Action(name=name, argument=argument)

Then execute it:

def execute(action: Action):
    if action.name == "CALCULATE":
        return {
            "type": "calculation",
            "value": safe_calculate(action.argument),
        }

    if action.name == "FINAL":
        return {
            "type": "final",
            "value": action.argument,
        }

    return {
        "type": "error",
        "value": f"Unknown action: {action.name}",
    }

And finally build the loop:

def run_agent(model, task, max_steps=5):
    state = AgentState(task=task)

    for step in range(max_steps):
        action = decide(model, state)
        observation = execute(action)

        state.history.append({
            "step": step,
            "action": action,
            "observation": observation,
        })

        if observation["type"] == "final":
            state.done = True
            state.final_answer = observation["value"]
            break

    return state

That is already enough to expose most of the important pieces.

task
state
model chooses action
executor runs action
observation added to state
model chooses again
...

The model did not become more intelligent.

We changed the computation around it.


6. The environment matters

An agent does not act in a vacuum.

The thing receiving actions is usually called the environment.

That word can sound more exotic than it is.

For a robot, the environment might literally be the physical world.

For a coding agent, it might be:

filesystem
Git repository
compiler
unit tests
terminal
CI system

For a research agent:

search engine
web pages
papers
database
notes

For a writing agent:

document
outline
style rules
source material
revision history

For a customer-support agent:

conversation
CRM
knowledge base
ticket system

The environment is simply the system whose state can be observed and changed.

This gives us a more complete picture:

                  ┌───────────────┐
                  │  environment  │
                  └───────┬───────┘
                          │ observation
                    ┌───────────┐
                    │   agent   │
                    └─────┬─────┘
                          │ action
                  ┌───────────────┐
                  │  environment  │
                  └───────────────┘

If the environment never changes and the system never observes new information, then the “agent” may simply be a complicated prompt pipeline.

Sometimes that is fine.

But we should be precise about what mechanism we have actually built.


7. State is not the same thing as memory

Agents need state.

That does not necessarily mean they need long-term memory.

State is simply the information required to continue the current computation.

For example:

state = {
    "task": task,
    "step": 3,
    "last_action": "run_tests",
    "last_observation": "2 tests failed",
}

That is working state.

A more complete state might include:

state = {
    "task": task,
    "plan": plan,
    "history": history,
    "files_changed": files_changed,
    "test_results": test_results,
    "budget_remaining": budget,
}

Long-term memory is different.

It asks whether information survives beyond this run and can influence future runs.

We will dedicate a later post to memory.

For now, the important distinction is:

state
= what this run currently knows

memory
= information preserved for later retrieval

8. History changes the next decision

Why keep history at all?

Because an agent should usually avoid making each decision as though nothing happened before it.

Consider:

Step 1
Action: RUN_TESTS
Observation: test_parse_date failed

Step 2
Action: EDIT parse_date()
Observation: file changed

Step 3
Action: RUN_TESTS
Observation: all tests pass

Each observation changes what should happen next.

Without state, we repeatedly ask:

What should I do?

With state, we ask:

Given what has already happened, what should I do next?

That is a much richer computation.


9. A workflow is not necessarily an agent

Now consider this program:

research = researcher(task)
plan = planner(task, research)
draft = writer(task, plan)
review = critic(draft)
final = reviser(draft, review)

This might be sophisticated.

It might call five different models.

It might even use five classes named ResearchAgent, PlannerAgent, WriterAgent, CriticAgent and RevisionAgent.

But structurally it is still:

A
B
C
D
E

The route is predetermined.

The system does not inspect an observation and decide whether B, C, D or E should run next.

That makes it closer to a workflow.

A workflow can be excellent engineering.

In many applications it is preferable to a free agent loop because it is:

  • predictable;
  • easier to test;
  • easier to reproduce;
  • easier to budget;
  • easier to secure.

So this is not a hierarchy where “agent” means better.

It is a distinction in control flow.


10. The control-flow test

A useful diagnostic is to ask:

Who decides what happens next?

If the answer is:

the programmer already hard-coded the next step

then we probably have a workflow.

If the answer is:

the system inspects the current state or observation and selects the next action

then we are moving into agentic control.

Compare these two examples.

Fixed workflow

research()
plan()
write()
review()

Agentic loop

while not done:
    action = choose_action(state)
    observation = execute(action)
    state = update_state(state, action, observation)

That second form is the basic architecture we care about in this series.


11. The action space defines what the agent can do

The model may be extremely capable.

But the agent can only affect the environment through the actions exposed to it.

Suppose we define:

ACTIONS = {
    "SEARCH": search,
    "READ_FILE": read_file,
    "WRITE_FILE": write_file,
    "RUN_TESTS": run_tests,
    "FINAL": finish,
}

Then the agent’s effective capabilities are bounded by those operations.

This is valuable for both design and safety.

Instead of giving a model arbitrary code execution, we may expose narrow operations such as:

read_file(path)
search_docs(query)
run_test(test_name)
propose_patch(diff)

The action interface becomes a contract between:

model decision
agent runtime
external system

This is one reason tool design matters so much in practical agent systems.

A weak tool interface can make a strong model look incompetent.

A precise tool interface can make a modest model surprisingly effective.


12. Actions should be structured

We could ask a model to emit arbitrary text:

I think maybe you should search for PyTorch documentation.

Then our program has to guess what that means.

A better design is:

{
  "action": "search",
  "query": "PyTorch autograd documentation"
}

Now the boundary is explicit.

def dispatch(action):
    if action["action"] == "search":
        return search(action["query"])

The exact serialization could be JSON, a typed object, a function call or another schema.

The important concept is structured control.

The model proposes an action inside a constrained grammar.

The runtime validates it.

Only then is anything executed.


13. Never confuse proposing an action with executing it

This separation is critical:

MODEL
proposes action
VALIDATOR
checks action
EXECUTOR
changes environment

The model should not automatically be trusted merely because it produced something that looks executable.

For example:

action = model_decision(state)

validated = validate(action)
if not validated.ok:
    return validated.error

observation = execute(validated.action)

This gives us a place to enforce:

  • allowed tools;
  • argument schemas;
  • path restrictions;
  • timeouts;
  • budgets;
  • permissions;
  • confirmation boundaries;
  • sandboxing.

Agent design is therefore not only a prompting problem.

It is a software architecture problem.


14. The model is a policy over actions

In the previous series we discussed explicit policy heads.

The same idea appears here at a different level.

Given current state s, the agent needs to choose an action a.

Conceptually:

state
policy
action

The policy might be:

  • an LLM;
  • a small learned classifier;
  • deterministic rules;
  • a search policy;
  • a mixture of those things.

This is useful because it breaks the assumption that every decision inside an agent must be made by a frontier language model.

For example:

def choose_action(state):
    if state["tests_failed"]:
        return "DEBUG"

    if not state["tests_run"]:
        return "RUN_TESTS"

    return llm_choose_action(state)

That is still an agent loop.

Some control is deterministic.

Some is learned.

This hybrid approach is often better than asking the model to rediscover obvious control rules every time.


15. Agents need stopping conditions

Any loop needs a way to stop.

This seems trivial until an LLM repeatedly decides that one more search, one more rewrite or one more attempt might help.

A minimal stopping rule might be:

if action.name == "FINAL":
    stop()

But practical systems usually need more.

if step >= max_steps:
    stop("step budget exhausted")

if cost >= max_cost:
    stop("cost budget exhausted")

if elapsed >= timeout:
    stop("time budget exhausted")

if verified_success:
    stop("goal achieved")

This gives us a general form:

continue while
    goal not satisfied
AND step budget remains
AND time budget remains
AND cost budget remains
AND safety constraints remain satisfied

Without explicit stopping conditions, “autonomy” can simply become uncontrolled iteration.


16. Success should be observable when possible

Suppose an agent is asked:

Fix the failing unit test.

How does it know when the task is complete?

One weak answer is:

The model thinks the code looks correct.

A stronger answer is:

The previously failing test passes.

Even stronger:

The failing test passes,
the complete test suite passes,
and no forbidden files changed.

The more the environment exposes measurable success signals, the less we need to rely on self-assessment.

This will become a major theme later in the series when we build verification-driven agents.

For now, remember:

An agent loop becomes much stronger when success can be observed rather than narrated.


17. Failure is an observation too

Suppose an action fails:

READ_FILE /missing/config.yaml

The result might be:

{
  "ok": false,
  "error": "FileNotFoundError"
}

A useful agent does not hide that failure.

It feeds it back into the loop:

action
error
observation
new decision

Now the agent may choose:

LIST_DIRECTORY

then discover the correct path and continue.

This is one of the most concrete differences between a one-shot model call and an agent loop.

A model call fails when the answer is wrong.

An agent may be able to observe the failure and recover.


18. Retries are not the same thing as agency

A simple retry loop looks like:

for _ in range(3):
    try:
        return call_api()
    except TimeoutError:
        pass

That is useful resilience.

But the decision is predetermined:

if timeout → retry same thing

An agentic recovery loop might instead choose among:

retry
change query
use different tool
inspect error
change plan
ask for help
stop

So we should distinguish:

retry policy

from:

adaptive action selection

Again, neither is inherently better.

The simplest mechanism that solves the problem is usually the right one.


19. Context is the agent’s working world model

A practical agent typically passes a context object through the loop.

context = {
    "goal": goal,
    "history": [],
    "observations": [],
    "artifacts": {},
    "step": 0,
}

Each component reads what it needs and writes what it produces.

For example:

context["plan"] = make_plan(context)
context["search_results"] = search(context["query"])
context["test_results"] = run_tests()

This is one reason a shared context dictionary appears so often in real agent frameworks.

It functions as a lightweight state bus.

But it can also become dangerous.

If every component can read and mutate every key, then after enough growth we get:

mystery state
+ hidden dependencies
+ accidental overwrites
+ impossible debugging

So even something as simple as a context object eventually needs contracts.

We will revisit that as the series grows.


20. The agent runtime is not the intelligence

Real systems quickly accumulate infrastructure:

BaseAgent
PromptLoader
ModelClient
MemoryStore
ScoringService
Logger
ToolRegistry
Context
RetryPolicy
Telemetry

That infrastructure matters enormously.

But we should keep the conceptual layers separate.

AGENT TECHNIQUE
    what computation are we performing?

RUNTIME
    how do we execute that computation reliably?

MODEL
    what learned function participates in the computation?

This separation keeps us from attributing capability to the wrong layer.

If a system improves after adding best-of-N generation, the improvement may come from more sampled computation, not from a better model.

If it improves after adding retrieval, the improvement may come from better information, not from better reasoning.

If it improves after adding tests, the improvement may come from external verification, not self-reflection.

These distinctions are exactly what we want to understand in this series.


21. A useful taxonomy: model, workflow, agent

Let us create a simple working taxonomy.

Model call

input → model → output

Example:

summary = llm(document)

Workflow

A → B → C → D

The route is predetermined by code.

Example:

research = research_step(task)
plan = plan_step(research)
draft = draft_step(plan)
review = review_step(draft)

Agent

state
choose next action
execute
observe
update state
choose next action

The route can change based on observations.

This taxonomy is deliberately pragmatic rather than philosophical.

It gives us useful engineering questions.


22. There is a spectrum, not a binary switch

Real systems are often hybrids.

Consider:

fixed preprocessing
agent loop
fixed verification pipeline

Or:

planner
fixed list of plan steps
agentic recovery only when a step fails

Or:

workflow
router chooses one of three branches
workflow continues

So it is not useful to argue endlessly about whether a system “really counts” as an agent.

What matters more is identifying exactly where adaptive control exists.

Ask:

What state is observed?
What decisions are adaptive?
What actions are available?
What changes in the environment?
What feedback comes back?
What causes the loop to stop?

Those questions tell us far more than the label.


23. The agent loop can be deterministic

We often associate agents with LLMs because current AI systems use them heavily.

But nothing about the loop requires a language model.

def decide(state):
    if state.temperature > 25:
        return Action("TURN_ON_FAN", "")
    return Action("WAIT", "")

This is an agent in the broad computational sense.

The policy is deterministic.

Why does that matter for our series?

Because it prevents us from accidentally teaching:

agent = LLM + prompt

A better formulation is:

agent = state + policy + actions + feedback loop

An LLM is one possible policy implementation.


24. LLMs are useful because the action policy can generalize

A deterministic policy is excellent when we can enumerate the rules.

But suppose the agent sees:

The build failed because generated protobuf bindings are stale.

The correct next action may require interpreting a novel error and relating it to repository structure.

A language model can map rich observations to actions without us writing an explicit branch for every possible sentence.

Conceptually:

unstructured observation
         LLM
 structured action

That is where LLMs become extremely useful inside agent loops.

They act as flexible policies over complicated state.

But flexibility brings uncertainty.

So we surround them with contracts, tools, validators and verification.


25. The first important design trade-off: freedom vs control

Suppose an agent can choose any shell command:

RUN_SHELL <anything>

That gives it enormous flexibility.

It also makes validation, reproducibility and safety much harder.

Now compare:

READ_FILE(path)
WRITE_PATCH(diff)
RUN_TEST(name)
SEARCH_CODE(query)

The action space is narrower.

But each action has clearer semantics.

This gives us a recurring trade-off:

broad action space
    ↑ flexibility
    ↓ controllability

narrow action space
    ↓ flexibility
    ↑ controllability

Good agent systems usually do not maximize autonomy blindly.

They expose the smallest useful action space for the task.


26. The second trade-off: autonomy vs determinism

Consider two systems.

System A

research
write
review

System B

agent decides:
research more?
write?
review?
search?
restart?
finish?

System B has greater autonomy.

But it also has more possible trajectories.

That means:

  • harder testing;
  • harder reproduction;
  • more variable latency;
  • more variable token cost;
  • more failure modes.

Autonomy is therefore not free capability.

It is an architectural trade-off.


27. The third trade-off: more inference vs better inference

One of the themes of the advanced agent series later will be that many agent techniques increase capability by spending more inference-time computation.

For example:

one answer

becomes:

8 answers
+ scoring
+ revision

Or:

one reasoning trajectory

becomes:

search tree of 50 trajectories

The underlying model may be identical.

This matters because when an agent system improves, we should ask:

Did the architecture improve the quality of computation, or did we simply buy more samples and more tokens?

Both can be useful.

But they are different claims.


28. A minimal reusable agent abstraction

Let us now write a cleaner interface.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any


@dataclass
class Observation:
    kind: str
    data: Any


@dataclass
class Action:
    name: str
    args: dict[str, Any] = field(default_factory=dict)


@dataclass
class State:
    goal: str
    history: list[tuple[Action, Observation]] = field(default_factory=list)
    step: int = 0
    done: bool = False


class Agent(ABC):
    @abstractmethod
    def decide(self, state: State) -> Action:
        raise NotImplementedError

The runtime owns execution:

class AgentRuntime:
    def __init__(self, agent, tools, max_steps=10):
        self.agent = agent
        self.tools = tools
        self.max_steps = max_steps

    def execute(self, action: Action) -> Observation:
        if action.name == "FINAL":
            return Observation("final", action.args["answer"])

        tool = self.tools.get(action.name)
        if tool is None:
            return Observation("error", f"Unknown action {action.name}")

        try:
            value = tool(**action.args)
            return Observation("tool_result", value)
        except Exception as exc:
            return Observation("error", repr(exc))

    def run(self, goal: str) -> State:
        state = State(goal=goal)

        while not state.done and state.step < self.max_steps:
            action = self.agent.decide(state)
            observation = self.execute(action)

            state.history.append((action, observation))
            state.step += 1

            if observation.kind == "final":
                state.done = True

        return state

Notice what happened.

The runtime itself does not care whether decide() uses:

  • an LLM;
  • rules;
  • a small neural model;
  • a search algorithm;
  • several experts.

That is a powerful separation.


29. Now put an LLM inside the policy

import json


class LLMAgent(Agent):
    def __init__(self, model):
        self.model = model

    def decide(self, state: State) -> Action:
        history = [
            {
                "action": action.name,
                "args": action.args,
                "observation": observation.data,
            }
            for action, observation in state.history
        ]

        prompt = f"""
Goal:
{state.goal}

History:
{json.dumps(history, indent=2)}

Choose the next action as JSON.

Allowed actions:
- SEARCH {{"query": "..."}}
- CALCULATE {{"expression": "..."}}
- FINAL {{"answer": "..."}}
"""

        raw = self.model(prompt)
        data = json.loads(raw)

        return Action(
            name=data["action"],
            args=data.get("args", {}),
        )

Now we have a complete conceptual agent:

goal
state
LLM policy
structured action
runtime validation / dispatch
tool or environment
observation
state update
repeat

Everything in the later series is an extension of this diagram.


30. Where do prompts fit?

Prompts matter.

But prompts are not the whole architecture.

A prompt might define:

  • role;
  • task;
  • action schema;
  • constraints;
  • available tools;
  • examples;
  • output format.

That is the policy interface presented to the model.

But changing this:

You are a helpful coding agent.

into this:

You are an expert autonomous software-engineering agent.

has not added a tool, a verifier, a search tree, memory or feedback.

Agent capability comes from the system computation, not from agent-like adjectives in the prompt.


31. Where do models from the previous series fit?

This is where the learning path joins together.

Suppose our agent generates five candidate actions.

We need to choose one.

We could ask another LLM.

Or we could use a learned scorer.

state
generate candidate actions
 ├→ A
 ├→ B
 ├→ C
 ├→ D
 └→ E
   scorer
  best action

Now MR.Q suddenly has a concrete role.

A multi-head architecture such as EBT or SICQL might provide several signals.

A recursive model such as Tiny might spend extra computation on hard candidates.

An HRM-style model might be evaluated as a more expensive decision layer.

The important point is that the model series and agent series are different layers of the same system.

PyTorch
models
agent policies / scorers
agent runtime
environment

32. Caching is an agent-runtime optimization, not a reasoning technique

Suppose an agent repeatedly sees the same prompt or equivalent state.

We could call the model every time.

Or we could reuse a previous result.

key = hash_prompt(prompt)

if key in cache:
    return cache[key]

result = model(prompt)
cache[key] = result
return result

This can massively reduce:

  • latency;
  • token cost;
  • local inference time.

But again, we should classify it correctly.

Caching changes execution economics.

It does not necessarily improve reasoning quality.

This is exactly why separating runtime from agent technique is valuable.


33. Logging is part of the architecture if you want to understand failures

Agent loops are stochastic programs with branching control flow.

If we log only the final answer, debugging becomes painful.

A useful trace should contain at least:

run id
step
state summary
action proposed
action validated
action executed
observation
latency
cost
termination reason

For example:

{
  "step": 4,
  "action": "RUN_TESTS",
  "args": {"target": "tests/test_parser.py"},
  "observation": {
    "passed": 17,
    "failed": 1
  }
}

This is not glamorous agent research.

It is what makes agent systems debuggable.

And as systems become more agentic, traceability becomes more—not less—important.


34. The agent should know why it stopped

A final state should record a termination reason.

state.termination = "verified_success"

Possible values might include:

verified_success
model_finished
step_budget_exhausted
timeout
cost_budget_exhausted
invalid_action
permission_denied
unrecoverable_error

Why bother?

Because these are not equivalent outcomes.

A system that returns an answer because tests passed is different from one that returns because it ran out of steps.

If both are simply labelled completed, we destroy useful information.


35. Agents introduce new failure modes

A model can hallucinate.

An agent can hallucinate and then act on the hallucination.

That changes the risk profile.

Common agent-specific failure modes include:

Repetition

search
search
search
search

Thrashing

edit A
undo A
edit A differently
undo again

Goal drift

The agent gradually optimizes something adjacent to the original task.

Tool misuse

The selected action is semantically wrong even though the tool call is syntactically valid.

Premature completion

The model declares success without verifying it.

Budget blindness

The agent spends more time or tokens chasing diminishing improvements.

State corruption

Later decisions depend on stale or incorrectly updated context.

These are system-level failures.

A better base model may reduce some of them, but architecture matters too.


36. A tiny loop can already outperform a larger prompt

Imagine a coding task.

One-shot approach

Read this error and give me the fixed code.

Agent loop

inspect error
read relevant file
propose patch
run test
observe failure
inspect new error
revise patch
run test

The same underlying model may perform much better in the second system because it receives fresh evidence after each action.

This is one of the central ideas of agentic computation.

We do not require the model to predict the entire future correctly in one shot.

We allow it to interact with reality.


37. But loops can also make things worse

More steps mean more opportunities to fail.

Suppose each independently risky decision succeeds 95% of the time.

A twenty-decision process does not magically become more reliable because it has more reasoning steps.

The exact probabilities in real agents are not independent, but the intuition matters:

more decisions
→ more opportunities for recovery
AND
→ more opportunities for error

So we should never equate:

more agent steps

with:

better agent

The loop should exist because the environment can provide useful feedback.


38. When should you use a single call instead?

Use a single model call when:

  • the task is self-contained;
  • the required information is already in context;
  • there is no useful external feedback;
  • one output is sufficient;
  • latency matters;
  • determinism matters;
  • failure can be handled by the caller.

Examples:

rewrite this paragraph
classify this message
extract these fields
summarize this document
translate this text

Do not build an autonomous loop merely because agents are fashionable.


39. When is a workflow enough?

Use a fixed workflow when the steps are known in advance.

For example:

transcribe audio
extract entities
summarize
format report

There is no strong reason for a model to rediscover this sequence on every run.

Hard-code it.

You gain:

  • predictable execution;
  • easier tests;
  • clearer ownership;
  • simpler failure recovery.

40. When does an agent loop become useful?

Use adaptive control when the correct next action genuinely depends on what happens during execution.

Examples:

Debugging

run test
inspect actual failure
choose what to inspect next

Research

search
read results
decide whether evidence is sufficient
search differently if needed

Repository work

inspect repository
decide relevant files
change code
run validation
respond to failures

Investigation

form hypothesis
collect evidence
update hypothesis
choose next experiment

The common property is information arrives during the task that should change future actions.

That is where the loop earns its cost.


41. A practical definition

For this series, we will use the following working definition:

An agent is a stateful system that selects actions, executes them against an environment, observes the results, and uses those observations to influence subsequent decisions.

This definition is intentionally engineering-focused.

It does not require consciousness.

It does not require long-term memory.

It does not require an LLM.

It does not require unrestricted autonomy.

It gives us a concrete architecture to build and test.


42. The anatomy of an agent

From now on, when we inspect an agent system, we can decompose it into these questions.

Goal

What is the system trying to accomplish?

State

What information persists during the run?

Observation

What can the system learn from the environment?

Policy

How is the next action selected?

Action space

What operations are allowed?

Executor

What actually performs the action?

Feedback

How does the result return to the decision system?

Termination

What stops the loop?

Verification

How do we know whether the goal was achieved?

Trace

What evidence do we preserve about the trajectory?

That checklist will be reused throughout the rest of the series.


43. The same agent can use different policies

Because the runtime and policy are separated, we can perform a useful experiment.

Keep everything constant:

tools
state
budget
environment
verification

Then swap only the decision mechanism.

rules
vs
small local model
vs
frontier LLM
vs
search policy

Now we can ask whether a more expensive policy actually improves task completion.

This is exactly the kind of controlled comparison we used in the Models From First Principles series.


44. The same model can support different agent techniques

We can also hold the model constant and change the surrounding algorithm.

For example:

same model
   ├→ one-shot
   ├→ best-of-N
   ├→ critique/revise
   ├→ planner/executor
   ├→ tool loop
   └→ search tree

Now we can measure what the agent technique contributes independently of model improvements.

This separation will become especially important once we reach advanced agents.


45. Do not benchmark agents only by final quality

Suppose two agents both solve 80% of tasks.

Agent A uses:

2 model calls
1 tool call
3 seconds

Agent B uses:

47 model calls
19 tool calls
2 minutes

Those are not equivalent systems.

Useful agent metrics include:

success rate
verified success rate
steps per task
model calls per task
tool calls per task
tokens per task
wall-clock latency
cost per successful task
recovery rate after failure
invalid-action rate
premature-stop rate

The denominator per successful task is especially important.

A cheap agent that fails constantly may not be cheap at all.


46. The trajectory is part of the result

For a simple model call, the result is usually the output.

For an agent, we often need both:

final result
+
trajectory

For example:

{
  "result": "patch applied and tests pass",
  "trajectory": [
    "read parser.py",
    "read failing test",
    "edit parser.py",
    "run test",
    "observe failure",
    "edit parser.py",
    "run full suite"
  ]
}

Why preserve the trajectory?

Because it enables:

  • debugging;
  • auditing;
  • replay;
  • cost analysis;
  • failure classification;
  • training data;
  • future strategy learning.

Later, those traces can themselves become memory or evidence.


47. A simple experiment: one-shot vs loop

Before building sophisticated agents, run a small controlled experiment.

Choose tasks with external verification.

For example, ten small Python bugs with unit tests.

Condition A

one prompt
one model call
produce patch

Condition B

same model
same task
agent can:
- read files
- edit patch
- run tests
- retry up to 5 steps

Measure:

verified success
model calls
wall time
tokens
number of edits
number of recovered failures

This tells us whether the loop itself adds useful capability for that task distribution.

That is much stronger than saying:

agents are better because they are agentic.


48. Another experiment: fixed workflow vs adaptive routing

Suppose every task could use:

search → plan → answer → review

Compare that fixed workflow against an agent that chooses whether each stage is necessary.

The adaptive version might skip research for trivial questions or trigger a second review only when confidence is low.

Measure:

quality
latency
cost
steps

If adaptive routing gives the same quality for half the cost, agency earned its complexity.

If it gives the same route almost every time, the workflow was probably enough.


49. Real systems usually combine deterministic and agentic control

A serious system might look like:

validate input             deterministic
retrieve repository state   deterministic
choose investigation action agentic
execute tool                deterministic
validate tool result         deterministic
choose next action           agentic
run final tests              deterministic
accept only if tests pass    deterministic

This pattern is extremely important.

We do not have to choose between:

fully scripted

and:

fully autonomous

The strongest architecture may put adaptive decisions only where uncertainty genuinely exists.


50. Autonomy should be earned one boundary at a time

A useful engineering progression is:

model proposes text
model proposes structured action
program validates action
program executes narrow tool
model observes result
model chooses another action

Then, only if evidence supports it, expand:

more tools
more steps
more write permissions
more planning freedom
more search depth

This is safer and easier to debug than beginning with arbitrary shell access and a prompt saying “complete the task autonomously.”


51. What Stephanie teaches us without making this a Stephanie series

The real Stephanie agent runtime contains many of the infrastructural pieces we have just separated conceptually:

  • a shared base agent;
  • model configuration;
  • synchronous and asynchronous model calls;
  • prompt loading and persistence;
  • context propagation;
  • prompt/result caching;
  • scoring hooks;
  • logging;
  • an abstract run() contract.

Those are useful real-world implementation examples.

But they are not the curriculum.

The curriculum is the underlying technique.

So throughout this series we will do the same thing we did with the model series:

real implementation
extract mechanism
rebuild independently
understand trade-offs
test contribution

That keeps the material useful even if the reader never installs Stephanie.


52. What comes next

This introductory post has deliberately kept the agent primitive small.

state
choose action
execute
observe
repeat

Now we can start adding techniques one at a time.

The planned progression is roughly:

00  What Is an Agent, Really?

01  The Simplest Agent:
    Prompt → Model → Structured Action

02  Best-of-N:
    Generate Several Candidates and Select One

03  Critique and Revise:
    Let the System Improve Its Own Draft

04  Planning:
    Separate Deciding What to Do From Doing It

05  Act → Observe → Correct:
    Build a Real Feedback Loop

06  Tool-Using Agents:
    Give the Model a Controlled Action Space

07  Memory:
    Working State, Retrieval and Reusing Past Work

08  Search:
    Explore More Than One Trajectory

09  Verification:
    Use Reality, Tests and Measurements as Feedback

We will stop the first series there.

Then Advanced Agents From First Principles can take over with techniques such as:

self-consistency
tree of thought
beam search
MCTS
evolutionary search
mixture of experts
multi-agent debate
adaptive routing
self-tuning systems

That separation is deliberate.

First learn the loop.

Then learn how to make the loop search.


53. The entire learning ladder

We can now see the larger structure.

Level 1 — PyTorch

tensor
autograd
module
attention
transformer
language model

Level 2 — Models

embeddings
pair scorer
Q / V / policy
modular heads
recurrence
hierarchical recurrence
recursive models

Level 3 — Agents

model call
state
action
environment
observation
feedback loop
tools
memory
verification

Level 4 — Advanced agents

multiple trajectories
search
experts
routing
coordination
adaptation

Each level is built from the previous one.

That is the point of doing this from first principles.


54. The central rule for this series

There is one rule I want to carry through every post:

Do not add an agent mechanism because it sounds more intelligent. Add it because you can identify the failure it is supposed to fix and measure whether it fixes it.

If one model call solves the task, use one model call.

If a fixed workflow solves it, use a fixed workflow.

If fresh observations genuinely need to change the next action, introduce an agent loop.

If the loop still fails because one trajectory is brittle, then later we can introduce search.

The progression should always be:

observe failure
identify missing mechanism
add smallest useful mechanism
measure again

That is how we keep agents understandable.

And that is where we will start in the next post: by building the simplest structured-action agent we can possibly make.