Where a Security Agent’s Memory Begins
Adapted from my course, Build Your Own Agentic Defense.
A model does not carry a memory from one call to the next. The conversation, the tool requests it made, and the results those tools returned are available to it on the next call only if we supply them again.
That makes continuity something we have to engineer into the harness. We decide what to capture, what to keep, and what to make available again. We are building a record of the model's work (including the communication between the model and the harness), along with the rules for storing and reusing that record.
As our agent grows more complex, those decisions can become more involved. But we can begin with a simple design blueprint made of three elements: state, the information the harness carries; stages, the steps that work with it; and transitions, which carry the work and its results from one stage to the next.
In this article, I want to show how those three elements work together through an elementary example. We'll follow how the harness records what happened, carries that information forward, and keeps earlier versions available for review. Then we'll look at what has to be added if the record needs to survive beyond a single run. This is the foundation we can build on as we add more steps, longer conversations and memory that lasts.
State
Throughout this article, we'll use one small agent. An analyst gives it an observation, something they noticed and want a second opinion on. The agent then does two pieces of work: first it records the observation, and then it sends the observation to a model and records the model's analysis. After that, it stops.
The second piece of work depends on the first: it can only send the observation to the model if the observation was recorded somewhere it can find it. That record, the information our harness keeps while the agent works, is called the state.
One complete pass of the agent, from receiving the observation to stopping, is called a run. In this example, the state is held only in the memory of the harness process. When the run finishes and that process exits, the state disappears with it. If we want to keep it beyond this run, we have to write it to storage, such as a file or a database, and provide a way to load it again when needed. That persistence requires additional engineering; keeping state in memory does not make it a lasting record.
What goes into the state is our choice, and it depends on what the agent needs to keep. For this agent, two lists are enough: one for the observations it receives, and one for the analyses the model produces. In TypeScript, we can describe that shape like this:
type PipelineState = {
inputs: { id: string; text: string; at: string }[];
analyses: { id: string; text: string; at: string }[];
};
Let's read it from the top. PipelineState has two fields, inputs and analyses. The [] after each one tells us the field is an array, a list of items. Every item in either list is a small record with three fields: an id that identifies it, the text itself, and a timestamp called at. So the whole state is just two lists of small records. When a run begins, the harness creates this object with both lists empty, and the lists fill up as the work goes on.
Stage
We have seen how the state holds a record of what happened during the run. Now let's look at the work that produces those records: preparing the analyst's observation, then obtaining the model's analysis. Each of these distinct units of work is called a stage. In code, we implement a stage as a function that reads the current state, performs its task, and returns a result for the harness to carry forward.
Our small agent has two stages, and they do different kinds of work. The first, addInput(), takes the text the analyst typed and turns it into a record by giving it an id and a timestamp. There is no model involved here at all; it is plain code. The second, addAnalysis(), takes that record, builds a request around it, sends the request to the model and wraps the answer in a record of its own.
What makes this useful is that the run treats both stages in exactly the same way. Each one reads the state and returns a result, whether its work is a few lines of plain code or a call to a model. That means we can mix ordinary code and model work freely in one run, and save the model for the steps that actually need its judgment.
Transition
A transition is how the run gets from one stage to the next. Each stage produces something, and that result has to end up in the state so the next stage can use it. In our agent the order is fixed: first we prepare the input, then we analyze it.
You might expect the transition to simply add each new record to the state we already have. We are going to do something slightly different. Each stage creates a new version of the state that includes its result, and leaves the version it was given exactly as it was. A value that never changes once it has been created is called immutable, so we say this agent uses immutable state.
Nothing enforces this for us, so it is a rule our code has to keep. The TypeScript type above doesn't stop anyone from editing the state in place. Copying only the outer object isn't enough either: if the new version still shares an array with the old one, and a stage pushes a record into that array, it has changed both versions at once. So each stage creates new arrays, or new records, for whatever it changes.
Follow One Run
Let's watch all of this happen in a real run. Imagine an analyst notices something odd: a build server opened outbound connections in the middle of the night, and nothing is scheduled to run on that host. They hand that observation to our agent, which will record it, ask the model for an assessment, and then stop. The whole pipeline is just two stages long:
START → addInput() → addAnalysis() → END
The figure below follows the state through that run.
Start with an empty state
When the run begins, the harness creates the first version of the state, STATE₀. Nothing has happened yet, so both lists are empty:
// STATE₀ — born at boot
{ inputs: [], analyses: [] }
Empty as it is, this is a real object, and it is the version the first stage will build on.
Add the observation
addInput() takes the analyst's text, gives it an id and a timestamp, and returns a new version of the state, STATE₁, with that record in its inputs list:
// STATE₁ — returned by addInput(); STATE₀ is unchanged
{
inputs: [
{
id: 'inp_001',
text: 'a build server opened outbound connections at 02:00 — nothing is scheduled for that host',
at: '2026-08-03T02:07:04Z'
}
],
analyses: []
}
The id and the timestamp might look like bookkeeping, but they are the start of an audit trail. Later, when we want to know which observation led to which analysis, and when, they are what let us answer. At this point STATE₁ holds one input and no analysis, and it is handed on to the model stage. STATE₀ is untouched; it still shows the empty starting point.
Add the assessment
addAnalysis() takes the observation from STATE₁ and places it in a request as the user message, alongside a fixed system prompt, then calls the model. When the answer comes back, the stage wraps it in a record with the same three fields and returns STATE₂:
// STATE₂ — returned by addAnalysis(); STATE₀ and STATE₁ are unchanged
{
inputs: [{ id: 'inp_001', text: 'a build server opened outbound connections…', at: '2026-08-03T02:07:04Z' }],
analyses: [
{
id: 'ana_001',
text: 'Consistent with scheduled automation rather than a person at a keyboard — worth establishing the cadence and destination.',
at: '2026-08-03T02:07:09Z'
}
]
}
Both lists now hold one record. The input's text is shortened here so it fits on the page; the stage itself keeps the full original. The analysis has its own id, the model's answer, and a timestamp five seconds after the input's, which is roughly how long the model took to respond. And because the harness kept each version as it went, three separate objects now exist side by side, STATE₀, STATE₁ and STATE₂, each one frozen at its moment in the run.
Before moving on, look closely at what the model said. It suggests scheduled automation, yet the analyst told us that nothing is scheduled on that host. Recording an assessment doesn't make it true. Because our state keeps the evidence and the interpretation in separate lists, nothing has been lost: a later step, or a person reviewing the run, can see the mismatch and look into it. Had the model's answer simply overwritten the observation, that contradiction would have disappeared with it.
Why Copy Instead of Edit?
The first time you see this pattern, all the copying can look wasteful. Why not keep a single object and push each new record into it, with something like state.inputs.push(...)? For a run this small, that would work. But making a new version at each step gives us two things that matter as soon as we need to understand what happened during a run, or to combine work that was done in more than one place.
Keep a view of earlier work
Suppose the assessment turns out to be wrong, and we want to know what the model had in front of it when it answered. Because we copied instead of editing, the answer is simple: STATE₁ is still there, exactly as it was when addAnalysis() received it. We can read it directly, rather than trying to piece it back together.
Now picture the other approach. If we had edited one object in place and saved a reference to it after each step, we would not have three moments in the run. We would have three references to the same object, and every one of them would show its latest contents.
Immutable state makes this history easy to keep, but keeping it is still a decision: a version we don't hold on to is gone. A design that edits in place can keep a history too, by recording snapshots or events explicitly. Either way, the history exists only because we chose to keep it.
Combine independent results
Now imagine two detection nodes working on the same evidence at the same time, one looking for beaconing and the other for unusual DNS activity. If both write into one shared object without any coordination, the second write can wipe out the first, and a finding simply disappears.
With copies, both nodes start from the same unchanged state, and each returns its own new version. Neither can disturb what the other is reading. The harness then brings the two results together according to a rule we choose, a merge policy. Copying doesn't decide how conflicting results should be combined, and a careless merge can still drop a finding, but it gives us one clear place where that decision gets made.
Define how each result changes state
In software engineering, a function that takes the current state and one new event, and returns the next state, is called a reducer. It separates two jobs that are easy to blur together: producing a result, such as an analysis, and recording that result in the state.
A till receipt is a good way to picture it. Each item you buy adds a line, and a new running total is printed underneath. The latest total tells you where you stand, while the tape above it shows how you got there. A reducer is the rule that works out each new total; keeping the earlier totals is what gives us the history.
The same discipline sits at the heart of Redux, a widely used library for managing state: the state is read-only, and changes are made by pure functions that calculate the next state from the previous one and an action. Our model call isn't pure: ask it the same question twice and you may get two different answers. But its result becomes an input to ordinary, predictable code that updates the record.
The 12-Factor Agents principles describe an agent in similar terms, as a stateless reducer. "Stateless" doesn't mean there is no state. It means the state is handed to the function explicitly, rather than hidden inside it. The application still has state to manage, and that state is exactly what we have been building.
Beyond One Run
We now have a way to carry information through a run, and to keep earlier versions of it as we go. Two more decisions determine whether any of that information is useful beyond this single pass.
Save what needs to survive
Our state lives in the memory of the harness process. When the run finishes and the process exits, the state goes with it. A snapshot held in memory is not the same thing as a saved record.
So when a run produces something worth keeping, such as a validated finding, the harness has to write it somewhere that outlives the process: a database, a graph store or a file. We can save snapshots for later review as well, but keeping a final finding and keeping the full history of how it was reached serve different purposes. What we keep, and in what form, depends on what the work that comes after will need.
Decide how work continues
Our pipeline handles one observation and then stops. If the analyst wants to reply, ask a follow-up question or add more evidence, we need an outer loop: something that accepts the next message, decides what to carry forward and starts the right work again. The state supplies the information that loop works with, but it doesn't create the loop itself.
That is the foundation: a state that holds what the run has produced, stages that read it and add to it, and transitions that carry each new version forward without changing the one before. Everything an agent remembers, within one run or across many, is built on decisions like these, and every one of those decisions is made in the harness, in code we write.
Start here
Take your first step to
mastering agents for defensive security
- $50 off With your code
Build Your Own Agentic Defense
$499$449 - Free guide PDF · 26 pages
Defense in the Age of Agents
- Newsletter Every two weeks
Every new article and video, in one email
Keep reading
- Next Inside the Skeleton of a Security Agent A model call returns text. The engineering that turns it into a useful security agent happens around it: inputs, tools, boundaries and validated outputs.
- Related Your Prompt Is Not a Security Boundary Instructions can guide a model. Real boundaries come from the checks, approvals and permissions that control what its tools can actually do.
- Related How an Agent Decides What to Do Next Think, act, observe: follow a tool request around the agent loop, and see how each result shapes the next question.
Ready to master agents for defensive security?
Start mastering agents for defensive security with my flagship, self-paced online course, designed specifically for defenders.
This lesson comes from Module 03, Agentic Foundation. From there, the course goes into Agent Data Interface, Tools, MCP, Skills, Initiation & Detection and more.
What’s included
- 127 lessons
- Hands-on labs
- Build It Yourself guides
- Lifetime course access
- Course updates included
- One-time purchase
References2
- 12-Factor Agents — Factor 12: Make your agent a stateless reducer. Dex Horthy / HumanLayer. The proposed framing of an agent as a reducer with explicit state. github.com ↗
- Three Principles. Redux documentation. Read-only state and pure functions that calculate the next state. redux.js.org ↗