A coding agent

A coding agent is the loop you've watched Claude Code or Pi run: read the repo, make one change, run the tests, read the output, decide what's next — until it's green. Most of that is plumbing: a tool protocol, a sandbox to run commands in, a transcript, a budget, a way to add new abilities. Workbooks ships all of it as primitives, so the part you write is small — and this lesson shows where each piece lives: the brain, where it runs, how it's contained, how you give it new powers, and how you run a fleet.

The brain

An agent is a sandboxed function: the block defines it completely — what it knows, what it can do, what it may touch, and where it stops. Four fields, nothing outside the block needed:

agent :coder do
  prompt """
  You are a coding agent working in /work — a checkout of the repository. Your one tool is
  bash. Work like a careful engineer: read first, change ONE thing, run the tests and READ
  the output, repeat — then stop and summarize. Never guess a result you could run for.
  """

  tools coreutils, git, rg            # capabilities — the kits it may run
  grant fs, exec                      # permissions — the host powers it may use
  limit turns: 40, timeout: 180_000   # guardrails — when it must stop
end

tools/grant/limit are composable Elixir — a value, a list, or a reference to something defined elsewhere. That's the whole agent; you don't define it twice. (Shorthand: agent :x do …prose… end with no fields is just a prompt — fine for a quick brain.)

Where it runs, and how it's contained

This is the part that matters for trusting an agent with your repo:

So "let an autonomous agent edit and run code" is safe by construction: the blast radius is one /work sandbox.

Its capabilities are kits — and you can add one

An agent's abilities are its kits: coreutils (the unix base — ls, cat, sed, grep…) and web ship by default. To give the agent a new power, you don't edit the agent — you author a toolkit, a small CLI compiled to a wasm command that instantly shows up in bash:

toolkit :rg do
  // summary: ripgrep-style search — rg <pattern> over /work
  #include <stdio.h>
  // …a real grep/ripgrep in C, compiled to wasm32-wasi…
  int main(int argc, char **argv) { /* search stdin/files for argv[1] */ }
end

Now rg is a command the :coder can run — capability granted, sandboxed like every other kit. That's the model: capabilities are composable CLIs, not bespoke tool definitions. (The sandbox kind takes the complementary path — guest code with explicit grant: net|kv|fs|…] capabilities; see [Grants & capabilities.)

The loop you don't have to write

When the agent runs, the runtime drives the loop for you: send the instructions + task to the model advertising bash → if it calls bash, run the command in the /work sandbox and feed the output back → repeat (think, run, read) → stop when the model answers without a tool call, or the wall-clock budget expires. You never touch the tool-call wiring, transcript, or context-window management.

Giving it a task

The agent :coder block above is the agent; the runtime runs declared agents for you. To drive one from your own code — seeding a repo into /work and capturing the edits — call Nexus.Agent.run/1 from a server. It takes the same fields as the block (prompt, tools, grant, limit), runs the loop, and returns the final answer plus whatever the agent wrote:

server :fix do
  route "POST /fix", :run

  def run(req) do
    case Nexus.Agent.run(
           prompt: "You fix failing tests in /work. Read, change one thing, run the tests, repeat.",
           tools: [:coreutils, :git, :rg],
           grant: [:fs, :exec],
           limit: [turns: 40, timeout: 180_000],
           task: req.body["task"],         # "make the failing date test pass"
           seed: req.body["files"]         # %{"lib/date.ex" => "…", "test/date_test.exs" => "…"}
         ) do
      {:ok, %{answer: summary, vfs_files: files}} -> %{summary: summary, files: files}
      {:error, reason} -> {500, %{error: inspect(reason)}}
    end
  end
end

Same vocabulary either way: declare a reusable agent with the agent block, or define one inline here for a one-off — prompt/tools/grant/limit mean the same thing in both. seed writes the repo into the sandbox; vfs_files hands back the edited files — the diff.

Running a fleet

Because a run is a function call, doing a hundred is a map. Fan a coder across every failing ticket; the BEAM runs thousands of lightweight processes, so they go at once:

server :sweep do
  @coder [
    prompt: "You fix failing tests in /work. Read, change one thing, run the tests, repeat.",
    tools: [:coreutils, :git, :rg],
    grant: [:fs, :exec]
  ]

  def run(tickets) do
    tickets
    |> Enum.map(fn t -> Task.async(fn -> Nexus.Agent.run([task: t.body, seed: t.repo] ++ @coder) end) end)
    |> Task.await_many(:infinity)
  end
end

One agent or a thousand is the same code — only the list gets longer. That fan-out is the agents-at-scale pattern; swap the worker for any job.

What to take away