When agent ensembles operate as independent services on a network, they occasionally need to share state. The kitchen ensemble needs to know current inventory levels. The front desk needs to know room assignments. The maintenance team needs to know which equipment is out of service.
The question is not whether to share state -- it is how to share it without creating the coordination problems that shared mutable state always creates in distributed systems.
The consistency spectrum
Not all shared state needs the same consistency guarantees. Some observations:
- Inventory notes ("extra beer stocked for the weekend") are advisory. If two ensembles read slightly different versions, the consequence is minor. Eventual consistency is fine.
- Room assignments are exclusive. Two ensembles should not assign the same room to different guests. This needs stronger coordination -- distributed locks or optimistic locking.
- Configuration preferences ("kitchen closes at 11pm") are rarely updated and widely read. Eventual consistency with version tracking works well.
Forcing a single consistency model on all shared state is either too expensive (locking everything) or too weak (eventual consistency for exclusive resources). The useful design is per-scope consistency selection.
SharedMemory with configurable consistency
AgentEnsemble v3.0.0 introduces SharedMemory -- a wrapper around the existing MemoryStore that adds consistency-aware read/write semantics:
SharedMemory sharedMemory = SharedMemory.builder()
.store(MemoryStore.inMemory())
.consistency(Consistency.EVENTUAL)
.build();
// Store an entry
sharedMemory.store("inventory", MemoryEntry.builder()
.content("Wagyu beef: 12 portions remaining")
.storedAt(Instant.now())
.build());
// Retrieve entries
List<MemoryEntry> entries = sharedMemory.retrieve("inventory", "beef", 10);
The Consistency enum controls the coordination behavior:
| Model | Behavior | Use case |
|---|---|---|
EVENTUAL |
Last-write-wins, no coordination | Context, preferences, notes |
OPTIMISTIC |
Version-checked writes, retry on conflict | Counters, shared documents |
LOCKED |
Distributed lock before each read/write | Room assignments, exclusive resources |
The consistency model is set per SharedMemory instance, which means different scopes can use different models:
// Advisory inventory notes -- eventual consistency
SharedMemory inventory = SharedMemory.builder()
.store(MemoryStore.inMemory())
.consistency(Consistency.EVENTUAL)
.build();
// Room assignments -- distributed lock
SharedMemory rooms = SharedMemory.builder()
.store(MemoryStore.inMemory())
.consistency(Consistency.LOCKED)
.build();
How the consistency models work
Eventual consistency
The simplest model. Writes go directly to the backing store with no coordination. Reads return the latest local value. If two ensembles write to the same key concurrently, the last write wins.
This is appropriate for state that is informational rather than authoritative -- agent context, preferences, notes, status updates. The cost of a stale read is low.
Optimistic locking
Each entry carries a version number. Writes include the expected version. If the actual version differs (because another ensemble wrote since the last read), the write fails with a ConcurrentModificationException and the caller retries with the updated version.
// Read with version
VersionedEntry entry = sharedMemory.readVersioned("inventory", "beef-count");
// Modify and write back with expected version
sharedMemory.writeVersioned("inventory", "beef-count",
entry.withContent("11 portions"), entry.version());
This is appropriate for state that is updated frequently by multiple ensembles but where conflicts are rare. The optimistic assumption is that most writes will not conflict, so the overhead of locking is avoided in the common case.
Distributed locking
Each read or write acquires a distributed lock on the scope. Only one ensemble can read or write at a time. This provides the strongest consistency guarantee but the highest coordination cost.
This is appropriate for exclusive resources -- room assignments, equipment reservations, anything where concurrent access would cause correctness problems.
The lock implementation depends on the transport backing. With in-process transport, it is a ReentrantLock. With Kafka or Redis, it is a distributed lock (Redlock or similar).
Shared memory in the network configuration
Shared memory scopes are declared at the network level and automatically available to all ensembles in the network:
NetworkConfig config = NetworkConfig.builder()
.ensemble("kitchen", "ws://kitchen:7329/ws")
.ensemble("front-desk", "ws://front-desk:7329/ws")
.sharedMemory("inventory", SharedMemory.builder()
.store(MemoryStore.inMemory())
.consistency(Consistency.EVENTUAL)
.build())
.sharedMemory("rooms", SharedMemory.builder()
.store(MemoryStore.inMemory())
.consistency(Consistency.LOCKED)
.build())
.build();
Each scope has a name that ensembles use to access it. The kitchen writes to "inventory"; the front desk reads from it. The consistency model is transparent to the application code -- the SharedMemory API is the same regardless of the consistency level.
When shared memory helps
Shared memory is useful when ensembles need to communicate state that does not fit the request/response pattern. Some examples:
- Inventory tracking -- the kitchen updates inventory as it uses ingredients; room service reads inventory before making promises to guests
- Shift context -- the front desk shares current occupancy, VIP arrivals, and special events; other ensembles use this context to adjust their behavior
- Equipment status -- maintenance updates the status of equipment; the kitchen checks before starting tasks that require specific equipment
In each case, the state is written by one ensemble and read by others. The communication is asynchronous and does not require a direct request/response exchange.
When shared memory hurts
Shared memory is a distributed state coordination mechanism. It has the same problems as every distributed state coordination mechanism:
Consistency vs. availability tradeoff. Locked consistency blocks when the lock cannot be acquired (lock holder is down, network partition). Eventual consistency never blocks but may return stale data. There is no free lunch.
Debugging is harder. When an ensemble reads stale data, the bug manifests as incorrect behavior, not as an error message. Tracing why the kitchen thought there were 12 portions when there are actually 8 requires understanding the write/read timeline across multiple ensembles.
Scope proliferation. Without discipline, shared memory scopes multiply. Each scope is a coordination point that adds operational complexity. Prefer a small number of well-defined scopes over many narrow ones.
In-memory backing is not durable. The default MemoryStore.inMemory() loses data on process restart. For production, back it with a persistent store (Redis, a database, or a custom MemoryStore implementation).
The design principle
The useful insight is that shared state in agent networks is not monolithic. Different categories of state need different consistency guarantees, and forcing a single model is either too expensive or too weak.
Per-scope consistency selection lets you use eventual consistency for advisory state (low coordination cost, high availability), optimistic locking for frequently updated counters (low cost in the common case, retry on conflict), and distributed locks for exclusive resources (high coordination cost, strong guarantees).
The consistency model is a property of the data, not a property of the system. Choose it based on what happens when two ensembles access the same state concurrently.
Shared memory is part of AgentEnsemble. The shared memory guide covers the full API including consistency models and network configuration.
I'd be interested in how others handle shared state across agent systems -- whether you use explicit shared memory, pass context through task results, or avoid shared state entirely.
Top comments (0)