Building Anton · Chapter 5 of 14

Anton, chapter 5: Two primitives

March 23, 2026 · 4 min read

Two weeks earlier, LangGraph had been exactly the right choice.

It gave Anton structure, checkpointing, observability, and a clean way to organise each domain as a subgraph. It helped me move quickly while I was still discovering what the system wanted to be.

By Monday morning, it had become the system rather than a tool inside it.

Every agent was a LangGraph subgraph. The parent was a state machine. Conversation history used LangChain message types. The trace viewer parsed LangGraph checkpoints. Skills were wrapped as LangChain tools.

There were now three representations of the same architecture: the framework graph, Anton's own domain registry, and the dashboard view. They drifted, and I reconciled them by hand.

The framework had not become bad.

Its value had simply fallen below its friction.

That is the moment to remove an abstraction.

Skills and agents

I reduced Anton to two primitives.

A skill performs a bounded operation with a runtime contract.

An agent reasons, selects skills, and loops until it has completed the request.

Nothing else gets a special category.

No pseudo-agents. No classify-and-dispatch pipelines disguised as agents. No bespoke endpoints that bypass the common runtime.

When something does not need reasoning, it is a skill. When it does, it is an agent.

Anything that cannot fit either shape has to justify why.

The rule was austere, and that was the point.

The small runtime underneath

Then LangGraph came out.

I replaced it with runAgent(): a few hundred lines containing only what Anton actually needed.

An LLM call. Tool dispatch. A loop limit. Trace emission. Permission filtering. Output validation.

That was the runtime.

I could hold it in my head.

This did not mean LangGraph had been a mistake. Without it, I might not have learned which parts were essential. Frameworks are often most valuable while the problem is still being discovered.

But once the underlying shape becomes clear, carrying the discovery apparatus forever is optional.

The codebase changed vocabulary with the architecture. "Graph" became "agent" in package names, files, documentation, and the UI. A rule went into memory: no graph terminology.

Words that once described the implementation had become misleading descriptions of the system.

Uniform from the outside

Every agent now had the same shape: a thin function passing input and configuration to runAgent().

Every delegate did the same thing: call an agent and return its result.

Schedules, direct invocation, the parent, and the dashboard all saw one surface.

Uniformity from the outside made experimentation inside much cheaper.

Skill names were standardised to verb_entity: get_event, create_event, update_event, list_events.

Aliases and one-off variants were folded back into canonical skills. update_event_by_title, for example, became a supported path through update_event rather than a second tool the model had to distinguish.

The web domain disappeared entirely. It duplicated parts of documents and research, and without the graph architecture making every branch look like a domain, there was no reason to preserve it.

A coherent tool surface is easier for people to understand and easier for a model to use.

Replication exposes the next problem

With the runtime simplified, I built a replication engine.

The framing was a Von Neumann probe: clone the entire Anton stack onto another server with one command.

The mechanism was deliberately unromantic (rsync, Docker Compose, and a seed process) but it created three useful properties.

A household could run its own Anton. The Spark could fail without becoming irreplaceable. And I could create a disposable clone for changes that would otherwise make me nervous.

Replication immediately exposed the next weak point: secrets.

Vaultwarden authentication did not clone cleanly. A new instance came up without the credentials required to be useful, and fixing it required manual work on every machine.

A clone that needs hand repair is not really a clone.

I left the problem open, but now it was visible.

Transport is not intelligence

Telegram arrived the same week.

It used the same backend, agents, and skills as WhatsApp. The bridge merely translated incoming messages into jobs and outbound responses back into Telegram.

The agent runtime did not know or care which transport a message came from.

That small integration validated a decision from the first day: transport belongs at the edge.

A new way of speaking to Anton should not require a new Anton.

Memory must know who is asking

Two retrieval fixes closed the week.

Domain provenance began affecting ranking, so calendar queries preferred calendar-derived facts and media questions preferred media-derived facts.

More importantly, document facts stopped leaking between users.

A family assistant serves several people who share some context and keep other context private. Memory cannot treat "the household" as one undifferentiated user.

Knowing a fact and knowing who is allowed to recall it are part of the same operation.

By Thursday night, Anton had two primitives, one runtime, one naming convention, a replication path, and a second transport.

The codebase looked like what I had wanted two weeks earlier but could not yet have described.

That is one of the strange things about architecture: the clean version is often visible only after you have built the complicated one.

The week's lesson was not "avoid frameworks."

It was:

Keep an abstraction only while it pays more than it costs.

LangGraph had paid handsomely. Then it stopped. Removing it was not a rejection of the earlier decision; it was the consequence of what that decision had taught me.