🔥 0
0
Lesson 6 of 10 25 min +200 XP

File-Based Agent Communication

Why File-Based Communication?

When multiple agents need to coordinate, they have several options:

MethodProsCons
Direct API callsReal-time, synchronousTight coupling, context limits
Message queuesDecoupled, scalableInfrastructure complexity
File-basedSimple, debuggable, persistentSlower, 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

File-Based Communication 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.