The Agent Is a Repo
Andy Smith
Last time I said an AI agent should be a Git repo with a config file. That was a nice idea. Now let’s see if it actually works.
Hypothesis
If an agent is just a config — name and system prompt — then we should be able to express it declaratively and have the tooling validate it. No runtime, no framework, just a schema that says “this is an agent” and rejects anything that isn’t.
Nix is a good fit here. It’s already declarative, it already has a type system (sort of), and it already knows how to build things reproducibly. If we can define an agent as a Nix expression, we get validation, reproducibility, and composability for free.
Increment
Two repos. That’s all we built this iteration.
agent.nix — the schema
A Nix flake that exports one function: mkAgent. You give it a config, it gives you back a dev shell with the agent’s identity baked in.
The schema is minimal on purpose. An agent needs two things to exist: a name and a system prompt. Everything else — transports, memory, adapters — comes later. The schema enforces this:
{
description = "Reflection agent.nix — declarative agent schema";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
{
lib.mkAgent = { agent }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
cfg = agent;
assertions = [
{
assertion = cfg ? name
&& builtins.isString cfg.name
&& cfg.name != "";
message =
"agent.name must be a non-empty string";
}
{
assertion = cfg ? system-prompt
&& builtins.isString cfg.system-prompt
&& cfg.system-prompt != "";
message =
"agent.system-prompt must be a non-empty string";
}
];
failedAssertions =
builtins.filter (a: !a.assertion) assertions;
assertionCheck =
if failedAssertions != [] then
throw (builtins.concatStringsSep "\n"
(map (a: "assertion failed: ${a.message}")
failedAssertions))
else
true;
agent-info = pkgs.writeShellScriptBin "agent-info" ''
echo "name: ${cfg.name}"
echo ""
echo "system prompt:"
echo "${cfg.system-prompt}"
'';
in
assert assertionCheck;
{
devShells.default = pkgs.mkShell {
packages = [ agent-info ];
shellHook = ''
echo ""
echo " reflection: ${cfg.name}"
echo ""
'';
};
}
);
};
}
The key design decision: mkAgent returns flake outputs, not a derivation. The capsule’s flake.nix just calls mkAgent and returns whatever it gives back. This means the capsule doesn’t need to know about nixpkgs or flake-utils — those are the schema’s concern.
Assertions run at evaluation time. If you forget the name, Nix tells you before anything gets built:
error: assertion failed: agent.name must be a non-empty string
example-agent — Ada
The capsule is almost comically simple:
{
description = "Ada — example Reflection agent capsule";
inputs = {
agent-nix.url = "github:reflection-network/agent.nix";
};
outputs = { self, agent-nix }:
agent-nix.lib.mkAgent {
agent = {
name = "Ada";
system-prompt = ''
You are Ada, a helpful assistant.
You respond in the same language the user writes to you.
'';
};
};
}
That’s the entire agent definition. nix develop drops you into a shell:
reflection: Ada
The shell includes an agent-info command that prints the full config:
$ agent-info
name: Ada
system prompt:
You are Ada, a helpful assistant.
You respond in the same language the user writes to you.
Result
It works. An agent is a 17-line flake.nix that imports a schema and declares its identity. The schema validates the config at evaluation time. The dev shell gives you a way to inspect the agent.
Some things I noticed along the way:
The capsule knows nothing about infrastructure. It doesn’t import nixpkgs. It doesn’t know what system it’s running on. It just says “I am Ada, here is my prompt” and the schema handles everything else. This is exactly what I wanted — the agent definition is pure identity, no machinery.
Nix assertions are surprisingly good for this. I was worried I’d need a proper type system or validation library. But assert with a throw gives you clear error messages at eval time, and that’s all you need for a schema this small.
eachDefaultSystem is doing heavy lifting. The capsule author doesn’t think about architectures. mkAgent produces outputs for every platform.
What’s missing is obvious: the agent can’t do anything yet. It can tell you who it is, but it can’t talk to anyone. That’s next.