Event-sourced agent actions: why we log to postgres before the LLM even starts
Durability first: capture intent before inference
Event-sourced agent actions: why we log to postgres before the LLM even starts
When you run a swarm of autonomous agents, the hardest problem isn't the LLM calls. It's knowing what the hell happened. Agents make decisions, call tools, mutate state, and then the LLM context window is gone. If you don't record the intent before the action, you're flying blind.
That's why we log every agent action to Postgres before the LLM even starts. Not after. Before. This is the core of our event-sourced architecture for agent swarms. It's not glamorous, but it's the difference between a system you can debug and a system you can only pray over.
The problem with the typical agent loop
Most agent frameworks look like this:
- User sends a prompt.
- Agent calls the LLM with a system prompt and conversation history.
- LLM returns a response, possibly with tool calls.
- Agent executes those tool calls (e.g., updating a database, sending an email).
- The result is appended to the conversation history.
- Repeat until done.
That's fine for a demo. But in production, you have multiple agents running concurrently, each with its own context window, sharing state. If one agent fails mid-loop, you lose the entire chain of reasoning. The LLM's response is gone, the tool call result is gone, and you have no idea what the agent intended to do.
You could log after the fact, but that's like writing a journal after you've already made the mistake. You get the outcome, not the intent. And when something goes wrong, you need the intent.
Event sourcing: the basics
Event sourcing is a pattern where you store every state change as an immutable event. Instead of storing the current state, you store the sequence of events that led to that state. To reconstruct state, you replay the events.
We apply this to agent actions. Every action an agent takes—whether it's a tool call, a decision, or even a thought—is an event. We append that event to a Postgres table before the agent executes anything. If the agent crashes, we can replay from the last event and resume.
This isn't new. Financial systems have done this for decades. But most agent frameworks treat the LLM context as the source of truth, which is ephemeral and non-deterministic. We flip that: Postgres is the source of truth, and the LLM is just a compute step.
Why Postgres?
You could use Kafka, Redis, or a dedicated event store. We chose Postgres because it's already in our stack. Postgres 16 with logical replication, JSONB, and pgvector for embeddings. It's battle-tested, transactional, and supports complex queries. For event sourcing, we need atomic append and replay, which Postgres handles perfectly.
We also use pgbouncer for connection pooling, because a swarm of agents can generate a lot of connections. But the key is that Postgres gives us ACID guarantees. When we write an event, it's durable. No lost updates, no partial writes.
The event schema
Our event table is simple but effective:
CREATE TABLE agent_events (
id BIGSERIAL PRIMARY KEY,
agent_id UUID NOT NULL,
run_id UUID NOT NULL,
sequence BIGINT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (run_id, sequence)
);agent_id: which agent emitted the event.run_id: a unique identifier for a specific execution flow.sequence: a monotonically increasing number within a run, used for ordering.event_type: e.g.,llm_request,tool_call,tool_result,decision.payload: the full JSON payload, including the prompt, the response, the tool input, and the output.
We also have a runs table that stores metadata about each run, like the start time, the user, and the final status.
This schema is deliberately not normalized. We store the entire payload as JSONB because we don't know in advance what fields an agent might need. Postgres handles JSONB efficiently, and we can index on specific fields if needed.
Writing the event before the LLM
The critical part is the sequence of operations. For every LLM call, we do:
- Append an
llm_requestevent with the full prompt, the model name, temperature, and any other parameters. - Call the LLM API (e.g., via vLLM or llama.cpp for self-hosted, or an external API).
- Append an
llm_responseevent with the raw response, including tool calls. - For each tool call, append a
tool_callevent with the tool name and arguments. - Execute the tool (e.g., a SQL query, an HTTP request).
- Append a
tool_resultevent with the result.
Steps 1 and 3 are non-negotiable. We never skip them. Even if the LLM call fails, we have the request logged, so we can debug why.
Here's a pseudocode sketch:
async def run_agent(agent_id, run_id, prompt):
seq = 0
# Append initial request
await append_event(agent_id, run_id, seq, 'llm_request', {
'prompt': prompt,
'model': 'llama-3.1-70b',
'temperature': 0.7
})
seq += 1
while True:
response = await llm_call(prompt, history)
await append_event(agent_id, run_id, seq, 'llm_response', response)
seq += 1
if response.tool_calls:
for tool_call in response.tool_calls:
await append_event(agent_id, run_id, seq, 'tool_call', tool_call)
seq += 1
result = await execute_tool(tool_call)
await append_event(agent_id, run_id, seq, 'tool_result', result)
seq += 1
# Update context with result
else:
breakThis is simplified, but the pattern is clear. Every step is recorded.
Benefits of logging before the LLM
Durability
The biggest benefit is durability. If the process crashes, the events are already in Postgres. You can restart the agent with the same run_id, query the last event, and resume from there. No lost work.
Imagine an agent that's processing a financial transaction. It calls the LLM to decide whether to approve a loan. The LLM says approve, and the agent is about to execute the approval. If the server crashes between the LLM response and the tool execution, you have the llm_response event with the decision. You can replay and execute the approval. Without that log, you'd have to re-run the LLM, which might give a different answer.
Replayability
Event sourcing gives you the ability to replay an entire run. You can reconstruct the exact sequence of events that led to a particular outcome. This is invaluable for debugging.
When an agent does something unexpected, you can pull up the events and see exactly what the LLM saw, what it decided, and what it did. You can even replay the run with a different model or temperature to compare.
We use this for regression testing. When we update an agent's prompt or model, we replay historical runs to see if the behavior changes. This is only possible because we have the full event log.
Auditability
For compliance, especially under the EU AI Act, you need to be able to explain what an AI system did. Event sourcing gives you an immutable audit trail. Every action an agent takes is recorded, with timestamps and payloads.
You can answer questions like: "Why did the agent send that email?" or "What data did the agent access?" by querying the event log. This is a requirement for high-risk AI systems, and it's good practice for any production system.
Debugging
When something goes wrong, you don't have to guess. You can look at the events and see exactly where the failure occurred. Was it the LLM returning a malformed response? Was it a tool that timed out? Was it a logic error in the agent's code?
We've had cases where an agent went into an infinite loop. With the event log, we saw that it was repeatedly calling the same tool with the same arguments. The issue was a missing guard in the agent's logic. The event log made it obvious.
Performance considerations
You might worry that writing to Postgres before every LLM call adds latency. In our experience, the overhead is minimal. A single INSERT into Postgres is sub-millisecond on a local network. The LLM call itself takes seconds. So the overhead is negligible.
But you need to be careful with connection management. A swarm of agents can generate hundreds of events per second. Use pgbouncer in transaction mode to pool connections. And consider batching events if you're writing at high volume, though we've found that individual inserts are fine for most workloads.
If you're really concerned about performance, you can write events asynchronously, but we prefer synchronous writes for durability. The trade-off is worth it.
Handling failures
What if the event write fails? Then you don't proceed. The agent stops. That's the correct behavior. If you can't record what you're about to do, you shouldn't do it.
We treat event writes as a precondition for any action. If the write fails, we raise an exception and the agent enters a failed state. This ensures that every action is recorded.
Replaying and resuming
To resume a run after a crash, you need a way to replay events and reconstruct the agent's state. We do this by having the agent's context be derived from the events.
Instead of storing a conversation history in memory, we reconstruct it from the llm_request and llm_response events. The agent loads all events for a run_id, builds the context, and continues from the last sequence number.
This is a bit more work than just keeping a list in memory, but it makes the system stateless. You can run the agent on any machine, and it will pick up where it left off.
Real-world example: our customer support agent
We have a customer support agent that handles email triage. It reads an email, decides on a category, and drafts a response. It uses a tool to look up the customer's order history.
Before we implemented event sourcing, if the agent crashed, we'd lose the entire interaction. The customer would get a duplicate email, or no response at all.
Now, every step is logged. If the agent crashes after looking up the order history, we can resume with the context already built. The customer gets a coherent response.
We also use the event log to analyze agent performance. We can see how often the agent requests the order history tool, and whether it's making good decisions. This data is gold for improving prompts.
Alternatives to Postgres
We've considered other options:
- Kafka: Great for high-throughput streaming, but adds operational complexity. For our volume, Postgres is simpler.
- Redis: Fast, but not durable by default. You'd need to configure AOF persistence, and it's still not as reliable as Postgres for long-term storage.
- Dedicated event stores like EventStoreDB: Purpose-built, but another system to run. We prefer to keep our stack lean.
Postgres is the right choice for most teams. It's already in your stack, it's reliable, and it's powerful enough.
Conclusion
Logging agent actions to Postgres before the LLM runs is a simple pattern that pays off enormously. It gives you durability, replayability, auditability, and easier debugging. It's the difference between a toy agent and a production system.
Next time you build an agent, start with the event log. Write the intent before the action. You'll thank yourself later.
This is the first in a series on building robust agent swarms. Stay tuned for more.