Build Roles, Not Agents
In the last post I introduced Sam, a business development agent that's been running inside Quant for a few weeks, and closed on a one-line thesis: the role is the unit of evolution, not the agent. That post was the origin story. This one is the architecture — so it goes technical fast.
The claim to test is simple. When we say "an agent," we usually mean a model plus a prompt plus some tools: a thing that exists at runtime and vanishes when the process ends. Praxis says that's the wrong unit. The durable thing — the one that accumulates, that you manage, that you'd be sad to lose — is the role, and a role is a directory on disk. The model is just the runtime that animates it. Swap the model and the role keeps everything it has learned, because the learning was never in the weights.
Self-improvement → Role-based growth
The pattern Praxis defines itself against is the self-improving agent — the one that rewrites its own playbook from outcomes. It's a tempting design, and an unstable one:
| Self-improving agent | Role-based agent | |
|---|---|---|
| What grows | The whole playbook, including identity | Only bounded surfaces |
| Who authors voice & rules | The agent, over time | The operator, up front |
| Failure mode | Unbounded drift; small misjudgements compound | Bounded; drift is visible and revertible |
| Where growth lives | Weights / opaque state | Files on disk |
The framework's philosophy doc puts the risk plainly:
Self-improving agents rewrite their own playbook based on outcomes. That's powerful but unbounded — the constitution drifts, the supervisor loses the ability to predict what the agent will do, and small misjudgements compound into structural ones.
A role-based agent grows within a defined role. It writes memory, files escalations, and drafts proposed skills — but its voice, its hard rules, and its reference data stay in the operator's hand.
Key insight: the goal isn't to forbid autonomy, it's to make it legible. Everything the role learns is a file, so growth is "legible to a human, not buried in vector embeddings."
The anatomy of a role
Here's the whole abstraction. There's no hidden one behind it:
role-home/
├── persona.md # the constitution: identity, voice, hard inhibitions
├── CLAUDE.md # operating manual, read on session start
├── verbs/ # modular playbooks, one markdown file per behaviour
│ └── proposed/ # drafts the role writes; operator promotes them
├── lib/ # declarative reference data (YAML)
│ └── autonomy.yaml # which surfaces the role may edit, and how
├── memory/ # observational notebook the role writes freely
├── escalations/ # structured asks the role raises; operator triages
├── output/ # typed work product (document/draft/record/plan/reference)
└── logs/ # JSONL decision feed
Two properties make this more than a folder convention. It's portable —
plain markdown and YAML in a git repo, readable with no framework installed —
and the framework reads the shape, not the role. The dashboard knows what a
memory entry looks like, what an escalation's frontmatter holds, what keys live
in autonomy.yaml. It does not know or care what Sam specifically is.
Key insight: the dashboard parses the convention, not the role. Adding a new role — a recruiter, a support-triage agent, a research analyst — takes zero framework code.
Autonomy: differentiated, not graduated
This is the opinionated core. The obvious way to model autonomy is a single dial
— autonomy: 0–10, turn it up as trust grows. Praxis rejects that. Different
surfaces carry different blast radii, so each surface gets its own mode:
| Mode | What the role may do | Typical surface |
|---|---|---|
full |
Anything — the surface is its own work | memory/, escalations/, verbs/proposed/ |
append-only |
Add new list entries, never edit/delete; caps at max_pending |
discovered heuristics |
inline-enrichment |
Update declared soft fields on existing entries only | a notes column on team.yaml |
bounded |
Move a parameter inside an operator-set {min, max, step} |
daily send rate |
gated |
Nothing, ever — autonomously off-limits | persona.md, compliance, the constitution |
The entire policy lives in one operator-authored file — which the role is forbidden to edit, because editing it would let the role widen its own autonomy:
# lib/autonomy.yaml
surfaces:
- path: lib/research-strategies.yaml
mode: append-only
max_pending: 5
root_key: strategies
unique_by: id
- path: lib/warmup.yaml
mode: bounded
bounds:
sends_per_day: { min: 10, max: 100, step: 5 }
Key insight: gate by what could go wrong, not by what feels important. A bad entry in a heuristics list costs you one missed page — recoverable. An edit to the compliance list could end the project. Autonomy should track risk, not importance.
Escalations: a typed channel for "I'm stuck"
In the last post, the moment I trusted most was Sam stopping mid-task after two cold emails bounced, working out why the contact data was stale, and filing a proposal to fix it. That wasn't improvised — escalation is a first-class, typed surface, with four kinds:
| Kind | Meaning | Blocking? |
|---|---|---|
help |
Stuck now; can't continue without input | Yes |
improvement |
Noticed friction or a gap | No |
proposed_skill |
Drafted a new verb; asking to promote it | No |
criterion_drift |
A declared success criterion has trended off-green | No |
Each is a markdown file with YAML frontmatter, moved through a lifecycle from the operator's triage queue. The human-in-the-loop isn't a guardrail bolted on the side — it is the data model. The role proposes; the operator disposes; the whole exchange lives in the file.
An audit table → just git
There is no separate audit database. Every mutation is a git commit, and Praxis uses two synthetic authors so the history stays greppable:
$ git log --author="Praxis Role" --oneline
a1c3f9d role(memory): note person-calibration for finance team
7e20b41 role(escalation): file improvement — delivery-friction
3b8d5e2 role(lib): adjust warmup:sends_per_day
Autonomous edits are attributed to Praxis Role <role@praxis.local>; operator
actions carry the operator's own identity. So git log --author="Praxis Role"
shows you everything the role did on its own, and the rollback is the command
you already know — git revert <sha>. Constitutional changes can still happen,
but only through a co-authoring flow: the model drafts the diff, the operator
reviews it, and the commit lands under the operator's name with a
Co-Authored-By: Praxis Role trailer. The role can ask to change its
constitution. It can't do it.
Key insight: because growth is partitioned into small, single-path commits, the worst-case autonomous mutation is one markdown entry on one known path — and undoing it is one command.
Swapping the brain
Because the role is files, the model underneath is replaceable. Praxis wires inference through a provider interface rather than a hard-coded SDK — it ships an Anthropic provider, and Sam runs on a Quant Cloud provider that translates the same request shapes to a different backend. Set one environment variable and the role runs on a different model, keeping every memory and every calibration, because none of it ever lived in the model.
Conclusion
Most agent tooling optimises the runtime — better prompts, bigger context, sharper models. Praxis optimises the artifact the runtime leaves behind. The core moves to remember:
- The role, not the agent → the durable unit is a directory you can read.
- Differentiated, not graduated → autonomy is matched to per-surface risk, not earned over time.
- Files, not weights → growth is markdown and YAML, legible and portable.
- Git, not a database → every change is an attributed, revertible commit.
I'll be honest about the boundary: a few weeks proves the architecture can hold, not that it will. Drift and operator fatigue play out over months, and the design makes growth visible and revertible — it doesn't make the operator vigilant. That's the next thing I'm watching.
The repo is at steveworley/praxis-framework. In the next post, we'll cross to the operator's side of the glass — the triage queue, co-authoring constitutional changes, and what reviewing a role actually feels like day to day. If you're building agents and you've felt the pull to hand them more autonomy over time, I'd like to know whether the per-surface model holds against your case, or breaks.