File-Based Agent Communication
Why File-Based Communication?
When multiple agents need to coordinate, they have several options:
| Method | Pros | Cons |
|---|---|---|
| Direct API calls | Real-time, synchronous | Tight coupling, context limits |
| Message queues | Decoupled, scalable | Infrastructure complexity |
| File-based | Simple, debuggable, persistent | Slower, needs conflict handling |
Anthropic chose file-based communication because:
> "Agents communicate through structured files rather than direct API calls. One agent writes specifications, another reads and responds within or alongside that file, enabling clean context handoff."
The File Communication Pattern
Benefits:
- Agents don't need to run simultaneously
- Human can inspect/modify files
- Natural checkpointing
- Easy debugging
File Types in Agent Communication
1. Specification Files
Planner writes specs for Generator:
// specs/feature-001-spec.json
{
"specId": "feature-001",
"createdBy": "planner",
"createdAt": "2025-01-17T10:00:00Z",
"status": "pending_implementation",
"feature": {
"name": "User Registration",
"description": "Allow new users to create accounts",
"priority": 1
},
"requirements": [
"Email and password registration",
"Email validation format check",
"Password minimum 8 characters",
"Confirmation email sent"
],
"acceptanceCriteria": [
{
"id": "ac-001",
"criterion": "User can submit valid registration form",
"verified": false
},
{
"id": "ac-002",
"criterion": "Invalid email shows error message",
"verified": false
}
],
"technicalGuidance": {
"suggestedApproach": "Use react-hook-form for form handling",
"apiEndpoint": "POST /api/auth/register",
"databaseTable": "users"
}
}
2. Implementation Reports
Generator reports back to Evaluator:
// reports/feature-001-implementation.json
{
"reportId": "impl-001",
"specId": "feature-001",
"createdBy": "generator",
"createdAt": "2025-01-17T12:30:00Z",
"status": "ready_for_evaluation",
"implementation": {
"filesCreated": [
"src/components/RegisterForm.tsx",
"src/api/auth.ts",
"src/hooks/useRegistration.ts"
],
"filesModified": [
"src/App.tsx",
"src/routes/index.tsx"
],
"linesOfCode": 245
},
"selfEvaluation": {
"allCriteriaAddressed": true,
"confidenceScore": 0.85,
"knownIssues": [],
"testResults": {
"unitTests": "12/12 passing",
"typeCheck": "No errors"
}
},
"gitInfo": {
"branch": "feature/user-registration",
"commits": [
"abc123 - Add RegisterForm component",
"def456 - Add registration API",
"ghi789 - Connect form to API"
],
"headCommit": "ghi789"
},
"runInstructions": {
"startCommand": "npm run dev",
"testUrl": "http://localhost:5173/register"
}
}
3. Evaluation Results
Evaluator provides feedback:
// evaluations/feature-001-eval.json
{
"evalId": "eval-001",
"specId": "feature-001",
"implReportId": "impl-001",
"createdBy": "evaluator",
"createdAt": "2025-01-17T14:00:00Z",
"verdict": "needs_revision",
"criteriaResults": [
{
"criterionId": "ac-001",
"passed": true,
"evidence": "Screenshot showing successful form submission",
"screenshotPath": "screenshots/ac-001-pass.png"
},
{
"criterionId": "ac-002",
"passed": false,
"evidence": "Error message not displayed for 'invalid-email'",
"screenshotPath": "screenshots/ac-002-fail.png",
"failureDetails": "Form submits without showing validation error"
}
],
"scores": {
"functionality": 0.7,
"design": 0.8,
"codeQuality": 0.85,
"overall": 0.78
},
"feedback": {
"mustFix": [
"Add client-side email validation before form submission",
"Display error message under email field for invalid format"
],
"suggestions": [
"Consider adding password strength indicator",
"Add loading state to submit button"
],
"positives": [
"Clean component structure",
"Good use of TypeScript types",
"Proper error handling for API calls"
]
},
"nextAction": "return_to_generator"
}
4. Shared State Files
For coordination across all agents:
// state/project-state.json
{
"projectId": "ecommerce-001",
"lastUpdated": "2025-01-17T14:00:00Z",
"lastUpdatedBy": "evaluator",
"features": {
"feature-001": {
"name": "User Registration",
"status": "in_revision",
"attempts": 1,
"currentOwner": "generator"
},
"feature-002": {
"name": "User Login",
"status": "pending",
"attempts": 0,
"currentOwner": null
}
},
"pipeline": {
"planningComplete": true,
"currentPhase": "implementation",
"blockedFeatures": []
},
"metrics": {
"featuresCompleted": 0,
"featuresInProgress": 1,
"featuresPending": 5,
"totalAttempts": 1,
"averageAttemptsPerFeature": 1
}
}
Implementing File Communication
FileComm Protocol Class
interface FileCommMessage {
id: string;
type: 'spec' | 'implementation' | 'evaluation' | 'state';
from: 'planner' | 'generator' | 'evaluator';
to: 'planner' | 'generator' | 'evaluator' | 'all';
timestamp: string;
payload: any;
}
class FileCommProtocol {
private baseDir: string;
constructor(baseDir: string = './agent-comm') {
this.baseDir = baseDir;
}
// Send a message by writing to a file
async send(message: Omit<FileCommMessage, 'id' | 'timestamp'>): Promise<string> {
const fullMessage: FileCommMessage = {
...message,
id: generateId(),
timestamp: new Date().toISOString()
};
const dir = `${this.baseDir}/${message.type}s`;
const filename = `${fullMessage.id}.json`;
await ensureDir(dir);
await writeFile(`${dir}/${filename}`, JSON.stringify(fullMessage, null, 2));
// Also write to inbox of recipient
if (message.to !== 'all') {
const inboxDir = `${this.baseDir}/inbox/${message.to}`;
await ensureDir(inboxDir);
await writeFile(`${inboxDir}/${filename}`, JSON.stringify(fullMessage, null, 2));
}
return fullMessage.id;
}
// Receive messages from inbox
async receive(agent: string): Promise<FileCommMessage[]> {
const inboxDir = `${this.baseDir}/inbox/${agent}`;
try {
const files = await readDir(inboxDir);
const messages: FileCommMessage[] = [];
for (const file of files) {
if (file.endsWith('.json')) {
const content = await readFile(`${inboxDir}/${file}`);
messages.push(JSON.parse(content));
}
}
// Sort by timestamp
return messages.sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
} catch {
return [];
}
}
// Mark message as processed
async acknowledge(agent: string, messageId: string): Promise<void> {
const inboxPath = `${this.baseDir}/inbox/${agent}/${messageId}.json`;
const processedDir = `${this.baseDir}/processed/${agent}`;
await ensureDir(processedDir);
await moveFile(inboxPath, `${processedDir}/${messageId}.json`);
}
// Wait for a specific message type
async waitFor(
agent: string,
messageType: string,
timeout: number = 60000
): Promise<FileCommMessage | null> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const messages = await this.receive(agent);
const matching = messages.find(m => m.type === messageType);
if (matching) {
return matching;
}
await sleep(1000); // Poll every second
}
return null; // Timeout
}
}
Using the Protocol
// Planner sends spec to Generator
const comm = new FileCommProtocol();
// Planner
async function plannerSendSpec(feature: Feature) {
const specId = await comm.send({
type: 'spec',
from: 'planner',
to: 'generator',
payload: {
feature,
acceptanceCriteria: generateCriteria(feature),
technicalGuidance: generateGuidance(feature)
}
});
console.log(`📤 Planner: Sent spec ${specId}`);
}
// Generator
async function generatorProcessSpecs() {
const messages = await comm.receive('generator');
for (const msg of messages) {
if (msg.type === 'spec') {
console.log(`📥 Generator: Received spec for ${msg.payload.feature.name}`);
// Implement the feature
const implementation = await implement(msg.payload);
// Send implementation report to evaluator
await comm.send({
type: 'implementation',
from: 'generator',
to: 'evaluator',
payload: {
specId: msg.id,
...implementation
}
});
// Acknowledge the spec
await comm.acknowledge('generator', msg.id);
}
}
}
// Evaluator
async function evaluatorProcessImplementations() {
const messages = await comm.receive('evaluator');
for (const msg of messages) {
if (msg.type === 'implementation') {
console.log(`📥 Evaluator: Received impl for spec ${msg.payload.specId}`);
// Evaluate the implementation
const evaluation = await evaluate(msg.payload);
// Send evaluation back
const targetAgent = evaluation.passed ? 'all' : 'generator';
await comm.send({
type: 'evaluation',
from: 'evaluator',
to: targetAgent,
payload: evaluation
});
await comm.acknowledge('evaluator', msg.id);
}
}
}
Handling Conflicts
When multiple agents might write to the same file:
class ConflictSafeFileComm extends FileCommProtocol {
// Use file locking
async acquireLock(filePath: string, timeout: number = 5000): Promise<boolean> {
const lockPath = `${filePath}.lock`;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
// Try to create lock file exclusively
await writeFile(lockPath, JSON.stringify({
lockedBy: process.pid,
lockedAt: new Date().toISOString()
}), { flag: 'wx' }); // wx = exclusive create
return true;
} catch (e) {
// Lock exists, wait and retry
await sleep(100);
}
}
return false;
}
async releaseLock(filePath: string): Promise<void> {
const lockPath = `${filePath}.lock`;
await deleteFile(lockPath);
}
async safeWrite(filePath: string, content: string): Promise<boolean> {
const locked = await this.acquireLock(filePath);
if (!locked) {
console.log(`⚠️ Could not acquire lock for ${filePath}`);
return false;
}
try {
await writeFile(filePath, content);
return true;
} finally {
await this.releaseLock(filePath);
}
}
// Use versioning for shared state
async updateSharedState(
update: (state: ProjectState) => ProjectState
): Promise<boolean> {
const statePath = `${this.baseDir}/state/project-state.json`;
const locked = await this.acquireLock(statePath);
if (!locked) return false;
try {
// Read current state
const current = JSON.parse(await readFile(statePath));
// Apply update
const updated = update(current);
updated.version = (current.version || 0) + 1;
updated.lastUpdated = new Date().toISOString();
// Write back
await writeFile(statePath, JSON.stringify(updated, null, 2));
return true;
} finally {
await this.releaseLock(statePath);
}
}
}
Directory Structure
agent-comm/
├── specs/ # Planner → Generator
│ ├── spec-001.json
│ └── spec-002.json
├── implementations/ # Generator → Evaluator
│ ├── impl-001.json
│ └── impl-002.json
├── evaluations/ # Evaluator → Generator/All
│ └── eval-001.json
├── inbox/
│ ├── planner/
│ ├── generator/
│ └── evaluator/
├── processed/
│ ├── planner/
│ ├── generator/
│ └── evaluator/
├── state/
│ └── project-state.json
└── locks/
Key Takeaways
- Files as message queues - Agents write and read files instead of calling APIs
- Clear ownership - Each file type has a writer and reader
- Inbox pattern - Agents have inboxes for incoming messages
- Acknowledgments - Move processed messages out of inbox
- Locking prevents conflicts - Use file locks for shared state
- Human-debuggable - Can inspect and modify any file
Practice Exercise
Design a file-based communication system for a content management system:
- Define the message types (content spec, draft, review)
- Create the directory structure
- Implement the send/receive protocol
- Add conflict handling for simultaneous edits
Next Lesson
Now let's explore Testing with Browser Automation - how agents verify their work using Puppeteer and Playwright.