Home/
Part XIII — Expert Mode: Systems, Agents, and Automation/38. Building a Code-Change Agent Safely/38.2 The "diff generation" agent pattern
38.2 The "diff generation" agent pattern
Overview and links for this section of the guide.
On this page
The Concept
The diff generation agent produces actual code changes, but never applies them directly. It outputs a structured diff that humans review and apply.
Agent Input: Approved plan for "Fix session timeout"
Agent Output:
FILE: src/auth/session.ts
<<<<<<< SEARCH
const timeout = 3600000;
=======
const timeout = config.session.timeout ?? 3600000;
>>>>>>> REPLACE
Diff Format
We use Search/Replace blocks instead of unified diffs because LLMs can't reliably track line numbers:
// search-replace-format.ts
interface DiffBlock {
file: string;
blocks: Array<{
search: string; // Exact text to find
replace: string; // Replacement text
}>;
}
// Why this format works:
// ✅ No line numbers to get wrong
// ✅ Unambiguous matching
// ✅ Easy to validate before applying
// ✅ Human readable
// Example
const diff: DiffBlock = {
file: "src/auth/session.ts",
blocks: [
{
search: `const timeout = 3600000;`,
replace: `const timeout = config.session.timeout ?? 3600000;`
},
{
search: `import { db } from './db';`,
replace: `import { db } from './db';\nimport { config } from '../config';`
}
]
};
Implementation
// diff-agent.ts
const DIFF_GENERATION_PROMPT = `You are a code modification agent.
Given an approved plan, generate precise code changes.
Rules:
1. Use Search/Replace format ONLY
2. Search text must be EXACT (including whitespace)
3. Provide ONLY the minimal change needed
4. Include imports if new dependencies are added
5. Preserve existing code style
Output format for each change:
FILE: path/to/file.ts
<<<<<<< SEARCH
exact existing code
=======
replacement code
>>>>>>> REPLACE
Generate all changes now:`;
export class DiffGenerationAgent {
async generateDiff(plan: ApprovedPlan): Promise {
// First, read all relevant files
const fileContents = await Promise.all(
plan.affectedFiles.map(f => this.readFile(f.path))
);
const context = plan.affectedFiles.map((f, i) =>
`FILE: ${f.path}\n\`\`\`\n${fileContents[i]}\n\`\`\``
).join('\n\n');
const response = await this.model.generateContent({
systemInstruction: DIFF_GENERATION_PROMPT,
contents: [{
role: 'user',
parts: [{
text: `PLAN:\n${JSON.stringify(plan)}\n\nCURRENT FILES:\n${context}`
}]
}]
});
return this.parseDiff(response.response.text());
}
private parseDiff(output: string): DiffBlock[] {
const blocks: DiffBlock[] = [];
const filePattern = /FILE: (.+?)\n/g;
const diffPattern = /<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/g;
// Split by FILE markers
const sections = output.split(/FILE: /);
for (const section of sections) {
if (!section.trim()) continue;
const lines = section.split('\n');
const file = lines[0].trim();
const content = lines.slice(1).join('\n');
const diffBlocks: Array<{search: string; replace: string}> = [];
let match;
while ((match = diffPattern.exec(content)) !== null) {
diffBlocks.push({
search: match[1],
replace: match[2]
});
}
if (diffBlocks.length > 0) {
blocks.push({ file, blocks: diffBlocks });
}
}
return blocks;
}
}
Applying Diffs
// diff-applier.ts
interface ApplyResult {
success: boolean;
file: string;
error?: string;
backup?: string;
}
export class DiffApplier {
async apply(
diffs: DiffBlock[],
options: { dryRun?: boolean; createBackup?: boolean } = {}
): Promise {
const results: ApplyResult[] = [];
for (const diff of diffs) {
const result = await this.applyToFile(diff, options);
results.push(result);
if (!result.success) {
// Stop on first failure
break;
}
}
return results;
}
private async applyToFile(
diff: DiffBlock,
options: { dryRun?: boolean; createBackup?: boolean }
): Promise {
try {
let content = await fs.readFile(diff.file, 'utf8');
const original = content;
for (const block of diff.blocks) {
// Verify search text exists
if (!content.includes(block.search)) {
return {
success: false,
file: diff.file,
error: `Search text not found: "${block.search.slice(0, 50)}..."`
};
}
// Check for ambiguous matches
const occurrences = content.split(block.search).length - 1;
if (occurrences > 1) {
return {
success: false,
file: diff.file,
error: `Ambiguous: found ${occurrences} matches`
};
}
// Apply replacement
content = content.replace(block.search, block.replace);
}
if (options.dryRun) {
console.log(`[DRY RUN] Would modify: ${diff.file}`);
return { success: true, file: diff.file };
}
// Create backup
if (options.createBackup) {
await fs.writeFile(`${diff.file}.backup`, original);
}
// Write changes
await fs.writeFile(diff.file, content);
return { success: true, file: diff.file, backup: options.createBackup ? `${diff.file}.backup` : undefined };
} catch (e) {
return { success: false, file: diff.file, error: e.message };
}
}
}
Always Dry Run First
Run with dryRun: true before actually applying. This catches "search text not found" errors before
modifying any files.