Architecture Deep Dive¶
This section is the internals reference for AgentECS. It is written for someone who needs to hold the whole runtime in their head again — a returning maintainer, a new contributor, or anyone about to change behaviour rather than use it.
It differs from the System section in intent. System documentation describes the model you program against. This section describes the machine that implements it, at the level of files, functions, and line numbers.
How to read this section¶
Read it in order the first time. Each page assumes the one before it.
-
The same system described four times, from the one-paragraph mental model down to the individual functions that carry each responsibility.
-
Seven end-to-end traces — a tick, a read, a write, a spawn, a merge, an access violation, a retry — each one following control flow across every layer.
-
What the runtime actually guarantees, and the list of things that are declared in the API but not yet wired to behaviour. Read this before trusting a docstring.
-
How the design arrived here, in five eras, with the architectural pressure behind each one.
Thirty-second refresher¶
A World owns a Storage backend and an ExecutionStrategy (the scheduler).
Calling world.tick() asks the scheduler for an execution plan, runs each group of
systems concurrently against a frozen view of storage, concatenates what they produced,
and writes the whole batch back at the group boundary.
Systems never touch storage. They receive a ScopedAccess — a view narrowed to the
component types they declared — and every mutation they make is appended to a private
SystemResult buffer as an ordered MutationOp. Reads consult that buffer first, so a
system sees its own writes but not its peers'.
flowchart LR
S["@system function"] -->|reads / writes| SA[ScopedAccess]
SA -->|buffers ops| SR[SystemResult]
SA -->|falls through to| ST[(Storage)]
SCH[Scheduler] -->|executes| S
SR -->|merged at group boundary| W[World.apply_result_async]
W -->|commits| ST
Three consequences follow from that picture, and most of the subtlety in the codebase comes from them:
- Reads return copies.
ScopedAccess.get()deep-copies everything on the way out, so mutating what you read changes nothing. You must write the value back. - Writes are ordered, not merged. Each op carries an
op_seq. Apply order is authoritative;Combinable.__combine__only folds repeated writes to the same(entity, type)key. - Parallel systems do not see each other. They compute from the same starting
state. Combining their independent full values at apply time is why
__combine__implementations need to be additive rather than assume they ran in sequence.
Where the code lives¶
| Package | Role | Stateful? |
|---|---|---|
models/ |
Identity, components, queries, system descriptors, results, settings | No |
protocols/ |
Storage, ExecutionStrategy, SystemExecutor, HistoryStore, adapter interfaces |
No |
functions/ |
Pure operations: normalization, combining, merging, plan building | No |
services/ |
registry, scheduler, storage/, world/ — one concern each |
Yes |
api/ |
@component, @system, and the World that supplies the defaults |
Registry only |
adapters/ |
Optional LLM and vector-store integrations | Boundary |
Tick history (HistoryStore, TickRecord) is protocol and model only — World does
not yet emit records.
Dependency direction is enforced by .importlinter and runs as task lint:imports.
See Development & Code Architecture for the
contracts themselves.
[ECS]: Entity Component System [LWW]: Last-Writer-Wins [REPL]: Read-Eval-Print Loop [PyO3]: Rust bindings for the Python interpreter