Steve's thoughts and experiments
Build Roles, Not Agents image

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.