Every AI coding tool that touches your files eventually has to answer the same question: rewrite the whole file, or patch just the lines that changed. Cursor built its entire editing pipeline around that one decision, and the engineering behind it, known internally as Fast Apply and speculative edits, is one of the more quietly important pieces of infrastructure in modern software development. This tutorial walks through building a working version of that mechanism yourself: a standalone patch engine that chooses between a localized diff and a full-file rewrite, tracks the token cost of each path, and falls back gracefully when a patch fails to apply.
You will not be configuring Cursor itself here. Instead, you are building the underlying decision engine that any AI-assisted coding tool, extension, or CI bot needs if it calls an LLM to generate code changes and then has to get those changes safely onto disk. By the end, you will have a working Python project you can plug into your own tooling, point at any OpenAI- or Anthropic-compatible model, and benchmark against the published numbers for Cursor Composer 2.
Why “Diff vs Rewrite” Is the Core Decision Every AI Coding Tool Makes
When a large language model suggests a code change, something has to turn that suggestion into bytes on disk. There are really only three ways to do it. The model can emit the entire file again, with the edit folded in. It can emit a localized patch, usually a unified diff or a search-and-replace block, that only touches the changed lines. Or it can use a hybrid approach that treats the original file as a draft and verifies the parts that did not change instead of regenerating them from scratch. That third option is what Cursor calls speculative edits, and it is the mechanism behind the Fast Apply feature that ships inside Cursor Composer.
The trade-offs are not subtle once you measure them. A full-file rewrite re-emits every line of the file, including the 95% that did not change, which means output-token usage grows roughly linearly with file length. A localized diff only emits the changed hunk, so it stays cheap and fast regardless of file size, but it depends on the model producing line references or context lines that still match the file by the time the patch gets applied. Anish Gandhi’s breakdown of how AI coding tools edit code frames this as a two-model problem: one model reasons about what should change, and a second, often smaller and faster model handles the mechanical job of writing that change into the file.
That split matters because the two jobs have different failure modes. A reasoning model that is bad at precise line-number bookkeeping will produce diffs that almost apply, but not quite, because it miscounted a blank line or hallucinated a context string. A rewrite model does not have that problem since it regenerates the whole file, but it pays for that safety in tokens, latency, and dollars. Building your own patch engine means picking which failure mode you can tolerate for which kind of edit, and that is exactly the decision Cursor Composer 2 automates for you.
Inside Cursor Composer 2: How Fast Apply and Speculative Edits Work
Cursor Composer 2 is Cursor’s own agentic coding model, built for longer-horizon software engineering tasks: planning across files, running terminal commands, and iterating on a task without constant hand-holding. Cursor’s own technical material describes Composer 2 as trained through continued pretraining and large-scale reinforcement learning on top of a base checkpoint, with public reporting pointing to Moonshot AI’s Kimi K2.5 as that base. It runs with a 200,000-token context window. This is a different, newer generation from the original Composer model that shipped with Cursor 2.0, so if you are comparing notes with an older writeup, make sure it is talking about Composer 2 and not Composer 1.
The part that matters for this tutorial is how Cursor’s agent decides between a diff and a rewrite once it has a change to make. According to Cursor’s own published material, there is no single fixed mode. The agent’s edit path is a model decision shaped by the size of the change, the structure of the file, and how reliably a patch is expected to apply, rather than a user-facing toggle with a documented hard threshold. Cursor does not publish an exact rule such as “rewrite whenever more than X percent of a file changes.” What it does publish is the performance story behind the mechanism it uses once a diff-style edit is chosen: Fast Apply.
Speculative Edits vs Speculative Decoding
Fast Apply separates two jobs: a reasoning model describes the intended change in natural language or an abbreviated diff, and a specialized apply model turns that description into the final file. Cursor has described the apply model as a fine-tuned, large model derived from an open base, and reports throughput of roughly 1,000 tokens per second, which works out to somewhere around 3,500 to 4,000 characters per second under the conditions Cursor tested. That speed comes from treating the original file as a deterministic draft token stream, the same trick speculative decoding uses to skip ahead through tokens the model is highly confident about, then only spending real compute verifying the regions that actually changed instead of regenerating the whole file top to bottom.
Cursor describes the result as functionally equivalent to a full-file rewrite while running up to 9 times faster than ordinary full regeneration. A separate technical summary of the same architecture puts the speedup in the 9 to 13 times range, depending on whether you are comparing against an earlier Cursor deployment or against vanilla inference on the same base model. Either way, the headline point survives: you get the safety profile of a full rewrite, meaning the apply model sees the entire file and is not blindly trusting stale line numbers, without paying the full token bill for one.
That is the exact mechanism this tutorial has you build a simplified version of: a decision layer that picks the cheapest reliable path, a diff-apply step with real fallback behavior, and a rewrite path that only fires when the diff path is not worth the risk.
Diff vs Rewrite by the Numbers: Token Cost and Latency
Before writing any code, it helps to see why this decision is worth automating instead of hardcoding one path forever. The gap between a full rewrite and a localized diff is not a rounding error, it compounds with file size. Below roughly 400 lines, a full rewrite can actually have a higher success rate, because the model sees the whole file as context and avoids the fragile line-position matching that trips up diffs. Past that size, the economics flip hard in the diff’s favor, because rewrite output keeps growing with the file while diff size stays roughly flat relative to the size of the actual edit.
| File / Edit Scenario | Full Rewrite Tokens | Full Rewrite Time | Local Diff Tokens | Local Diff Risk |
|---|---|---|---|---|
| 400-line file, single function change | ~4,000-6,000 output tokens | ~5-6 seconds | ~200-500 tokens, under 1 second | ~20-30% chance the patch fails to apply cleanly |
| 2,000-line file, single function change | ~16,000-24,000 output tokens | Scales linearly with file size | ~200-500 tokens, under 1 second | Similar patch-failure risk, but now a much bigger relative win if it succeeds |
| Small file (under ~400 lines), broad refactor | Comparable or cheaper than multiple diff hunks | Short, bounded by file size | Multiple hunks increase cumulative failure risk | Rewrite often wins on reliability here |
| Large file (2,000+ lines), broad refactor | Very high token cost, scales with file size | Long, can hit latency budgets | Still bounded by edit size, not file size | Diff usually wins on cost even with retries factored in |
Those numbers come from a widely cited breakdown of why AI coding tools default to rewriting files rather than diffing them, and they line up with what you will see once you instrument your own engine in the steps below. The headline insight is that a 20 to 30 percent chance of a failed patch sounds bad in isolation, but once you account for the fact that a failed diff still costs a fraction of a full rewrite, and you can retry a diff multiple times before a single rewrite finishes, the math usually still favors diffing for anything past a few hundred lines. The job of your decision engine is to make that trade-off automatically instead of leaving a human to guess.
Prerequisites: Tools, Versions, and Accounts You’ll Need
This build uses Python because its standard library already ships a diffing engine, which keeps the dependency list short. You can port the same logic to TypeScript or Go later if your tool is built on one of those, the decision logic does not change.
- Python 3.11 or newer (3.12 recommended for the faster standard library string handling used in large-file tests)
- pip 23.0 or newer
- Git 2.40 or newer, used to validate patches with
git apply --checkas a sanity layer - An API key for an LLM provider. Any OpenAI-compatible or Anthropic-compatible chat completions endpoint works; the examples below use a generic client wrapper so you can swap providers
- The
unidiffPython package (0.7.5 or newer) for parsing unified diffs - The
tiktokenpackage (0.8.0 or newer) for token counting, or an equivalent tokenizer for your chosen model - Optional: a local install of Cursor (build v2026.10.08 or newer, per Cursor’s own release notes) if you want to run a side-by-side comparison against Composer 2’s real Fast Apply behavior
- About 90 minutes for the full build and benchmark run
Install the Python dependencies in a fresh virtual environment before you start:
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install unidiff==0.7.5 tiktoken==0.8.0 openai==1.54.0
If you plan to use an Anthropic-compatible model instead, swap the last package for anthropic==0.40.0 and adjust the client wrapper in Step 8 accordingly. The rest of the engine does not care which provider generated the edit, it only cares about the text that comes back.
Steps 1-4: Scaffolding the Project and Generating Diffs
Start with a clean project layout so the decision logic, the diff tooling, and the rewrite path stay in separate, testable modules. This mirrors how the research on AI coding agent architecture, including the case study on designing a coding agent in the style of Claude Code, GitHub Copilot, and Cursor, structures the apply layer as its own component rather than bolting it onto the model call.
- Step 1: Scaffold the project. Create a directory called
fast-apply-enginewith subfoldersengine/,tests/, andsamples/. Put an empty__init__.pyinengine/so it imports cleanly. - Step 2: Define the edit request format. Every edit your engine handles should arrive as a dataclass with the file path, the original content, and the model’s proposed change, whether that change is a full replacement, a unified diff, or a natural-language instruction waiting to be turned into one.
- Step 3: Build the diff generator. Use Python’s built-in
difflibto produce a unified diff between the original file and any candidate new version, so you can measure diff size even when the model gave you a full rewrite. - Step 4: Write a token counter. Wrap
tiktokenso every path through the engine, diff or rewrite, reports an exact token count instead of a guess. This is what lets you reproduce the cost table above with your own numbers later.
# engine/diffing.py
import difflib
def make_unified_diff(original: str, updated: str, filename: str = "file.py") -> str:
original_lines = original.splitlines(keepends=True)
updated_lines = updated.splitlines(keepends=True)
diff = difflib.unified_diff(
original_lines,
updated_lines,
fromfile=f"a/{filename}",
tofile=f"b/{filename}",
lineterm="",
)
return "n".join(diff)
def diff_hunk_count(unified_diff_text: str) -> int:
return sum(1 for line in unified_diff_text.splitlines() if line.startswith("@@"))
Keep this module dumb on purpose. It should never decide anything, it should only measure. The decision logic belongs in its own file, which is Step 5.
Steps 5-8: Patch Application, Rewrite Fallback, and the Decision Threshold
This is the heart of the engine: the logic that picks diff or rewrite, applies the chosen path, and falls back when the first attempt fails. The analysis on how coding agents edit files with diffs, snapshots, and fast apply makes the same point this section is built around: a patch applier that cannot fall back is a patch applier that will eventually corrupt a file silently, which is worse than failing loudly.
- Step 5: Set the line-count threshold. Start at 400 lines, the point where the research above shows rewrite reliability and diff economics cross over. Treat it as a starting default, not a law.
- Step 6: Add a change-ratio check. A 1,000-line file with a one-line fix should never trigger a rewrite just because it is long. Measure the estimated size of the edit against the file size, not file size alone.
- Step 7: Build the patch applier with fallback. Attempt the diff first when the decision engine says diff. If
git apply --checkfails, or the patch applies but the resulting file fails a syntax check, fall back to requesting a full rewrite instead of giving up. - Step 8: Wire in retry limits. Cap retries at two attempts per edit before escalating to a human-visible failure. Unlimited retries are how a $0.003 diff attempt turns into a $4 debugging session.
# engine/decision.py
from dataclasses import dataclass
@dataclass
class EditDecision:
mode: str # "diff" or "rewrite"
reason: str
def decide_edit_mode(
file_line_count: int,
estimated_changed_lines: int,
line_threshold: int = 400,
change_ratio_threshold: float = 0.5,
) -> EditDecision:
if file_line_count == 0:
return EditDecision("rewrite", "new or empty file")
change_ratio = estimated_changed_lines / max(file_line_count, 1)
if change_ratio >= change_ratio_threshold:
return EditDecision("rewrite", f"change ratio {change_ratio:.2f} exceeds threshold")
if file_line_count <= line_threshold:
return EditDecision("rewrite", f"file under {line_threshold} lines, rewrite is cheap and safer")
return EditDecision("diff", f"file over {line_threshold} lines with localized change")
# engine/apply.py
import subprocess
import tempfile
import os
class PatchApplyError(Exception):
pass
def apply_unified_diff(original_path: str, diff_text: str) -> bool:
with tempfile.NamedTemporaryFile(suffix=".patch", delete=False, mode="w") as tmp:
tmp.write(diff_text)
patch_path = tmp.name
try:
check = subprocess.run(
["git", "apply", "--check", patch_path],
cwd=os.path.dirname(original_path) or ".",
capture_output=True,
text=True,
)
if check.returncode != 0:
raise PatchApplyError(check.stderr.strip())
subprocess.run(
["git", "apply", patch_path],
cwd=os.path.dirname(original_path) or ".",
check=True,
)
return True
finally:
os.remove(patch_path)
Notice that apply_unified_diff raises instead of swallowing the failure. The orchestrator built in Step 10 is where you decide what happens next: retry the diff with more context lines, or escalate to a full rewrite. Hiding that failure inside the applier is one of the most common mistakes teams make when they build this for the first time, and it shows up again in the pitfalls section below.
Steps 9-13: Token Accounting, Retries, and the CLI Benchmark Harness
With the decision logic and the apply path working, the rest of the build is instrumentation: counting tokens accurately, wiring up a real model call, and building a harness that runs the same edit through both paths so you can see the gap for yourself instead of trusting a vendor’s number.
- Step 9: Add real token counting. Use
tiktokento count the actual prompt and completion tokens for every call, not an estimate based on character count. - Step 10: Build the orchestrator. This is the function that calls
decide_edit_mode, dispatches to either the diff path or the rewrite path, retries on failure, and logs the outcome. - Step 11: Wire up the LLM client. Keep the provider call behind a thin wrapper so swapping models later does not touch the decision or apply logic.
- Step 12: Build the CLI benchmark harness. Feed it a directory of sample files and a list of edit instructions, and have it run every edit through both the diff path and the forced-rewrite path so you can compare them directly.
- Step 13: Add guardrail logging. Every decision, retry, and fallback should write a structured log line, because this is the data you will use to re-tune your threshold in the advanced tips below.
# engine/tokens.py
import tiktoken
_encoder = tiktoken.get_encoding("cl100k_base")
def count_tokens(text: str) -> int:
return len(_encoder.encode(text))
def estimate_cost_usd(input_tokens: int, output_tokens: int, rate_per_million_in: float, rate_per_million_out: float) -> float:
return (input_tokens / 1_000_000 * rate_per_million_in) + (output_tokens / 1_000_000 * rate_per_million_out)
# engine/orchestrator.py
from .decision import decide_edit_mode
from .apply import apply_unified_diff, PatchApplyError
from .tokens import count_tokens
MAX_RETRIES = 2
def run_edit(file_path: str, original: str, instruction: str, llm_client) -> dict:
line_count = original.count("n") + 1
estimated_changed = llm_client.estimate_changed_lines(instruction)
decision = decide_edit_mode(line_count, estimated_changed)
attempts = 0
while attempts <= MAX_RETRIES:
attempts += 1
if decision.mode == "diff":
diff_text, out_tokens = llm_client.generate_diff(original, instruction)
try:
apply_unified_diff(file_path, diff_text)
return {"mode": "diff", "attempts": attempts, "output_tokens": out_tokens, "status": "ok"}
except PatchApplyError:
if attempts > MAX_RETRIES:
decision.mode = "rewrite" # final fallback
continue
else:
new_content, out_tokens = llm_client.generate_rewrite(original, instruction)
with open(file_path, "w") as f:
f.write(new_content)
return {"mode": "rewrite", "attempts": attempts, "output_tokens": out_tokens, "status": "ok"}
return {"mode": decision.mode, "attempts": attempts, "status": "failed"}
This orchestrator is intentionally small. The point is not to reproduce Cursor’s exact Fast Apply model, which is a fine-tuned, large, proprietary apply model running at roughly 1,000 tokens per second. The point is to reproduce the decision structure around it, so your own tool gets the same cost and reliability benefits without needing Cursor’s training data or infrastructure.
The Complete Working Project: Full Source Layout
Once Steps 1 through 13 are done, your project tree should look like this, with every module doing exactly one job:
fast-apply-engine/
├── engine/
│ ├── __init__.py
│ ├── diffing.py # Step 3: unified diff generation and hunk counting
│ ├── decision.py # Step 5-6: diff vs rewrite decision logic
│ ├── apply.py # Step 7: patch application with git apply
│ ├── tokens.py # Step 9: token counting and cost estimation
│ ├── orchestrator.py # Step 10: ties decision, apply, and retries together
│ └── llm_client.py # Step 11: provider-agnostic model wrapper
├── samples/
│ ├── small_file.py # 120 lines, for threshold testing
│ ├── medium_file.py # 450 lines, for threshold testing
│ └── large_file.py # 2,100 lines, for threshold testing
├── benchmark.py # Step 12: CLI harness
├── requirements.txt
└── README.md
The benchmark.py harness is the one piece worth showing in full, because it is what actually lets you reproduce the token-cost table from earlier in this article using your own model and your own sample files, instead of taking anyone’s published numbers on faith.
# benchmark.py
import time
import argparse
from engine.orchestrator import run_edit
from engine.llm_client import LLMClient
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--file", required=True)
parser.add_argument("--instruction", required=True)
parser.add_argument("--force-mode", choices=["diff", "rewrite", "auto"], default="auto")
args = parser.parse_args()
with open(args.file) as f:
original = f.read()
client = LLMClient(force_mode=args.force_mode)
start = time.perf_counter()
result = run_edit(args.file, original, args.instruction, client)
elapsed = time.perf_counter() - start
print(f"mode={result['mode']} attempts={result['attempts']} "
f"output_tokens={result.get('output_tokens', 'n/a')} "
f"elapsed={elapsed:.2f}s status={result['status']}")
if __name__ == "__main__":
main()
Running Your First Benchmark: Sample Output
Run the harness against the three sample files, forcing each mode so you see both paths on the same input, then let the decision engine run on auto so you can confirm it picked the cheaper, safer path on its own.
$ python benchmark.py --file samples/medium_file.py --instruction "rename the validate() function to validate_input()" --force-mode diff
mode=diff attempts=1 output_tokens=312 elapsed=0.89s status=ok
$ python benchmark.py --file samples/medium_file.py --instruction "rename the validate() function to validate_input()" --force-mode rewrite
mode=rewrite attempts=1 output_tokens=5840 elapsed=5.41s status=ok
$ python benchmark.py --file samples/large_file.py --instruction "add input validation to the parse_config function" --force-mode auto
mode=diff attempts=1 output_tokens=287 elapsed=0.76s status=ok
$ python benchmark.py --file samples/small_file.py --instruction "restructure the whole error-handling block" --force-mode auto
mode=rewrite attempts=1 output_tokens=1120 elapsed=1.84s status=ok
That last run is the one worth paying attention to. The decision engine chose rewrite for a small file even though the instruction described a fairly broad change, because the change-ratio check from Step 6 correctly identified that a small file undergoing a large restructure is exactly the scenario where diffing gets fragile and a rewrite is both cheap and more reliable. That is the same logic Cursor’s own documentation points to when it says the choice is shaped by the structure of the change, not a single fixed rule.
Comparing Your Engine Against Cursor Composer 2 and Competing Fast-Apply Models
Cursor is not the only team that has built a dedicated apply model. The same pattern, splitting the reasoning model from a faster apply step, shows up in multiple analyses of how AI coding tools edit files, including a comparison of Fast-Apply approaches across Cursor, Morph, and Relace, laid out in detail by Dreaming Press’s piece on diff versus whole-file versus search-replace edit formats. You will not be able to reproduce their exact throughput numbers with a self-hosted engine built on a general-purpose chat model, because Cursor’s apply model is purpose-trained and runs on dedicated infrastructure. What you can reproduce is the shape of the win: a diff path that is 5 to 15 times cheaper in tokens than a rewrite, at the cost of an occasional failed patch that your retry and fallback logic absorbs instead of surfacing as a broken file.
To put your own numbers in context, here is how Cursor’s own Composer line has progressed across its last three generations, using the benchmark figures Cursor has published for each release:
| Model | CursorBench | Terminal-Bench 2.0 | SWE-bench Multilingual | Notes |
|---|---|---|---|---|
| Composer 1 | 38.0 | 40.0 | 56.9 | Shipped with Cursor 2.0, October 29, 2025 |
| Composer 1.5 | 44.2 | 47.9 | 65.9 | Interim release ahead of Composer 2 |
| Composer 2 | 61.3 | 61.7 | 73.7 | Released March 19, 2026; a 37% relative CursorBench gain over 1.5 |
| Composer 2 (Oct 2026 update) | up to 64.8 | up to 69.4 | up to 82.7 | Figures from Cursor’s October 16, 2026 page; may reflect updated eval configs rather than a new launch |
Those scores measure end-to-end agentic task performance, not the apply mechanism in isolation, so treat them as directional context rather than a benchmark of diff-versus-rewrite specifically. The point of running your own harness from Step 12 is that you get a number that is actually comparable to your own stack: your model, your files, your threshold. Cursor’s own published insights are useful for understanding the direction the industry is moving, but they are not a substitute for measuring your own workload.
Common Pitfalls When Building a Fast-Apply Engine
Most of the engines that fail in production fail for a small set of repeatable reasons. Watch for these as you extend the project beyond the tutorial build.
- Trusting line numbers over context lines. Models drift on exact line numbers constantly. Always match on surrounding context text, not a line index, or your patches will silently apply to the wrong location in a file that has shifted since the model last saw it.
- Setting the threshold once and never touching it. The 400-line default is a reasonable starting point, not a constant. Teams that work mostly in large generated files or mostly in small utility modules will want to re-tune it using the logging from Step 13.
- Ignoring multi-hunk patches that partially apply. A diff with three hunks where only two apply cleanly is worse than a diff that fails outright, because the file is now in a half-edited, inconsistent state. Always validate with
git apply --checkbefore committing to the real apply. - Underestimating the cost of retries. A failed diff attempt is cheap on its own, but a loop that retries five times before giving up can end up costing more than a single rewrite would have. Cap retries explicitly, as in Step 8.
- Not normalizing line endings. A file saved with CRLF line endings being patched against a diff generated assuming LF will fail in ways that look like a model problem but are actually an encoding problem.
- Skipping a post-apply syntax check. A patch that applies cleanly at the text level can still produce a file that does not parse, especially with indentation-sensitive languages. Run a quick syntax check before treating any edit as successful.
- Benchmarking on toy files only. A 50-line sample file will make almost any threshold look fine. Test against real files from your own codebase, including the ugly 2,000-line ones, before trusting the default threshold.
Troubleshooting Guide
These are the failures you are most likely to hit once this engine is running against real edits instead of the sample files.
| Symptom | Likely Cause | Fix |
|---|---|---|
git apply fails with “patch does not apply” |
Context lines in the diff no longer match the current file state | Regenerate the diff against the freshest copy of the file right before applying, not a cached version |
| Patch applies but introduces duplicate code | Model emitted a diff hunk that overlapped with an existing edit from a prior retry | Reset to the original file before each retry attempt instead of patching on top of a failed partial apply |
| Rewrite path changes code outside the requested scope | Rewrite models tend to “clean up” nearby code even when not asked | Add an explicit instruction constraint, and diff the rewrite output against the original to flag unexpected changes |
| Token cost estimate does not match the real API bill | Using character-count heuristics instead of the model’s actual tokenizer | Switch to tiktoken or the provider’s official tokenizer, as built in Step 9 |
| Decision engine always picks rewrite, even on huge files | Change-ratio estimate is too conservative, overestimating edit size | Log the actual diff size after the fact and compare it to the pre-edit estimate to recalibrate |
| Encoding errors on apply | Non-UTF-8 source files, often from legacy codebases | Detect encoding explicitly before diffing, and normalize to UTF-8 for the diff step, then convert back on write |
| Timeout on very large files | Rewrite path sending the entire file as both input and output context | Force diff mode above a hard file-size ceiling regardless of change ratio, as a safety valve |
| Patch works locally but fails in CI | Different Git versions handle fuzzy patch matching differently | Pin the Git version in CI and disable fuzzy matching with git apply --exact where supported |
| Engine retries forever on a bad edit | Missing or misconfigured retry cap | Confirm MAX_RETRIES is actually being read from config and not silently defaulting to an unbounded loop |
Advanced Tips: Multi-File Edits, Streaming, and Guardrails
The 13-step build above handles a single file at a time, which covers most day-to-day edits. Production AI coding tools almost always need more than that, and the extensions below are where the engine starts to look like something you could actually ship inside a tool.
Multi-File Transactions
Once an edit spans more than one file, you need all-or-nothing semantics, the same way a database transaction works. Generate diffs for every affected file first, validate every single one with git apply --check, and only then apply them as a batch. If any file fails validation, abort the whole batch and fall back to a per-file rewrite only for the files that failed, leaving the ones that passed untouched. This avoids the worst outcome in multi-file editing: three files updated correctly and a fourth left in a broken state because its diff did not apply.
Streaming Token Accounting and Threshold Retuning
If your LLM client streams responses, you can start counting output tokens before the full response finishes, which lets you add a circuit breaker: if a rewrite response is already past your expected token budget for that file size and still streaming, you can cancel it early and fall back to a diff attempt instead of waiting for a response you already know is too expensive. Feed every decision, including cancellations, into the structured log from Step 13, and periodically replay that log against updated thresholds to see whether 400 lines is still the right cutoff for your actual codebase, or whether your files run large enough that 250 lines would save more in practice.
A simple guardrail worth adding early: track a rolling failure rate per file type or per repository. If diff attempts against a specific file are failing above, say, 40 percent of the time, that is a signal the file has unusual formatting or whitespace conventions, and the engine should default straight to rewrite for that file without wasting a diff attempt first.
For a deeper look at how production tools expose and tune this exact knob, the earlier guide on tuning diff versus rewrite behavior through Cursor Rules covers the configuration side of the same problem this tutorial solves at the code level. If you already run Cursor day to day and want to see the native setup path rather than building your own engine, the walkthrough on setting up Cursor Composer’s diff versus rewrite behavior is the more direct starting point.
Teams that need visibility into which edits actually went through diff versus rewrite in a live Cursor workspace, rather than a self-built engine, should also look at the guide on auditing Cursor AI edits between diff and rewrite modes, which covers building the logging layer on the product side instead of the engine side. And if you have already built something like the benchmark harness in Step 12 and want to compare it against Cursor’s own command-line behavior, the piece on benchmarking Cursor CLI’s diff versus rewrite edits walks through a parallel benchmark using Cursor’s headless CLI instead of a custom engine.
Finally, once you are running this engine against a real API bill, pair it with the cost-tracking approach from the guide on building a Cursor Composer cost guardrail, which applies the same token-accounting discipline from Step 9 of this tutorial to a live Composer workspace rather than a standalone script. For the broader context on how this fits into the rest of the coding-tools landscape, the software category hub tracks ongoing coverage of editor and tooling comparisons as new releases ship.
Why This Matters Beyond Cursor
The diff-versus-rewrite decision is not a Cursor-specific quirk, it is a problem every team building on top of an LLM for code generation eventually runs into, whether that is an internal CI bot that auto-fixes lint failures, a documentation generator that patches code comments, or a custom VS Code extension. The case study on designing a coding agent in the style of Claude Code, GitHub Copilot, and Cursor treats the apply layer as one of the hardest parts of the whole system to get right, harder in practice than the prompting or the model choice, because it is the layer where a subtle bug turns into a corrupted file in someone’s working tree.
Building your own version, even a simplified one like the engine in this tutorial, forces you to confront the trade-offs that vendors like Cursor have already made for you inside a black box. A separate analysis of how coding agents edit files with diffs, snapshots, and fast apply makes a similar point: once you measure the real token cost and failure rate on your own files, you get a much better sense of whether a commercial tool’s defaults are actually serving your codebase, or whether you need something tuned differently for the kind of files your team actually works in.
Frequently Asked Questions
What’s the real difference between a diff-based edit and a full-file rewrite in an AI coding tool?
A diff-based edit only transmits and applies the changed lines, usually as a unified diff or a search-and-replace block, while a full-file rewrite has the model regenerate the entire file including every unchanged line. Diffs are cheaper and faster but depend on exact context matching to apply correctly; rewrites are more reliable on small files but get expensive fast as file size grows.
How does Cursor Composer 2 decide whether to use a diff or a full rewrite?
Cursor has not published a fixed numeric rule. Its own documentation describes the choice as shaped by the size and structure of the requested change and the expected reliability of a patch, handled by the agent and the Fast Apply mechanism rather than exposed as a user-facing setting with a hard threshold.
Is it actually worth building a custom fast-apply engine instead of just using Cursor or another AI editor?
If you are building your own tool, extension, or automation on top of an LLM, yes, because you need this logic regardless of which editor you personally use day to day. If you are just trying to get more reliable edits inside Cursor itself, tuning Cursor Rules or using the Composer CLI benchmark is a faster path than building this from scratch.
What’s the real token cost gap between diff and rewrite on a large file?
For a roughly 2,000-line file, a full rewrite can run 16,000 to 24,000 output tokens, while a localized diff for the same single-function change typically runs 200 to 500 tokens. The gap narrows for small files and can even favor rewrite under about 400 lines, which is why a fixed threshold matters.
Why do search-and-replace or diff edits fail even when the model’s suggested change looks correct?
The failure is almost always a context-matching problem, not a logic problem. If the file has shifted since the model last read it, or the model slightly misremembers whitespace or a blank line, the patch cannot find the exact text to anchor against and fails to apply, even though the actual code change the model proposed was correct.
Can this patch engine work with any LLM, or is it tied to a specific provider?
The decision and apply logic in this tutorial is provider-agnostic. The only provider-specific code is the thin client wrapper in Step 11, which you can point at an OpenAI-compatible endpoint, an Anthropic-compatible endpoint, or a self-hosted model, as long as it returns either a unified diff or a full file.
What line-count threshold should I actually use for my own project?
Start at 400 lines, which is where published research shows rewrite reliability and diff economics cross over, then use the structured logging from Step 13 to measure your own failure rate and token cost over a few weeks of real edits, and adjust from there. Codebases full of large generated or vendored files often need a lower threshold.
How is this different from just configuring Cursor Rules or using the Cursor CLI benchmark approach?
Cursor Rules and the Cursor CLI benchmark both operate on top of Cursor’s existing Fast Apply mechanism, tuning or measuring behavior that Cursor itself controls. This tutorial builds the underlying mechanism from scratch, which only matters if you are shipping your own AI coding tool, bot, or extension rather than using Cursor as your daily editor.