June 23, 2026

Nano Agent Loop

What an Agent Actually Does Between Prompts

This project started when I briefly saw a mention of a new Elixir wrapper around a bash sandbox. I had already worked on an agent harness, so I thought it would be good practice to build the loop again, this time as small as possible.

I wanted to make the agent loop visible without hiding it behind a framework.

I was also inspired by the Pi harness, which describes itself as a minimal agent harness. So I decided to see how quickly I could build the smallest version of the loop.

The result was elixir-nano-agent-loop.

The project name is a reference to Karpathy’s nanoGPT and nanochat projects.

Nano Agent Loop follows the same spirit, but focuses only on the agent loop. It is not a framework, a durable runtime, or a production agent system. It is a tiny example of the loop itself: prompt, model, tool, output, next step.

This matters because agent systems often look more mysterious than they are.

A good harness is still a complex system: tools, storage, sandboxing, UI, loops, queues, evals, and so on.

But if we remove all the scaffolding, the core idea is simple: the loop.

The Loop

  1. Send the current context to the model
  2. Let the model either answer or request a tool call
  3. Execute the tool call in the host program
  4. Append the tool result back into the conversation
  5. Repeat until the model produces a final answer

The interesting part is that the model can form a hypothesis, test it by running a deterministic command, read the output, notice that its assumption was incomplete, and choose the next step. This is a simple but powerful feedback loop.

If we trim it down to the basics, an agent loop is mostly a state transformation. There is a conversation, a set of tools, and a function that keeps asking the model what should happen next.

In simplified Elixir, the shape looks like this:

def run() do
  tool = Tool.new!(name: "bash", callback: fn commands -> run_bash(commands) end)
  context = Context.new([Context.system(@system_prompt), Context.user(prompt)])
  loop(context, tool)
end

defp run_bash(commands) do
  tool_output = Session.exec(commands)
  tool_output
end

defp loop(context, tool) do
  case ReqLLM.generate_text(context, [tool]) do
    %{type: :tool_calls, tool_calls} ->
      next_context = Context.execute_and_append_tools(tool_calls, tool)
      loop(next_context, tool)

    %{text: text} ->
      text # final result
  end
end

The model does not execute the tool. The harness does that. The model only asks to call a tool:

%{
  name: "bash",
  arguments: %{
    command: "wc -l tx.txt && head -5 tx.txt"
  }
}

The harness decides whether the request is allowed. Then it runs the command in a constrained environment, captures the output, and feeds the result back.

This closes the loop. The output becomes part of the next model call. The next step is no longer based only on the prompt. It is based on what actually happened.

Step by step, the LLM can move closer to the goal. It can also get stuck, or repeat the same kind of step in a cycle.

Demo Time

Let’s see how it works in action:

elixir nano_agent_loop.exs "Create /work/sales.csv, compute revenue by product with shell commands, write /work/report.txt, and show it."

The loop was just 5 turns:

turn 1

assistant -> bash
  ls -la /work 2>/dev/null || mkdir -p /work && ls -la /work
tool ->
  <no output>

turn 2

assistant -> bash
  cat > /work/sales.csv <<'EOF'
  product,quantity,unit_price
  Widget,10,2.50
  Gadget,5,9.99
  Widget,4,2.50
  Dadjet,7,3.75
  Gadget,3,9.99
  Dadjet,2,3.75
  EOF
  printf 'Created sales.csv:\n'
  sed -n '1,20p' /work/sales.csv
tool ->
  Created sales.csv:
  product,quantity,unit_price
  Widget,10,2.50
  Gadget,5,9.99
  Widget,4,2.50
  Dadjet,7,3.75
  Gadget,3,9.99
  Dadjet,2,3.75

turn 3

assistant -> bash
  awk -F, 'NR>1 {rev[$1]+=$2*$3} END {printf "Revenue by product\n"; printf "%-10s %10s\n", "Product", "Revenue"; for (p in rev) printf "%-10s %10.2f\n", p, rev[p]}' /work/sales.csv | sort -k1,1 > /work/report.tmp
  # Keep header first, sorted data after header
  { sed -n '1,2p' /work/report.tmp; sed -n '3,$p' /work/report.tmp | sort; } > /work/report.txt
  rm /work/report.tmp
  printf 'Report contents:\n'
  sed -n '1,20p' /work/report.txt
tool ->
  Report contents:
  Dadjet          33.75
  Gadget          79.92
  Product       Revenue
  Revenue by product
  Widget          35.00

turn 4

assistant -> bash
  {
    printf 'Revenue by product\n'
    printf '%-10s %10s\n' 'Product' 'Revenue'
    awk -F, 'NR>1 {rev[$1]+=$2*$3} END {for (p in rev) printf "%-10s %10.2f\n", p, rev[p]}' /work/sales.csv | sort
  } > /work/report.txt
  printf 'Report contents:\n'
  sed -n '1,20p' /work/report.txt
tool ->
  Report contents:
  Revenue by product
  Product       Revenue
  Dadjet          33.75
  Gadget          79.92
  Widget          35.00

turn 5

final:
Created `/work/sales.csv`, computed revenue by product using `awk`, wrote `/work/report.txt`, and verified/showed the report:

Revenue by product
Product       Revenue
Dadjet          33.75
Gadget          79.92
Widget          35.00

The useful part is not that it succeeds immediately. The useful part is that it can notice a bad result and try again.

This kind of loop was not always useful enough on its own. As LLMs became better, the same simple loop became much more useful.

What This Is Not

This nano loop is intentionally small, so it leaves many useful parts of a modern harness out of scope. But even without these parts, a tiny implementation can still be useful. If you give the model one capable tool, like bash, the loop already has enough to do real work.

Make the loop small enough, and the magic disappears. What remains is just a feedback loop: try, observe, repeat.

References

  1. elixir-nano-agent-loop
  2. Pi harness
  3. nanoGPT
  4. nanochat