
The moment you go from one agent to several working on the same codebase or the same production system over days and weeks, the bottleneck moves

Most tutorials teach you to build one agent: wire a model to some tools, write a system prompt, maybe add retrieval over your docs. That is a good place to start, and for plenty of use cases it is also a fine place to stop.
Scaling is a different skill. The moment you go from one agent to several working on the same codebase or the same production system over days and weeks, the bottleneck moves. It stops being the model's reasoning and becomes the system's ability to accumulate knowledge, share it between agents, and avoid repeating mistakes. That is an architecture problem, and it is worth learning to reason about directly.

A useful way to design that architecture is to borrow the structure of a mind. A competent operator, human or agent, does five things:
A single-agent demo can fake most of these with a big context window. A fleet cannot, so you have to build them on purpose.
To keep this concrete, we will build one example the whole way through: a small crew that ships code. Three agents work a shared service called checkout-service. An investigator looks into problems, a planner decides what to change, and an executor runs the deploy. They run again and again, day after day, on the same system. That repetition is what makes the five faculties matter, and it is where a single-agent design starts to creak.
We will orchestrate the crew with CrewAI and use SenseLab's cognitive layer for the parts an orchestrator does not give you. The same wiring works with LangGraph and Strands, shown near the end.
Everything the crew learns lives in one store, keyed by the service it concerns. Each agent gets its own identity so the store always knows who wrote what, but they read and write the same entities.
from amfs import AgentMemory
investigator = AgentMemory(agent_id="incident-investigator")
planner = AgentMemory(agent_id="deploy-planner")
executor = AgentMemory(agent_id="deploy-executor")
With that in place, the five faculties are things you build on top of shared, attributed memory.
The first job is turning what agents do and see into durable records. The obvious approach is to give the model a save_memory tool and let it decide what is worth keeping. At small scale this works. At scale it fails in two predictable ways: the model forgets to call it, and when it does, what it saves is inconsistent from one run to the next.
The lesson is that capture should be a property of the system, not a judgment the model makes on every turn. Instrument the boundaries where information actually enters: the agent's tool calls and their results, plus external events like alerts or CI runs.
In our crew, the investigator consults PagerDuty and records what it found, so the observation enters the shared causal chain instead of vanishing with the chat transcript.
incidents = pagerduty.list_incidents(service="checkout-service")
investigator.record_context(
"pagerduty-incidents",
f"{len(incidents)} active SEV-1s on checkout-service in the last hour",
source="PagerDuty API",
)
For events that arrive on their own, SenseLab connectors push external signals straight into memory with no agent in the loop, so an incident or a CI failure lands as a record whether or not anyone was watching.
As soon as you capture well, you have too much to use. Context windows are finite and every token costs latency and money, so loading the whole store on each turn is not an option. You have to select.
Plain similarity search is the usual first move, and its weakness is well known: it returns what is textually similar to the query, which is not the same as what is useful for the task. Two refinements help. Rank on more than similarity, so recency, reliability, and how often the fleet leans on an entry all count. And precompute: for a service an agent works often, a compiled summary loaded at the start of a task, a briefing, is cheaper and more coherent than querying piecemeal.
When the planner picks up the investigator's work, it starts from a briefing and then pulls only the high-confidence specifics it needs.
digests = planner.briefing(entity_path="checkout-service", limit=5)
configs = planner.search(
entity_path="checkout-service",
min_confidence=0.8,
sort_by="confidence",
limit=10,
)
SenseLab also tiers storage by how hot it is, and on the Pro tier it can learn the ranking from which memories tended to precede good deploys. That is an optimization to reach for once you have outcome history, not a starting point.
When you decide what to keep, the shape of storage matters more than it did with one agent.
Start from the fact that not all knowledge is equal. A verified config value and a working hypothesis are both worth storing, but they should not be treated the same. Typing entries, say as facts versus beliefs, lets you age them differently, so a hunch loses weight faster than a confirmed fact when nothing reinforces it. That deliberate forgetting keeps the store from turning into a landfill.
The planner writes both kinds as it reasons about the fix:
from amfs import MemoryType
planner.write(
"checkout-service", "retry-pattern",
{"max_retries": 3, "backoff": "exponential"},
confidence=1.0,
)
planner.write(
"checkout-service", "suspected-timeout-cause",
"p99 spikes track cold Redis connections",
confidence=0.5,
memory_type=MemoryType.BELIEF,
)
Knowledge also changes, and in a fleet more than one agent may change the same key at once. SenseLab uses copy-on-write versioning so old versions survive and a write that would clobber someone's newer value is detected, and it keeps writes synchronous so that when the planner writes a plan and hands off to the executor, the executor actually reads it. An eventually consistent store would let that handoff read stale data intermittently, which is a miserable class of bug to chase.
Several agents writing to one store share storage. They do not automatically share understanding, and the gap between those two shows up as coordination bugs.
The first issue is provenance. When anything can write, you need to know who wrote what, and when one agent uses another's knowledge you want that transfer on the record, or a wrong decision that cascades through the crew is impossible to trace back. So the executor reads the planner's work explicitly, and the transfer is logged.
plan = executor.read_from("deploy-planner", "checkout-service", "retry-pattern")
print("using plan from", plan.provenance.agent_id)
The second issue is vocabulary. If the investigator files things under "incident" and the planner reasons about "outage," they miss each other even with flawless storage. At small scale you paper over this by convention. As the fleet grows, converge on a shared schema so the same concept resolves to the same thing across agents. That is more a modeling exercise than a coding one, and it pays off in fewer silent misses. SenseLab carries author attribution on every entry, tracks cross-agent reads, and offers shared rooms where agents can discuss and negotiate.
The last faculty separates a memory system from a learning one. A filing cabinet remembers. A mind revises what it believes based on what happened.
The mechanism is a feedback loop. Record which pieces of knowledge informed a decision, and when the outcome is known, push that signal back onto exactly those pieces. Knowledge that keeps preceding good deploys earns trust; knowledge tied to failures gets flagged. None of this touches the model's weights. It is the surrounding system learning, which is where most of the practical improvement in a fleet comes from.
When the executor ships, it closes the loop.

from amfs import OutcomeType
executor.read("checkout-service", "retry-pattern") # reads are tracked
# ... the executor runs the deploy and watches the rollout ...
executor.commit_outcome(
"DEP-446",
OutcomeType.SUCCESS,
decision_summary="Shipped with exponential backoff from retry-pattern",
)
Committing the outcome back-propagates a confidence update to the entries the executor read and snapshots the decision trace, so you can debug why a deploy went the way it did, and, later, a corpus of those traces is close to the labeled data you would want if you ever fine-tune.
The primitives above are framework-agnostic. To actually run the crew, hand the same store to your orchestrator. With CrewAI, SenseLab drops in as the storage backend, so every memory operation the crew performs flows through the versioned, attributed store, and you record the outcome once the run finishes.

from crewai import Agent, Crew, Task, Process
from crewai.memory import Memory
from amfs.integrations.crewai import AMFSStorageBackend
from amfs import OutcomeType
backend = AMFSStorageBackend(agent_id="deploy-crew", entity_path="checkout-service")
investigator = Agent(
role="Incident Investigator",
goal="Find the root cause of checkout-service incidents",
backstory="On-call SRE who has seen this service break before.",
)
planner = Agent(
role="Deploy Planner",
goal="Decide the safest change to ship",
backstory="Release owner for checkout-service.",
)
executor = Agent(
role="Deploy Executor",
goal="Ship the change and watch the rollout",
backstory="Automation that drives the deploy pipeline.",
)
crew = Crew(
agents=[investigator, planner, executor],
tasks=[
Task(description="Investigate the latest incident and record findings", agent=investigator),
Task(description="Propose the safest deploy from findings and past patterns", agent=planner),
Task(description="Execute the deploy and monitor the rollout", agent=executor),
],
process=Process.sequential,
memory=Memory(storage=backend),
)
result = crew.kickoff()
# Close the loop so the next run starts from better-ranked memory.
backend.memory.commit_outcome("deploy-run-042", OutcomeType.SUCCESS)
Because the crew shares one backend, the planner's next run already starts from what the investigator found last time, ranked by how past deploys actually went.
The memory layer does not care which orchestrator you use. In LangGraph, the same store can hold graph state so a run survives restarts:
from amfs import AgentMemory
from amfs_langgraph import AMFSCheckpointer
mem = AgentMemory(agent_id="deploy-graph")
checkpointer = AMFSCheckpointer(mem)
# pass `checkpointer` to your compiled graph
In Strands, SenseLab attaches as a plugin that also traces every tool call into the causal chain automatically:
from strands import Agent
from amfs import AgentMemory
from amfs_strands import AMFSPlugin
mem = AgentMemory(agent_id="deploy-agent")
agent = Agent(plugins=[AMFSPlugin(mem)])
Whatever runs the agents, the five faculties live in the memory layer, so you can change orchestrators without relearning the service's history.
You do not need all five on day one, and building them at once is a good way to over-engineer a system before you understand how it is used. A sensible order for most teams:
Begin with capture and retrieval. Get observations into a shared, attributed store and get relevant slices back out. That alone removes most of the pain of agents relearning what a sibling already figured out.
Add structure next: type your entries, version them, and keep writes consistent once more than one agent writes concurrently.
Add the feedback loop when you have enough volume for outcomes to be informative. Grow into coordination features like a shared ontology as the fleet and its ambiguity grow.
The through-line is that scaling agents is less about a cleverer model and more about the system around it getting better at accumulating and sharing understanding over time. Design for the five faculties, roughly in that order, and the architecture tends to hold as you add agents.
SenseLab develops this layer and you can sign up and get started for free here
Free tier. No credit card. Connect in minutes.
