Three-Agent Architecture Pattern
Evolution from Two to Three Agents
The two-agent pattern works, but has limitations:
- Self-evaluation bias: Coders tend to rate their own work highly
- No quality gate: Features pass without rigorous testing
- Missing planning: Complex features need upfront design
Anthropic's three-agent harness addresses these:
The Planner Agent
The Planner converts vague requirements into detailed specifications:
Responsibilities
interface PlannerAgent {
// Convert requirements to detailed spec
createProductSpec(requirements: string): Promise<ProductSpec>;
// Break spec into implementable sprints
createSprintPlan(spec: ProductSpec): Promise<Sprint[]>;
// Define acceptance criteria
createSprintContract(sprint: Sprint): Promise<SprintContract>;
}
Product Specification
interface ProductSpec {
overview: {
name: string;
description: string;
targetUsers: string[];
coreValue: string;
};
technicalStack: {
frontend: string; // "React + Vite + TypeScript"
backend: string; // "FastAPI + SQLite"
styling: string; // "TailwindCSS"
testing: string; // "Playwright"
};
features: Feature[];
constraints: {
timeline: string;
complexity: 'low' | 'medium' | 'high';
qualityBar: string;
};
}
Sprint Contracts
Before implementation, Planner and Generator negotiate explicit agreements:
interface SprintContract {
sprintId: string;
feature: string;
// What "done" looks like
acceptanceCriteria: {
criterion: string;
testable: boolean;
verificationMethod: string;
}[];
// Testable behaviors
behaviors: {
given: string;
when: string;
then: string;
}[];
// What the evaluator will check
evaluationCriteria: {
dimension: string;
weight: number;
passingScore: number;
}[];
}
Example Sprint Contract:
{
"sprintId": "sprint-001",
"feature": "User Registration",
"acceptanceCriteria": [
{
"criterion": "User can submit registration form",
"testable": true,
"verificationMethod": "Playwright form submission test"
},
{
"criterion": "Validation errors display for invalid input",
"testable": true,
"verificationMethod": "Test with invalid email format"
},
{
"criterion": "Success message shows after registration",
"testable": true,
"verificationMethod": "Check for success toast/message"
}
],
"behaviors": [
{
"given": "User is on registration page",
"when": "User submits valid email and password",
"then": "Account is created and success message displays"
},
{
"given": "User is on registration page",
"when": "User submits invalid email format",
"then": "Validation error displays under email field"
}
],
"evaluationCriteria": [
{ "dimension": "functionality", "weight": 0.4, "passingScore": 0.8 },
{ "dimension": "design", "weight": 0.3, "passingScore": 0.7 },
{ "dimension": "code_quality", "weight": 0.3, "passingScore": 0.7 }
]
}
The Generator Agent
The Generator implements features based on sprint contracts:
Responsibilities
interface GeneratorAgent {
// Implement feature according to contract
implementFeature(contract: SprintContract): Promise<Implementation>;
// Self-evaluate before handoff
selfEvaluate(implementation: Implementation): Promise<SelfEvaluation>;
// Use git for version control
commitChanges(message: string): Promise<void>;
}
Implementation Pattern
async function generatorSession(contract: SprintContract) {
console.log(`šØ Generator: Starting ${contract.feature}`);
// 1. Read the sprint contract
const criteria = contract.acceptanceCriteria;
const behaviors = contract.behaviors;
// 2. Implement each acceptance criterion
for (const criterion of criteria) {
console.log(` š Implementing: ${criterion.criterion}`);
// Generate code
await implementCriterion(criterion);
// Test locally
const localTest = await runLocalTest(criterion);
if (!localTest.passed) {
console.log(` ā ļø Local test failed, fixing...`);
await fixImplementation(criterion, localTest.error);
}
// Commit incrementally
await bash(`git add .`);
await bash(`git commit -m "feat: ${criterion.criterion}"`);
}
// 3. Self-evaluate before handoff
const selfEval = await selfEvaluate(contract);
console.log(` š Self-evaluation: ${selfEval.score}/100`);
// 4. Create handoff document for evaluator
await writeFile('handoff-to-evaluator.json', JSON.stringify({
sprintId: contract.sprintId,
feature: contract.feature,
selfEvaluation: selfEval,
commitHash: await bash('git rev-parse HEAD'),
readyForEvaluation: true
}));
console.log(`ā
Generator: Handoff ready for Evaluator`);
}
The Evaluator Agent
The Evaluator provides rigorous, unbiased assessment:
GAN-Inspired Design
The evaluator is inspired by the discriminator in Generative Adversarial Networks:
Evaluation Criteria
Anthropic's evaluator uses four dimensions:
interface EvaluationCriteria {
designQuality: {
description: "Coherence, mood, identity";
rubric: {
excellent: "Cohesive design language, clear visual hierarchy";
good: "Consistent styling, minor inconsistencies";
poor: "Inconsistent, no clear design system";
};
weight: 0.25;
};
originality: {
description: "Custom decisions vs template defaults";
rubric: {
excellent: "Unique design choices, memorable";
good: "Some customization beyond defaults";
poor: "Generic, looks like unmodified template";
};
weight: 0.25;
};
craft: {
description: "Typography, spacing, color harmony";
rubric: {
excellent: "Polished details, pixel-perfect";
good: "Well-executed, minor issues";
poor: "Sloppy, inconsistent spacing/alignment";
};
weight: 0.25;
};
functionality: {
description: "Usability and task completion";
rubric: {
excellent: "All features work flawlessly";
good: "Core features work, minor bugs";
poor: "Critical features broken";
};
weight: 0.25;
};
}
Evaluator Implementation
async function evaluatorSession(handoff: HandoffDocument) {
console.log(`š Evaluator: Reviewing ${handoff.feature}`);
// 1. Load sprint contract
const contract = await loadSprintContract(handoff.sprintId);
// 2. Start the application
await bash('npm run dev &');
await sleep(3000); // Wait for server
// 3. Run Playwright tests for each behavior
const behaviorResults = [];
for (const behavior of contract.behaviors) {
const result = await runPlaywrightTest(behavior);
behaviorResults.push({
behavior: behavior,
passed: result.passed,
screenshot: result.screenshot,
error: result.error
});
}
// 4. Grade each dimension
const grades = {
designQuality: await gradeDesign(behaviorResults),
originality: await gradeOriginality(behaviorResults),
craft: await gradeCraft(behaviorResults),
functionality: await gradeFunctionality(behaviorResults)
};
// 5. Calculate weighted score
const finalScore = calculateWeightedScore(grades, contract.evaluationCriteria);
// 6. Generate feedback
const feedback = await generateDetailedFeedback(grades, behaviorResults);
// 7. Make pass/fail decision
const passed = finalScore >= contract.evaluationCriteria
.reduce((sum, c) => sum + c.passingScore * c.weight, 0);
// 8. Write evaluation report
await writeFile('evaluation-report.json', JSON.stringify({
sprintId: handoff.sprintId,
feature: handoff.feature,
grades,
finalScore,
passed,
feedback,
behaviorResults
}));
console.log(`š Evaluator: Score ${finalScore}/100 - ${passed ? 'PASSED' : 'FAILED'}`);
if (!passed) {
console.log(`š Feedback for Generator:`);
feedback.improvements.forEach(i => console.log(` - ${i}`));
}
return { passed, feedback };
}
The Feedback Loop
When the evaluator rejects work, the generator iterates:
async function runThreeAgentHarness(requirements: string) {
// Phase 1: Planning
const planner = new PlannerAgent();
const spec = await planner.createProductSpec(requirements);
const sprints = await planner.createSprintPlan(spec);
// Phase 2 & 3: Generation + Evaluation loop
for (const sprint of sprints) {
const contract = await planner.createSprintContract(sprint);
let passed = false;
let attempts = 0;
const maxAttempts = 3;
while (!passed && attempts < maxAttempts) {
attempts++;
console.log(`\nš Sprint ${sprint.id} - Attempt ${attempts}/${maxAttempts}`);
// Generator implements
const generator = new GeneratorAgent();
await generator.implementFeature(contract);
// Evaluator grades
const evaluator = new EvaluatorAgent();
const result = await evaluator.evaluate(contract);
passed = result.passed;
if (!passed && attempts < maxAttempts) {
// Feed feedback back to generator
await generator.incorporateFeedback(result.feedback);
}
}
if (!passed) {
console.log(`ā Sprint ${sprint.id} failed after ${maxAttempts} attempts`);
// Human intervention needed
}
}
}
When to Use Three Agents vs Two
| Scenario | Recommended Pattern |
|---|---|
| Simple CRUD app | Two-agent |
| Complex UI with design requirements | Three-agent |
| Backend API only | Two-agent |
| Full-stack with quality bar | Three-agent |
| Prototype/MVP | Two-agent |
| Production-quality product | Three-agent |
E-Commerce Three-Agent Example
ecommerce-three-agent/
āāā agents/
ā āāā planner.ts # Converts requirements to specs
ā āāā generator.ts # Implements features
ā āāā evaluator.ts # Tests and grades
āāā contracts/
ā āāā auth-sprint.json
ā āāā products-sprint.json
ā āāā cart-sprint.json
ā āāā checkout-sprint.json
āāā evaluations/
ā āāā [generated reports]
āāā harness.ts
Key Takeaways
- Three agents separate concerns - Planning, Implementation, Evaluation
- Sprint contracts define "done" - Explicit acceptance criteria
- GAN-inspired feedback - Evaluator drives quality improvement
- Self-evaluation is biased - Separate evaluator catches issues
- Iteration improves output - Multiple attempts with feedback
Practice Exercise
Design a three-agent harness for a dashboard application:
- Create a product spec with Planner output format
- Design sprint contracts for 3 features
- Define evaluation criteria specific to dashboards
- Write the feedback loop logic
Next Lesson
Now that we understand agent architectures, let's dive into Session Management & State Persistence - the techniques that enable work across multiple context windows.