How Aider works under the hood

How Aider works, traced through its source: no tools, edits parsed from text, a PageRank repo map, and how it differs from Codex. The code is quoted.

Codex and Aider look alike from the outside: you type, the model edits your code. Inside they are opposite designs. In Codex the model has tools and explores the repository itself. In Aider the model has no tools at all. The harness decides what the model sees, the model answers in plain text, and the harness finds the edits in that text and applies them.

This post follows one message through Aider's Python source, the same way my Codex and Elpis post does for Codex. Code is quoted verbatim from Aider 0.86.3.dev, commit 5dc9490, with paths relative to the repository root. Nothing here was run; it is read from the source.

1. The shape: one object per mode

main() builds a single Coder and loops on coder.run(). Each edit format is its own Coder subclass with its own prompts and parser, so a mode change such as /ask or /architect is not a flag. It raises SwitchCoder, and main builds a new Coder from the old one, copying the files, history and cost across (in Coder.create).

If the edit format changed, the old history is summarized first, because the code's own comment says the old format "will confused the new LLM. It may try and imitate it".

2. What the model sees

The request is assembled by ChatChunks in a fixed order: system prompt, examples, read-only files, the repo map, past history, the editable files, the current turn, and last a reminder of the edit-format rules.

done is the past history and cur is the current turn. Stable parts come first and volatile ones last, so the prompt cache keeps a long prefix (my inference from the caching code below). Notice that the editable files sit after the history, so the model reads the newest version of a file right next to your request.

Context blocks are faked as conversation: a message such as "here are the files" followed by an invented "Ok." from the assistant. A history summary comes back the same way, as a user message beginning "I spoke to you previously about..." (see summarizeall in aider/history.py and summaryprefix in aider/prompts.py). The model sees these as things that were said, not as notes from the system.

The whole conversation goes to litellm, one API over many providers, at temperature 0 by default (in Model.sendcompletion). Retryable errors back off from 0.125 seconds, doubling until the delay passes 60.

3. No tools, on purpose

The base class sets functions = None, and the reflection limit sits right next to it.

Three coders once used JSON function calls to edit, but they are out of the registry, and two raise "Deprecated" when built (in editblockfunccoder.py and wholefilefunccoder.py). The only reason the repository gives is a changelog note from v0.7.0 in HISTORY.md: "Initial experiments show that using functions makes 3.5 less competent at coding".

So the model writes edits as fenced text, and Aider finds them with regular expressions. The nearest thing to a shell tool is a bash block in the reply, which runs only if you say yes (in runshellcommands). Its output goes into the next turn, not back to the model at once.

That changes what "agentic" means. Codex loops until the model stops asking for tools. Aider's only loop is a reflection: after a turn, if Aider itself found a problem, it sends the problem back as the next user message, at most three times (the maxreflections = 3 above). Four things count as a problem: an edit that would not parse or match, a repo file the model named that is not in the chat, lint errors, and test errors. They all share the same budget of three.

That second one is how the model "opens a file". It cannot call a read tool, so it names the file in its reply. Aider notices, asks you whether to add it, and replays the turn. The check runs before any edit is applied, so an otherwise valid edit in that same reply is thrown away.

The early return comes before applyupdates() is ever called. A person is the gate where Codex would have a tool call.

4. From a reply to a change on disk

The default format is SEARCH/REPLACE: the model quotes the lines to change, then the lines to put there. The parser is loose on purpose, accepting five to nine marker characters and hunting up to three lines above a block for its filename, because one model kept misplacing them (in findoriginalupdateblocks and findfilename).

Note the {5,9} in each pattern and the i - 3 window for the filename.

Matching an imperfect SEARCH tries a fixed sequence: an exact line match, then the same match with the leading indentation repaired ("GPT often messes up leading whitespace", says the comment), then the same again without a stray blank line, then a ... elision that requires each piece to appear exactly once (in replacemostsimilarchunk).

perfectorwhitespace covers the first two steps. Keep an eye on the bare return near the end; section 10 comes back to it.

When a block fails, the error the model receives is written to be fixable: each failed block echoed back, the closest real lines in the file, a reminder that SEARCH must match exactly including whitespace, and "The other N blocks were applied successfully. Don't re-send them." (in EditBlockCoder.applyedits).

Aider also has unified-diff, whole-file and OpenAI-style patch formats. The whole-file format has no matching step at all: the file is simply overwritten.

5. The repo map: context without a model choosing it

Aider never lets the model search the repository, so it hands over a map. For every file it uses tree-sitter to pull out definitions and references, and caches them on disk keyed by modification time (in RepoMap.gettags and gettagsraw, in aider/repomap.py).

Then it builds a graph. Files are nodes, and an identifier that one file defines and another references becomes an edge from the referencing file to the defining one. The weight is a multiplier times the square root of the number of references, so a name repeated a hundred times does not drown out the rest. The multiplier is 10 if you mentioned the name, 10 if it looks distinctive (long snake, kebab or camel case), 0.1 if it starts with an underscore, 0.1 if more than five files define it, and a further 50 if the edge comes from a file already in the chat (in RepoMap.getrankedtags).

"Long" means at least eight characters.

Personalized PageRank then ranks the files, seeded by the chat files and anything you mentioned (the nx.pagerank call in the same function). No language model is involved. The score is split over each file's definitions, definitions in chat files are dropped because their full text is already sent, and Aider binary-searches for the largest map that fits the token budget, stopping once it is within 15% of it (in RepoMap.getrankedtagsmapuncached).

The budget is an eighth of the model's input window, clamped between 1,024 and 4,096 tokens.

6. Keeping the window under control

Aider never trims context by itself. Before each request checktokens() only warns, "Try to proceed anyway?", and tells you to /drop or /clear. If the provider still reports an exceeded window, the turn simply ends.

The one thing a model does manage is chat history. Once past history exceeds a sixteenth of the context window, clamped to between 1,024 and 8,192 tokens, a background thread summarizes the older part with the cheap "weak" model, keeping a recent tail of about half the budget word for word (the limit is set in the Model constructor; the split is in ChatSummary.summarizereal in aider/history.py).

For cost, --cache-prompts places Anthropic-style cache markers on up to three prefixes (examples, repo map, editable files), and a daemon thread pings the model every five minutes minus five seconds with a one-token request to keep the cache warm while you think (in ChatChunks.addcachecontrolheaders and Coder.warmcache).

Codex aims at the same goal differently: it re-sends the whole history with a promptcachekey and lets the provider's cache do the rest.

7. Git as the safety net

Aider has no sandbox; a search of its source finds none. Its safety net is git. Before editing a file it commits any uncommitted changes you had in it, so each edit gets its own clean commit. After writing, it commits the result with a message written by a model, and tells the model the hash (in checkfordirtycommit and autocommit, and GitRepo.commit in aider/repo.py). /undo will only revert a commit if it is one of this session's own, has a single parent, has not been pushed, and its files are clean (in rawcmdundo in aider/commands.py).

8. Architect and editor: two models, one handoff

In architect mode a strong model plans and a second model writes the edits. The architect produces no edits itself. When it finishes it asks "Edit the files?", then starts a new Coder on the editor model with no history and no repo map, and passes the architect's whole reply to it as the user message (in ArchitectCoder.replycompleted).

maptokens = 0 turns the repo map off, the two message lists are emptied, and content is the architect's reply. My reading is that this lets a strong reasoning model be paired with one that is better at the edit format, though the code does not say so.

9. Side by side

Aider Codex Who explores the repo The harness, through a repo map and the files you add The model, through shell and read tools How edits happen Parsed from the model's text A tool call (applypatch) The loop At most 3 harness-driven reflections Until the model stops calling tools Safety A git commit per edit, and your yes before any command the model suggests An approval policy and an OS sandbox Long sessions You manage the file set; old history is summarized in the background The window is compacted at 90%, and the model's own outputs are dropped Providers litellm, one call for many providers The Responses API, with adapters added in Elpis

Neither is simply better. Aider's design gives up autonomy for predictability: roughly one model call per turn plus at most three reflections, and a human at every decision that reaches outside the chat. Codex's gives the model room to explore and pays for it with a sandbox, an approval system and a longer context.

10. Things you would not guess

Fuzzy matching is switched off. A comment reads "Try fuzzy matching", but a bare

return sits just above the call, so it never runs. Only the error hint uses similarity (see the end of replacemostsimilarchunk in section 4).

A reply can leave a half-applied commit. Blocks that match are written to disk one by

one, and the result is auto-committed before the failures are sent back to be retried (in EditBlockCoder.applyedits and Coder.sendmessage).

A wrong filename is quietly forgiven. If a SEARCH does not match its named file,

Aider tries every other file in the chat and applies the edit to the first that matches (in EditBlockCoder.applyedits).

The context guard fails open. If token counting throws, it prints a warning and

returns 0, so the check passes (in Model.tokencount).

The unified-diff format replaces every match. The uniqueness check is commented out

and the code calls str.replace; the SEARCH/REPLACE format replaces only the first.

/drop a.py also drops data.py. Dropping matches by substring of the path, where

/add matches exactly or by glob (in cmddrop).

What this does not cover

I did not trace ContextCoder, watch mode's internals beyond the outline, voice input, the web scraper, or analytics. Aider can be run with a different edit format per model, and the defaults live in a settings file I read only through git.