Josefina Tersoli built a three-agent proposal pipeline for her Sydney agency and ran into the shared-state problem that every multi-agent system hits. Three agents (research, proposal, quality review) need to read and write the same client knowledge base without stepping on each other. The solution is not elegant. It is Markdown files on disk, a hand-rolled agent loop, and a tool that acts as the exit condition.
The code is at github.com/JTersoli/bigpickle-brain under MIT. It runs on Claude or local models through Ollama's Anthropic-compatible API. The interesting part is not the agents. It is how shared memory forces you to choose between coordination overhead and data corruption.
The Pipeline Shape
The system takes a brief about a prospect and produces a priced proposal. Three agents run in sequence:
- Research agent: scrapes the prospect's website and competitor sites named in the brief, writes a report with evidence
- Proposal agent: turns the report into a priced proposal using the agency's service catalog and price ranges
- Quality review: checks the proposal with code validation first, then a reviewer agent, sends it back for rewrite if it fails
All three read from and write to a shared memory layer: plain Markdown notes stored outside the repository. A human can open the folder in Obsidian or any editor, read what the agents wrote, and correct mistakes.
The loop is in src/loop.ts, about 200 lines on the Anthropic Messages API. Send system prompt, tools, and conversation. Run the tools the model calls. Feed results back. Repeat until an agent calls the exit tool.
Shared Memory as Markdown Files
The memory layer is a directory of .md files. Each client gets a note. Each service gets a note. Each competitor gets a note. Agents read files, append sections, update fields.
This is the simplest possible shared-state design. It is also the most fragile.
Race conditions: If two agents write to the same file at the same time, the second write clobbers the first. The pipeline runs agents in sequence, so this does not happen in practice. But if you parallelize research (scrape five competitors at once), you need file locking or a queue.
Memory boundaries: Nothing stops the research agent from writing to the pricing catalog or the proposal agent from editing competitor notes. The only boundary is the system prompt. If the model halts or ignores instructions, memory leaks across agent contexts.
Token budget: Every agent loads the entire memory folder into context. If you have 50 clients and 200 competitors, you hit token limits. The code does not chunk or filter. It assumes a small agency with a small knowledge base.
The Exit Tool Pattern
The loop runs until an agent calls a tool named complete_task. This is the exit condition. The tool does nothing except signal that the agent is done.
const tools = [
{
name: "complete_task",
description: "Call this when the task is finished and you have a final answer",
input_schema: {
type: "object",
properties: {
result: { type: "string", description: "The final result" }
},
required: ["result"]
}
},
// other tools: read_file, write_file, search_web, etc.
];
When the model calls complete_task, the loop breaks and returns the result. This is cleaner than parsing output for a stop phrase or counting turns. The model decides when it is done.
The problem: if the model never calls the exit tool, the loop runs forever. You need a turn limit or timeout. The code does not have one. In production, you add a max-turns guard and return a failure state.
Coordination Without Frameworks
The pipeline does not use LangChain, AutoGPT, or any orchestration framework. It is a TypeScript function that calls the Anthropic API in a loop. This is intentional.
Frameworks hide the mechanics. They give you abstractions like "agent" and "memory" and "tool" without showing you what those mean at the HTTP level. When you need to debug why an agent is stuck or why memory is inconsistent, you are reading framework source code instead of your own.
The hand-rolled loop is 200 lines. You can read it in ten minutes. You can see exactly when tools run, when memory is read, when the loop exits. You can add logging, tracing, or custom retry logic without fighting a framework's opinions.
The trade-off: you write boilerplate. You handle tool call parsing. You manage conversation history. You implement your own error recovery. For a three-agent pipeline, this is fine. For a 20-agent system, you want abstractions.
Memory Synchronization Table
| Strategy | Coordination Overhead | Data Loss Risk | Token Cost | Human Readability |
|---|---|---|---|---|
| Markdown files (current) | None (sequential agents) | High (parallel writes) | High (load all files) | High (plain text) |
| SQLite with row locks | Medium (lock contention) | Low (ACID guarantees) | Medium (query results only) | Low (SQL schema) |
| Redis with pub/sub | High (message passing) | Medium (network failures) | Low (key-value fetch) | Low (binary protocol) |
| Vector store with embeddings | Low (read-only queries) | None (append-only) | Variable (similarity search) | Medium (JSON metadata) |
The Markdown approach works because agents run in sequence. If you parallelize, you need locks. If you scale to hundreds of clients, you need a database. If you need semantic search, you need embeddings.
State Management Failure Modes
Stale reads: Agent A reads a client note, Agent B updates it, Agent A writes based on the old version. The update is lost. You need optimistic locking (version numbers) or pessimistic locking (file locks).
Partial writes: Agent writes half a note, crashes, leaves corrupted Markdown. Next agent reads garbage. You need atomic writes (write to temp file, rename) or a transactional store.
Context overflow: Memory folder grows, agents load everything, hit token limits, fail. You need memory pruning (delete old notes), chunking (load relevant notes only), or summarization (compress history).
Prompt injection via memory: A competitor's website contains text that looks like instructions. Research agent writes it to memory. Proposal agent reads it, interprets it as a command, ignores the real prompt. You need input sanitization or memory namespacing.
The Quality Review Loop
The quality agent runs two checks:
- Code validation: parse the proposal as structured data, check required fields, verify price calculations
- Reviewer agent: read the proposal, compare it to the brief and research, flag missing evidence or unsupported claims
If either check fails, the system sends the proposal back to the proposal agent with the failure reason. The proposal agent rewrites. The quality agent checks again. This loops until the proposal passes or hits a retry limit.
This is a simple feedback loop. It catches obvious errors (missing prices, wrong format) and some subtle ones (claims without evidence). It does not catch everything. A human still reviews the final output.
The retry limit is critical. Without it, two agents can argue forever. The proposal agent writes something, the reviewer rejects it, the proposal agent writes the same thing again, the reviewer rejects it again. You need a circuit breaker.
Observability Gaps
The code has no tracing, no metrics, no structured logging. You cannot answer:
- How many tool calls did each agent make?
- How long did each agent spend waiting on the API?
- Which memory files were read most often?
- How many proposals failed quality review on the first pass?
For a single-user agency tool, this is fine. For a production system, you need:
- OpenTelemetry spans around each agent turn
- Counters for tool calls, retries, failures
- Logs with correlation IDs to trace a proposal through the pipeline
- Dashboards showing agent latency, memory size, success rate
When to Use This Architecture
Good fit:
- Small team (1-10 people) with a small knowledge base (dozens of clients, not thousands)
- Sequential workflow where agents do not run in parallel
- Human-in-the-loop review where a person checks the output
- Markdown-native workflow where the team already uses Obsidian or similar
Bad fit:
- High-volume pipeline where agents must run in parallel
- Large knowledge base where loading all files into context is not feasible
- Autonomous operation where no human reviews the output
- Strict consistency requirements where data loss is unacceptable
Technical Verdict
This is a teaching implementation. It shows the shared-state problem clearly because it does not hide it behind abstractions. Three agents, one memory folder, no locks, no transactions, no chunking. It works for a small agency because the knowledge base is small and agents run in sequence.
If you parallelize agents, you need file locks or a database. If you scale the knowledge base, you need chunking or embeddings. If you remove the human review, you need better quality checks and retry limits.
The hand-rolled loop is the right choice for a small system. You see the mechanics. You control the flow. You add observability where you need it. For a larger system, you want a framework or at least a shared library that handles tool parsing, conversation management, and error recovery.
The exit tool pattern is useful. It is cleaner than parsing output or counting turns. But you still need a turn limit as a backstop.
Use this architecture when you are building a small multi-agent system and you want to understand the coordination problems before you add abstractions. Avoid it when you need parallel execution, large-scale memory, or strict consistency guarantees.
Top comments (0)