Build an AI Coding Agent That Ships Real Pull Requests: Architecture, Guardrails and Cost Per PR (2026)
- A coding agent that opens a pull request costs about 64 cents in models, sandbox and CI on the arithmetic below. That number is almost irrelevant. The cost that decides whether the system is worth building is the human review minute, and it is roughly fifty times larger.
- The single metric that determines the entire business case is merge rate — the share of agent attempts that a human actually merges. On the model here, the agent pays for itself above roughly a 19% merge rate and is a rounding error of value below it. Nobody selling you a coding agent will quote that number.
- Test-driven verification is not a quality feature, it is the containment boundary. An agent that cannot run your test suite in a sandbox is not a coding agent; it is an autocomplete with a git remote, and it will generate confident diffs that compile and break production.
An issue, a spec or a review comment turned into a bounded task with an explicit definition of done, a file allowlist and a budget. Unbounded tasks are how agents burn $8 and produce nothing.
human: ~6 min · fails: vague specAn AST-derived symbol map plus a hybrid index over code, tests, past PRs and review comments. This is the layer that decides whether the agent edits the right file at all.
$0.0004/task amortised · fails: wrong moduleOne turn that produces a typed plan: files to touch, tests that must pass, tests that must be written, and a turn budget it has to justify. Never let the loop start without one.
$0.0374 · fails: over-broad planRead, patch, run tests, read failures, patch again. Capped at twelve turns with no-progress detection. Two thirds of the entire bill lives here.
$0.43 · fails: oscillationA disposable container with the repo and no outbound network. The agent may run anything inside it and nothing outside it. This is the containment boundary, not a performance optimisation.
$0.039 · fails: network egressBranch, commit, push, open PR, read check runs, search code. Scoped to one branch on one repo, never to main, never to the org.
$0.072 CI · fails: over-scoped tokenTests green, diff inside declared bounds, no forbidden paths touched, a cold critic pass, and a human who has to click. Five checks, four of them deterministic.
$0.034 · fails: green tests, wrong changeWhat is an AI coding agent, architecturally?
A grounding index over the repository, a planner that emits a typed plan, a bounded edit loop that runs the test suite after every patch, a sandbox that contains it, and a merge gate that a human has to pass. Five parts. Only the edit loop looks like an agent, and it is the part you should spend the least design time on.
That framing is unfashionable because the demo of a coding agent is the edit loop. A model reads a file, writes a diff, and the video ends. What the video omits is that the diff was applied to a repository the model already had memorised, that nothing ran the test suite, and that no human had to decide whether to merge it. All three of those omissions are where the engineering is.
Be precise about the scope this design targets. It is not an autonomous engineer. It is a system that takes well-specified, test-covered, small-to-medium changes — a bug with a reproduction, a well-scoped refactor, a dependency bump with fallout, an endpoint that mirrors an existing one — and produces a pull request a human reviews in under ten minutes. Everything about the architecture below follows from choosing that scope and defending it.
This is a reference design: how I would build this, and what the arithmetic says it costs. It draws on architecting AccioMatrix, an event-driven AI assessment and interview platform orchestrating multiple LLMs, agents and MCP tooling for 20+ enterprise clients. I have not shipped a production coding agent, and nothing below is an invoice. Every dollar figure is a model with its inputs printed, and you should substitute yours before quoting any of it.
One published finding should sit next to your business case from the first meeting. METR's randomised controlled trial, published in July 2025, found that sixteen experienced open-source developers took 19% longer to complete 246 real tasks on their own repositories when allowed to use early-2025 AI tools — while estimating afterwards that the tools had made them 20% faster. That gap between measured and perceived productivity is the reason the merge-rate arithmetic later in this post matters more than any benchmark number you will be shown.
| Component | What it does | Implementation | Cost per attempt | Primary failure mode | Skip in v1? |
|---|---|---|---|---|---|
| Task intake | Turns an issue or comment into a bounded task with a definition of done, a file allowlist and a budget | A template plus a validator; reject tasks with no acceptance criteria | ~$0 model, ~6 min human | A vague task produces a plausible diff that solves a different problem | No — this is where merge rate is won |
| Repo grounding | AST symbol map plus hybrid index over code, tests, past PRs and review comments | tree-sitter or the language server, plus pgvector and BM25 | $0.0004 amortised | Agent edits a deprecated module that still compiles and is no longer called | No |
| Planner | One turn producing a typed plan: files, tests to pass, tests to write, turn budget | Sonnet-class, structured output, ~25k in / 900 out | $0.0374 | Plans a twelve-file refactor for a two-file bug and burns the whole budget | No — a fixed plan template is worse but still a plan |
| Edit loop | Read, patch, run tests, read failures, patch. Twelve-turn cap, no-progress detection | In-house loop, Sonnet-class, ~8 turns typical | $0.4304 (8 turns) | Oscillates between two failing states, re-applying the same patch with different whitespace | No |
| Sandbox runner | Disposable container, repo mounted, no outbound network, full test suite runnable | Firecracker-class microVM, 2 vCPU / 4 GiB, per-second billing | $0.039 | Egress allowed by default; a fetched dependency exfiltrates a token | Absolutely not |
| MCP tool gateway | git branch, commit, push, open PR, read check runs, code search — one branch, one repo | MCP servers behind a gateway, ~10 tools, ~1.5k tokens of schema per turn | In the turn cost | A token scoped to the org lets a bad plan force-push another repository | Gateway: no. MCP transport: often |
| CI verification | Runs the real pipeline on the pushed branch, not just the sandbox suite | GitHub-hosted Linux 2-core at $0.006/min, ~6 min per run | $0.072 (2 runs) | Sandbox green, CI red, because the sandbox skipped the integration tests | No |
| Merge gate | Tests green, diff bounds, forbidden paths, cold critic, human approval | Deterministic checks plus one Haiku-class critic on the diff | $0.034 | Green tests on a change that satisfies the tests and not the requirement | No |
| Run journal | Every turn, diff, test result, tool call, token count and dollar, queryable | Postgres tables plus OTel GenAI spans | ~$0 | You cannot answer why the agent did that three weeks later | No |
What does the full architecture look like?
Twelve components across four columns: intake and grounding, planning and the edit loop, sandbox and tool gateway, and the pull request that a human has to approve. The load-bearing detail is that the sandbox sits between the agent and every side effect, and the merge gate sits between the agent and the default branch. Neither boundary is inside the model.
Read it left to right and the containment story falls out. A task arrives and is resolved against a specific repository and a specific base commit — not a branch name, a commit SHA, because an agent that starts from a moving target produces a diff that no longer applies by the time a human looks at it. Grounding then answers a question the model cannot answer from its weights: in this repository, today, which files implement this behaviour and which tests cover them.
The planner exists to make the loop cheap. One turn that costs about 3.7 cents produces a typed plan naming the files it expects to touch, the tests that must go from red to green, and the turn budget it is requesting. That plan is then a contract: if the loop tries to edit a file outside the declared set, that is a signal worth acting on rather than an inconvenience to route around. Most agents that go badly wrong go wrong by quietly expanding their own scope, and a declared file set turns that from an invisible drift into a hard stop.
The edit loop is deliberately dull: read a file, propose a patch, apply it in the sandbox, run the targeted tests, read the failures, patch again. What makes it work is not the prompt but the feedback signal. A test failure with a stack trace is a dense, unambiguous, machine-generated correction — it is by some distance the highest-quality feedback any agent in your company receives, and it is free. The general shape of this loop, with its budgets and journal writes, is the same one described in the agent loop in production.
Everything to the right of the tool gateway is ordinary software that already exists in your company. Git hosts branches. CI runs pipelines. Humans review pull requests. The mistake teams make is trying to replace those with agent-native equivalents; the correct move is to make the agent a participant in the workflow your engineers already trust, because trust is the scarce resource in this product and you cannot bootstrap it with a new UI.
How does the agent ground itself in an unfamiliar repository?
With a symbol map derived from the parser, not from embeddings. Function and class definitions, their call sites, their imports and the tests that exercise them, extracted with tree-sitter or the language server and stored as a graph. Semantic search over code chunks is a useful second layer and a terrible first one.
The reason is that code retrieval and prose retrieval are different problems with the same interface. When a support agent searches documentation, semantic similarity is the right relation: the customer said one thing, the doc says another, and the embedding bridges the gap. When a coding agent needs to change the behaviour of a function, the relations it needs are exact: who calls this, what does it call, which tests import it, which module owns it. Those are graph queries, they are cheap, they are deterministic, and they are correct. An embedding will happily return a similarly named function in a deprecated package.
The layered index I would build has four tiers, and their costs are wildly asymmetric. The symbol graph is a parse over the repository — minutes of CPU, no model calls, refreshed incrementally per commit. The chunk embeddings are one pass at $0.02 per million tokens on a small embedding model, which puts a 500,000-line repository at roughly six million tokens and about twelve cents to embed in full, then near-zero to keep current. The past-PR corpus is where the institutional knowledge lives: how this team names things, which reviewer always asks for the null check, what got rejected last time. And the ownership map, from CODEOWNERS and git blame, is what lets the agent route its own PR to the right human.
Freshness is handled by commit, not by TTL, and this is a rare case where the cache invalidation problem is easy. A commit hook re-parses only the changed files and re-embeds only their chunks. A repository receiving 200 commits a day touching an average of four files is a few thousand re-embedded chunks daily, which is a fraction of a cent. Anyone quoting a large ongoing indexing bill for a code agent is either re-indexing the whole repository on a schedule or selling you something.
One deliberate omission: do not put the entire repository in context because the context window is large enough to hold it. It is a genuine option now and it is almost always the wrong one. A million-token context on Sonnet-class pricing is $2 of input per turn before the agent has done anything, and retrieval quality does not improve monotonically with context size — precision matters more than recall once you are past the relevant files. The related trade-offs for private corpora are worked through in RAG over private documents.
Definitions, call sites, imports, test coverage edges, extracted by tree-sitter or the language server. Answers who calls this and what tests cover it exactly, not approximately. Refreshed per commit on changed files only.
CPU only · no model callsBM25 plus vector over code and doc chunks, for the fuzzy first hop when the task description does not name a symbol. Second, never first.
~$0.12 to embed 500k LOC onceMerged diffs, review comments and the discussion that produced them. This is where team convention lives: naming, error handling, which reviewer always asks for the same thing.
highest-value, lowest-hygieneCODEOWNERS plus git blame recency, so the agent routes its own PR to a human who will actually recognise the code. Costs nothing and materially improves review latency.
$0 · improves time-to-reviewimport { createHash } from "node:crypto";
type Plan = {
files: string[]; // declared file set - enforced, not suggested
testsMustPass: string[];
testsToWrite: string[];
turnBudget: number; // <= MAX_TURNS
};
type TestResult = { passed: boolean; failures: string[]; durationMs: number };
const MAX_TURNS = 12;
const MAX_USD = 2.5;
const NO_PROGRESS_LIMIT = 3;
export type LoopOutcome =
| { status: "candidate"; diff: string; turns: number; usd: number }
| { status: "abandoned"; reason: string; turns: number; usd: number };
export async function editLoop(
plan: Plan,
sandbox: Sandbox,
model: Model,
journal: Journal,
): Promise<LoopOutcome> {
let usd = 0;
let seen = new Map<string, number>();
const budget = Math.min(plan.turnBudget, MAX_TURNS);
for (let turn = 1; turn <= budget; turn++) {
if (usd >= MAX_USD) {
return { status: "abandoned", reason: "usd_budget", turns: turn - 1, usd };
}
const context = await sandbox.readFiles(plan.files);
const step = await model.propose({ plan, context, turn });
usd += step.usd;
// Enforce the declared file set. Scope creep is a stop, not a warning.
const touched = filesInPatch(step.patch);
const outside = touched.filter((f) => !plan.files.includes(f));
if (outside.length > 0) {
await journal.step(turn, "scope_violation", { outside, usd });
return { status: "abandoned", reason: "scope_violation", turns: turn, usd };
}
const applied = await sandbox.applyPatch(step.patch);
if (!applied.ok) {
await journal.step(turn, "patch_rejected", { error: applied.error, usd });
continue; // a rejected hunk is information, not a crash
}
const result: TestResult = await sandbox.runTests(plan.testsMustPass);
const diff = await sandbox.diff();
await journal.step(turn, "tested", {
passed: result.passed,
failures: result.failures.length,
usd,
});
if (result.passed) {
return { status: "candidate", diff, turns: turn, usd };
}
// No-progress detection: same diff shape AND same failure signature.
const fp = fingerprint(diff, result.failures);
const n = (seen.get(fp) ?? 0) + 1;
seen.set(fp, n);
if (n >= NO_PROGRESS_LIMIT) {
return { status: "abandoned", reason: "no_progress", turns: turn, usd };
}
}
return { status: "abandoned", reason: "turn_budget", turns: budget, usd };
}
function fingerprint(diff: string, failures: string[]): string {
const normalisedDiff = diff.replace(/\s+/g, " ").trim();
const normalisedFailures = failures.map((f) => f.split(":")[0]).sort().join("|");
return createHash("sha256")
.update(normalisedDiff + "::" + normalisedFailures)
.digest("hex");
}What happens on one pull request, end to end?
Intake, grounding, one planner turn, eight edit turns each followed by a targeted test run, a push, a real CI run, a critic pass on the final diff, then a pull request that a human either merges or closes. Roughly fourteen minutes of wall clock, about eleven model calls and one row per turn in a journal you own.
The ordering choice that matters most is when the full test suite runs versus when a targeted subset runs. Inside the loop, only the tests named in the plan execute — typically eight to forty tests, three to twenty seconds. Running the complete suite after every patch would be architecturally purer and would multiply both the wall clock and the sandbox bill by an order of magnitude for a signal the agent mostly does not need. The full suite runs once, in real CI, on the pushed branch, and its failure is a legitimate reason to send the run back for one more turn rather than to open the PR.
The second detail is that the sandbox is created once per run and destroyed at the end, not created per turn. Sandbox billing is per second of existence rather than per second of execution, so an idle sandbox costs exactly what a busy one does. At the modelled rate of $0.000046 per second for 2 vCPU and 4 GiB, a fourteen-minute run is about 3.9 cents. Leave that sandbox alive for an hour because a queue backed up and it is 16.6 cents, which is a quarter of your entire per-PR budget spent on a container doing nothing. A hard TTL on the sandbox is a cost control disguised as hygiene.
The third detail is that the pull request carries evidence, not just a diff. The body should contain the plan the agent committed to, the tests that went red-to-green with their names, the files touched against the files declared, the turn count, and the dollar cost of the run. That is roughly three cents of model call and it changes the review from an act of faith into an act of checking. A reviewer who can see that the agent declared four files and touched four files, and that the two named tests moved from failing to passing, reviews substantially faster than one staring at an unexplained diff.
Finally, note where the run can end early and cheaply. Scope violation terminates at the turn it happens. No-progress terminates after three identical diff-plus-failure fingerprints. The dollar ceiling terminates before a turn rather than after it. Each of those is a few cents of loss instead of two dollars, and together they are why the abandoned-run cost in the model is $0.77 rather than the $2.50 ceiling.
- 1Stop 1 · Intake rejects the task$0.00
No acceptance criteria, no reproduction, or a file allowlist that spans more than eight files. The task goes back to a human with a specific reason. This is the cheapest possible failure and the most under-used.
- 2Stop 2 · Grounding finds nothing$0.0004
No symbol match and no chunk above the relevance floor. The agent should say I cannot locate the code this describes rather than guess a module. Refusal is a feature.
- 3Stop 3 · Scope violation~$0.15 at turn 3
The loop tries to patch a file outside the declared set. Terminate at that turn, journal the attempted files, and surface them — a repeated scope violation on the same task usually means the plan was wrong, not the agent.
- 4Stop 4 · No progress~$0.31 at turn 6
Three consecutive turns with the same diff-plus-failure fingerprint. This is oscillation, and it will not resolve itself with more turns. Terminate and open a draft PR showing the wall it hit.
- 5Stop 5 · Budget exhausted$0.77 modelled
Twelve turns or $2.50, whichever binds first. The dollar check runs before the turn, not after, so the ceiling is a ceiling.
- 6Stop 6 · Merge gate blocks$0.61 + human triage
CI red, diff outside declared bounds, a forbidden path touched, or the critic flags a mismatch between the change and the requirement. Draft PR with the failing check named.
How do you stop it merging nonsense?
With five checks outside the model, four of them deterministic, and a human who has to click merge. Tests green in real CI. Diff inside the declared file set and under a line ceiling. No forbidden path touched. A cold critic that compares the diff to the original requirement rather than to the plan. And an approval from a code owner.
The critic deserves the most explanation because it is the only one that involves a model, and because the naive version of it is useless. Asking the same model that wrote the diff whether the diff is good returns yes. What works is a separate, cheaper model given three things and nothing else: the original task description, the final diff, and the test names that changed state. It is asked one question — does this diff accomplish what the task asked, and does it do anything the task did not ask for. It never sees the agent's reasoning, because the reasoning is exactly the artefact that will talk it into agreement.
The forbidden-path list is the check that saves you from the worst outcome, and it is fifteen lines of configuration. Migrations, authentication and authorisation modules, payment handling, CI configuration, dependency manifests, infrastructure-as-code, secrets templates. An agent should be able to read all of those and write to none of them without an explicit per-task override that a human grants. The reason is not that a model cannot write a migration; it is that a wrong migration is not a code review problem, it is an incident, and the asymmetry between the value of automating that change and the cost of getting it wrong is not close.
The diff-bounds check is the quiet one that catches the most real problems. A task declared four files and a hundred lines; the diff touches four files and nine hundred lines. Nothing has failed — the tests are green and the paths are allowed — but something has gone sideways, usually a reformat, a lockfile regeneration or an over-eager refactor riding along with the actual change. A ceiling of roughly three times the planned line count, with the overrun surfaced in the PR body, turns that from a reviewer's twenty-minute discovery into a one-line banner.
None of these checks make the agent correct. They make its failures cheap and visible, which is the achievable goal. The honest framing to give a team is that the merge gate is a filter on a probabilistic generator: it converts a distribution of diffs, some of which are dangerous, into a distribution of diffs whose worst case is a wasted review. That is the same design philosophy as the confidence gate in the customer support agent build, applied to a domain where the blast radius is your default branch.
import { z } from "zod";
import micromatch from "micromatch";
export const CriticSchema = z.object({
accomplishes_task: z.boolean(),
does_extra_work: z.boolean(),
concerns: z.array(z.string()).max(5),
});
const FORBIDDEN = [
"**/migrations/**",
"**/auth/**",
"**/billing/**",
"**/payments/**",
".github/workflows/**",
"infra/**",
"**/*.tf",
"package-lock.json",
"pnpm-lock.yaml",
];
const DIFF_LINE_MULTIPLIER = 3;
type Candidate = {
task: string;
diff: string;
filesTouched: string[];
filesDeclared: string[];
linesChanged: number;
linesPlanned: number;
ciGreen: boolean;
testsFlipped: string[];
};
export type GateResult =
| { decision: "ready_for_review"; notes: string[] }
| { decision: "blocked"; reason: string; detail?: string };
export async function mergeGate(
c: Candidate,
critic: (c: Candidate) => Promise<z.infer<typeof CriticSchema>>,
): Promise<GateResult> {
// 1. free: the real pipeline, not the sandbox subset
if (!c.ciGreen) {
return { decision: "blocked", reason: "ci_red" };
}
// 2. free: forbidden paths are a hard stop, never a warning
const forbidden = micromatch(c.filesTouched, FORBIDDEN);
if (forbidden.length > 0) {
return { decision: "blocked", reason: "forbidden_path", detail: forbidden.join(", ") };
}
// 3. free: the plan is a contract, not a suggestion
const undeclared = c.filesTouched.filter((f) => !c.filesDeclared.includes(f));
if (undeclared.length > 0) {
return { decision: "blocked", reason: "undeclared_files", detail: undeclared.join(", ") };
}
// 4. free: a green diff 9x the planned size is still a red flag
const ceiling = Math.max(40, c.linesPlanned * DIFF_LINE_MULTIPLIER);
if (c.linesChanged > ceiling) {
return {
decision: "blocked",
reason: "diff_too_large",
detail: c.linesChanged + " changed vs ceiling " + ceiling,
};
}
// 5. the only model call, on a cheap model, without the agent's reasoning
const verdict = await critic(c);
if (!verdict.accomplishes_task) {
return { decision: "blocked", reason: "critic_task_mismatch", detail: verdict.concerns.join("; ") };
}
const notes = verdict.does_extra_work
? ["Critic flagged work beyond the task: " + verdict.concerns.join("; ")]
: [];
// The best outcome this function can produce is a PR a human still has to merge.
return { decision: "ready_for_review", notes };
}The best case. A failing test is the definition of done, the loop has a real signal, and the merge gate has something to verify against. Expect the highest merge rate in this class by a wide margin.
High merge rate, low review cost per line, and exactly the work engineers most resent. Raise the line ceiling explicitly for this task class rather than removing the check.
Naming the file to imitate in the task is worth more than any prompt engineering. Without it the agent invents a house style; with it, review is a diff against a known shape.
The forbidden-path list. The agent can produce a draft in a scratch branch for a human to take over, but it may not open a PR against these paths. The asymmetry is not close.
Without tests there is no falsification signal and the loop optimises for plausibility. Without acceptance criteria the critic has nothing to compare against. Both produce diffs that look right and waste a review.
What tools should the agent have, and how are they defined?
About ten, and the scoping matters more than the count. Reads: search code, read file, list symbols, read test output, read check runs, read past PRs. Writes: apply patch (sandbox only), commit, push to a run-specific branch, open a pull request. There is deliberately no merge tool and no force-push.
Every tool schema costs roughly 150 tokens on every turn, so ten tools is about 1,500 tokens per turn and roughly 15,000 tokens across a ten-turn run. That is real but not dominant. The dominant reason to keep the surface small is selection accuracy: an agent choosing between ten well-described tools picks correctly far more often than one choosing between forty, and every wrong tool choice costs a full turn at roughly five cents plus the wall clock.
The credential scoping is where a coding agent differs sharply from every other agent you will build. A support agent reads customer records; the blast radius of a mistake is one customer. A coding agent holds a git credential, and the blast radius of a badly scoped one is your organisation's source control. The correct scope is a token valid for one repository, one branch whose name contains the run id, with no force-push, no branch deletion, no tag push, no workflow-file write and no merge permission. Mint it per run, expire it in an hour, and record its identity in the journal so any commit can be traced to a run.
On transport: use MCP for the things you did not build — the git host, the CI system, an external code search — and plain typed function calls for your own sandbox. The July 2026 specification revision made the operational side considerably easier, removing protocol-level sessions and the Mcp-Session-Id header from Streamable HTTP so that any server instance can serve any request behind an ordinary load balancer, and adding Mcp-Method and Mcp-Name headers that a gateway can route and rate-limit on without parsing the JSON body. For an agent that fans out tool calls across many concurrent runs, being able to rate-limit per tool name at the edge is genuinely useful. The full gateway pattern is in MCP in production.
The tool definition below is the shape I would ship for the single most important tool in the system: running tests. Three things distinguish it from the naive version. The sandbox id comes from the verified run context and is never a model-settable parameter, so an agent cannot execute in another run's container. The output is truncated server-side with the failures preserved and the passing noise dropped, because a verbose test runner will emit 60,000 tokens of green dots. And the description tells the model when not to call it, which removes an entire class of wasted turn.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "sandbox-exec", version: "1.4.0" });
const MAX_FAILURES = 12;
const MAX_CHARS = 8000;
server.registerTool(
"run_tests",
{
title: "Run a named subset of the test suite in this run's sandbox",
description:
"Runs the given test files or test names inside the sandbox for the " +
"current run and returns pass/fail plus the first failures with stack " +
"traces. Call this after every patch. Do NOT call this to explore the " +
"codebase - use search_code or read_file, which are free and instant. " +
"Do NOT pass an empty selector to run the whole suite; the full suite " +
"runs once in CI after the branch is pushed.",
inputSchema: {
selectors: z.array(z.string()).min(1).max(40)
.describe("Test file paths or fully qualified test names"),
timeoutSec: z.number().int().min(5).max(300).default(120),
},
},
async ({ selectors, timeoutSec }, extra) => {
// Sandbox is NOT a model-settable argument. It is resolved from the
// verified run context. This is the entire run-isolation story.
const runId = requireRunId(extra.requestInfo);
const sandbox = await sandboxes.forRun(runId);
const started = Date.now();
const exec = await sandbox.exec({
cmd: testCommandFor(sandbox.language, selectors),
timeoutSec,
network: "none", // no egress, ever, including from the test process
});
const parsed = parseTestOutput(exec.stdout, exec.stderr);
const failures = parsed.failures.slice(0, MAX_FAILURES).map((f) => ({
test: f.name,
message: f.message,
trace: f.trace.split("\n").slice(0, 8).join("\n"),
}));
const payload = {
passed: parsed.failed === 0 && exec.exitCode === 0,
total: parsed.total,
failed: parsed.failed,
truncatedFailures: Math.max(0, parsed.failures.length - MAX_FAILURES),
durationMs: Date.now() - started,
timedOut: exec.timedOut,
failures,
};
const text = JSON.stringify(payload).slice(0, MAX_CHARS);
return {
// A failing suite is the signal the agent needs, not a tool error.
content: [{ type: "text", text }],
structuredContent: payload,
};
},
);| Kind | Scope | Approval | Blast radius if wrong | |
|---|---|---|---|---|
| search_code | read | one repo, indexed snapshot at base SHA | none | Agent edits the wrong module; caught by the critic or the reviewer |
| read_file | read | one repo, path-allowlisted | none | Wasted tokens. The cheapest possible mistake |
| list_symbols | read, deterministic | symbol graph only | none | Stale graph after a fast-moving merge; refresh is per-commit |
| read_past_prs | read, merged only | one repo, review comments included | none | Learns a convention the team has since abandoned |
| apply_patch | write, sandbox only | the run's container filesystem | none | Contained by construction. Nothing outside the sandbox can see it |
| run_tests | execute, no egress | the run's container, subset selectors | none | A test that mutates shared state — which is why egress is off and the container is disposable |
| git_commit | write | run-specific branch only | none | A bad commit on a branch nobody merges. Recoverable by deletion |
| git_push | write, no force | run-specific branch, per-run token, 1h TTL | none | The single most dangerous tool here. No force-push, no tag push, no workflow writes |
| open_pull_request | write | one repo, base branch fixed | merge gate must pass | Review-queue noise, which is a real cost at volume — cap PRs per repo per day |
| read_check_runs | read | the run's branch only | none | Misreads a flaky failure as a real one and burns two turns chasing it |
| merge_pull_request | not exposed | n/a | n/a | Deliberately absent. The human click is the product, not the friction |
| force_push / delete_branch | not exposed | n/a | n/a | Deliberately absent. There is no task worth the recovery story |
What does one pull request actually cost?
About 64 cents in models, sandbox and CI for a run that produces a PR, and about 77 cents for one the budget governor abandons. Blended over a modelled mix of 55% merged, 25% closed and 20% abandoned, that is roughly $1.21 of machine cost per merged pull request. The human cost on the same run is about $19, and that is the number that decides everything.
Work the resolve path and the shape is immediate. The planner turn is $0.0374. Eight edit turns at $0.0538 each — 20,000 fresh input tokens, 14,000 cached, 1,100 output on Sonnet-class pricing — is $0.4304, or 67% of the machine bill. The sandbox at $0.000046 per second for fourteen minutes is $0.039. Two CI runs at six minutes each on Linux 2-core at $0.006 per minute is $0.072. The Haiku-class critic on a 30,000-token diff is $0.034 and the PR body is $0.030. Grounding is $0.0004 amortised. Total $0.6432, every line of which is a token count multiplied by a printed rate.
Now the arithmetic that matters. Take a hundred attempts. Specification costs six minutes each at $75/hour fully loaded, which is $750. Machine cost across the mix is $66.60. Review costs nine minutes on the 55 merged PRs and seven on the 25 closed ones, which is 670 minutes or $837.50. Triage on the 20 abandoned runs is four minutes each, $100. That is $1,754 for 55 merged changes, or $31.89 each. The same 55 changes written by hand at a modelled 70 minutes each cost $4,812, or $87.50 each. The saving is real — about 64% — and 95% of both sides of the comparison is human minutes.
Which means the sensitivity analysis is the whole analysis. Hold everything else fixed and vary merge rate: at 40% the cost per merged change is $42.92, at 30% it is $56.39, at 20% it is $83.33, and the crossover with writing it by hand lands at roughly 19%. Below that the agent is a net cost, and the reason is not tokens — it is that every closed PR consumed seven minutes of a senior engineer's attention and produced nothing. A coding agent is a machine that converts review minutes into merged changes at some efficiency, and if the efficiency is bad the machine is a tax.
Two implications follow that most vendor pages will not tell you. First, tightening intake raises merge rate far more cheaply than upgrading the model, because rejecting a task that was never going to merge saves the entire review minute. Second, the cheapest available optimisation is making agent PRs faster to review — evidence in the body, small diffs, a declared file set the reviewer can check at a glance. Neither of those is a model problem. The general cost machinery for the model side, including routing and cache design, is in LLM routing, caching and cost per request.
| Line item | Model / rate | Tokens or units | PR opened | Abandoned run | Note |
|---|---|---|---|---|---|
| Repo grounding (amortised) | text-embedding-3-small · $0.02 per 1M | ~6M tokens indexed once, incremental after | $0.00040 | $0.00040 | One-time full index of a 500k-LOC repo is about $0.12 |
| Planner turn | Sonnet 5 · $2 / $10 per 1M | 25.0k in (12k cached) / 900 out | $0.03740 | $0.03740 | Cached prefix = system + 10 tool schemas + repo conventions |
| Edit turns | Sonnet 5 | 34k in (14k cached) / 1.1k out per turn | $0.43040 (8 turns) | $0.64560 (12 turns) | 67% of the machine bill. Each turn re-sends files plus failures |
| Sandbox compute | E2B-class · $0.000046 per second | 2 vCPU / 4 GiB, 840s vs 1,200s | $0.03864 | $0.05520 | Billed for existence, not execution. A TTL is a cost control |
| CI runs | GitHub Actions Linux 2-core · $0.006/min | 2 runs x 6 min vs 1 run x 6 min | $0.07200 | $0.03600 | The real pipeline, not the sandbox subset |
| Critic pass on final diff | Haiku 4.5 · $1 / $5 per 1M | 30.0k in / 800 out | $0.03400 | n/a | Sees task, diff and flipped test names. Never the agent's reasoning |
| PR body with evidence | Sonnet 5 | 12.0k in / 600 out | $0.03000 | n/a | Plan, tests flipped, files declared vs touched, turns, cost |
| Machine subtotal | — | — | $0.64284 | $0.77460 | Modelled from Aug 2026 list prices, not measured |
| Task specification | $75/hr fully loaded | 6 min | $7.50000 | $7.50000 | Paid on every attempt, including the ones that go nowhere |
| Human review or triage | $75/hr fully loaded | 9 min merged / 7 min closed / 4 min abandoned | $11.25000 | $5.00000 | The dominant term by a factor of about thirty |
| Total per attempt | — | — | $19.393 | $13.275 | Blended per merged change at 55% merge rate: $31.89 |
What breaks in production, and how would you know?
Eleven things, and the three most expensive all produce a green pipeline. A diff that satisfies the tests without satisfying the requirement, a flaky test the agent silently disables, and a change that quietly widens its own scope all look like healthy successful runs on every dashboard you own. No error rate moves.
Read the failure table below by its second column first, because what the user sees is what determines whether a failure is a wasted review, a rolled-back deploy or a security incident. The row worth staring at is the prompt-injection one. A coding agent reads issues, README files, dependency documentation and code comments — all of it attacker-influenceable in an open-source dependency, and all of it landing in a model context that holds a git credential. The defence is not a better prompt. It is that the credential cannot force-push, cannot touch workflow files, cannot merge, and expires in an hour, so a successful injection produces a bad branch rather than a bad deploy.
Four signals catch most of the rest. Merge rate by task class is the primary one and it must be broken out, because aggregate merge rate hides a bug-fix class at 70% and a refactor class at 15% averaging to a comfortable-looking 55%. Review time on agent PRs versus human PRs is the second: if reviewers are spending longer on agent diffs than on their colleagues', the system is transferring work rather than removing it. Revert rate within seven days is the third and the sharpest quality signal available, requiring no labelling and no judge. Turn distribution is the fourth, because a rightward shift in turns per run is the earliest warning that grounding quality has degraded.
Alert on rates and distributions, never on individual runs. An agent that produces one unmergeable PR in six is a functioning agent. An agent whose merge rate dropped from 55% to 32% over a week is an incident, and the usual cause is a repository change — a refactor that invalidated the symbol graph, a test suite that got slower and started timing out, a dependency bump that broke the sandbox image. All three are invisible in model metrics and obvious in merge rate.
The last row of the table is the capability that separates a system from an experiment. If you cannot answer, for a specific pull request from three weeks ago, which files the agent read, what its plan declared, which tests flipped, how many turns it took, what it cost, and which check the gate evaluated last, you do not have a production coding agent. You have a very expensive suggestion box. That capability is one journal table, one query, and it is what makes the difference between debugging the system and rewriting it.
| Failure mode | What the user sees | Where to fix it | Detection signal | Cost of getting it wrong |
|---|---|---|---|---|
| Diff satisfies the tests, not the requirement | A green PR that a reviewer approves and that ships the wrong behaviour | Merge gate: the cold critic compares diff to the original task, never to the plan | Revert rate within 7 days; reviewer change-request reasons | One production bug plus the review minutes that legitimised it — roughly $11 wasted and an incident |
| Agent disables or weakens a flaky test | All green, coverage silently down, the flake returns in production | Gate: block any diff that deletes assertions or adds skip markers without an explicit task authorisation | Assertion-count delta per PR; skip-marker linter in CI | Systemic. Every future run trusts a suite that no longer tests the thing |
| Scope creep inside an allowed file set | A 900-line diff for a 100-line task; review takes 40 minutes | Gate: diff-line ceiling at 3x planned, surfaced in the PR body as a banner | Ratio of changed lines to planned lines, p90 | Review cost triples on that PR and reviewer trust in the whole system drops |
| Prompt injection via issue text or dependency docs | A branch containing an unrelated change, possibly an exfiltration attempt | Credential scope: branch-only token, 1h TTL, no force-push, no workflow writes, no merge | Diff touching paths unrelated to any declared file; egress attempts from the sandbox | Contained to a branch by design. Without the scoping, this is your worst day |
| Sandbox has outbound network | Nothing visible; a dependency install phones home with an env var | Sandbox: egress denied by default including from the test process, allowlist only a package mirror | Egress attempt count per run; any non-zero value on a default-deny policy | Credential exfiltration. The one failure here that is not fixable with an apology |
| Grounding returns a deprecated module | A confident diff to code nothing imports; tests pass because nothing calls it | Grounding: symbol graph first, with call-site counts and last-modified as ranking features | Share of PRs touching files with zero inbound call edges | A full wasted run plus a review, about $19, and a reviewer who now distrusts intake |
| Loop oscillates between two failing states | Nothing for eleven minutes, then an abandoned run | Runtime: fingerprint the diff plus failure signature, terminate after 3 repeats | No-progress termination rate; turn-count distribution shifting right | $0.31 to $0.77 per stuck run, and the wall-clock slot it occupied |
| Base SHA moved during the run | A PR with conflicts that the reviewer has to resolve | Orchestrator: pin the base commit at intake, rebase once at push, abandon on a second conflict | Conflict rate at push; time between intake and push, p95 | Reviewer resolves a merge conflict for a machine, which is the fastest way to kill adoption |
| Test output floods the context window | Truncated reasoning, then a nonsense patch | MCP server: bound output server-side to the first N failures with passing output stripped | Tool result size p99; context-overflow error rate | A wasted turn at about $0.05, plus a patch generated from half a stack trace |
| Review-queue flooding | Engineers ignoring agent PRs entirely | Orchestrator: cap open agent PRs per repository per day; queue rather than fan out | Open agent PRs older than 48h; median time-to-first-review | The system dies socially before it fails technically. This is the most common real-world ending |
| No run journal | Nobody can explain a merged change three weeks later | Journal every turn, diff, tool call, test result, token count and dollar in Postgres | Can you answer why did it do that from one SELECT — yes or no | You lose the ability to distinguish a regression from a bad week, permanently |
- Merge rate, broken out by task classAggregate merge rate is the single most misleading number in this product. Bug fixes and speculative refactors do not belong in the same average.
- Review minutes on agent PRs versus human PRsIf agent diffs take longer to review than human ones, you have moved work rather than removed it. This is the honesty metric.
- Revert rate within 7 days of mergeThe sharpest quality signal in the domain. No judge, no labelling, no eval set. Wire it before launch.
- Turn-count distribution per runA rightward shift is the earliest warning that grounding degraded — usually after a refactor invalidated the symbol graph.
- Cost per merged change, with a price snapshotStore cost as bigint micros with the rate card id, and include the human minutes. A models-only cost dashboard tells you almost nothing here.
- Sandbox egress attempts, on a default-deny policyAny non-zero value is worth a look. This is the cheapest security signal you will ever add and it takes an afternoon.
- Reviewer change-request reason codesA dropdown on the review — wrong approach, missed a case, style, scope. Organisational change rather than engineering, which is why it never gets built and why it would be the most valuable eval set you own.
What does it take to build, in engineer-weeks?
About ten engineer-weeks for a two-person team to a v1 that can be pointed at a real repository, plus roughly four engineer-days a month of maintenance. The distribution surprises people: the agent loop is about a week, and the sandbox, the grounding index and the merge gate are six between them.
Week one is intake, the journal and cost attribution — the task schema with its acceptance criteria and file allowlist, the run and turn tables, and cost middleware that stores micros against a rate-card id. No demo value, and everything downstream depends on it. Week two is the sandbox: a disposable microVM image that can install your dependencies and run your test suite with egress denied, per-second billing, a TTL, and a clean teardown. This is unglamorous platform work and it is the component that most determines whether the project survives its first security review.
Weeks three and four are grounding. The symbol graph via tree-sitter or the language server, the incremental per-commit refresh, the hybrid chunk index, and the past-PR corpus with its review comments. Like the retrieval corpus in a support agent, this phase is underestimated by the widest margin, because the parse is a day and the incremental freshness plumbing is the rest.
Week five is the planner and the edit loop with all their budgets. Week six is the MCP tool surface and the per-run credential minting, which is more security work than software. Weeks seven and eight are the merge gate — the deterministic checks are quick, the critic prompt and its calibration against a set of real merged and rejected diffs is not. Weeks nine and ten are evals, dashboards, the PR-body template, review-queue rate limiting, and the actual work of introducing this to engineers who did not ask for it and will judge it on their first three PRs.
Two things get cut and both are mistakes. The first is the PR evidence body, cut because it is not a feature, despite costing three cents and being the single largest lever on review time, which is 95% of your cost. The second is intake validation, cut because rejecting a colleague's task feels rude — and then merge rate sits at 22%, the crossover is 19%, and nobody can explain why the project feels like a tax. If you need to move faster than ten weeks, compress scope rather than quality: one repository, one task class, six tools, a hard eight-turn cap. That is closer to five weeks and it is a real system.
- Week 1Intake, journal, cost attribution
Task schema with acceptance criteria and file allowlist. Run and turn tables. Cost middleware storing micros against a rate-card id. Zero demo value, total downstream dependency.
- Week 2The sandbox
Disposable microVM that installs dependencies and runs your suite with egress denied. Per-second billing, hard TTL, clean teardown. Platform work that decides whether this passes a security review.
- Weeks 3‑4Repo grounding
Symbol graph via tree-sitter or the language server, incremental per-commit refresh, hybrid chunk index, past-PR corpus with review comments. The parse is a day; freshness is the rest.
- Week 5Planner and edit loop
Typed plan, declared file set enforced by the loop, turn and dollar budgets, no-progress fingerprinting, journal writes per turn. Smaller than anyone expects.
- Week 6MCP tools and per-run credentials
Ten tools, branch-scoped token minted per run with a one-hour TTL, no force-push, no workflow writes, no merge. More security design than code.
- Weeks 7‑8Merge gate and critic calibration
Deterministic checks are quick. Calibrating the critic against a real set of merged and rejected diffs is the work, and skipping it means your gate is a guess wearing a schema.
- Weeks 9‑10Evals, evidence, rollout
Deterministic CI suite over a frozen task set, dashboards, the PR-body template, review-queue rate limiting, and shadow mode on a real repo where nobody merges anything for two weeks.
- Changes shipped
- 55
- Time per change
- 70 min
- Fully loaded rate
- $75/hr
- Machine cost
- $0
- Monthly cost
- $4,813
- Cost per merged change
- $87.50
- Task specification
- 100 x 6 min = $750
- Machine (models, sandbox, CI)
- $66.60
- Review on 80 PRs
- 670 min = $838
- Triage on 20 abandoned runs
- 80 min = $100
- Monthly cost
- $1,754
- Cost per merged change
- $31.89
What should you skip in version one?
Multi-repository changes, autonomous merge, self-improving prompts and a custom fine-tuned model. Each is a real capability and each costs a month you should instead spend discovering whether your merge rate clears 30% on one repository and one task class.
Skip multi-repository work first, because it is the most requested and the most expensive. A change spanning three services needs coordinated branches, a deployment ordering, and a review across three sets of owners — that is a release-engineering problem with an agent attached, and the agent is the easy part. Get single-repo merge rate stable before you go near it.
Skip autonomous merge permanently, or at least until you have a year of revert-rate data on a specific task class. The human click is not friction; it is the containment property that makes every other risk in this system survivable. The correct path is to measure how often a reviewer merges an agent PR without requesting a single change, and only when a narrow class clears something like 95% over hundreds of PRs should anyone open that conversation — and even then, only for that class.
Skip the multi-agent decomposition. A planner agent, a coder agent, a tester agent and a reviewer agent is a diagram people like and a system that costs several times more for the same output; Anthropic's own guidance reports multi-agent implementations typically consuming three to ten times the tokens of a single-agent approach for equivalent tasks. A single change to a single repository is a short-horizon task with an unambiguous success criterion, which is exactly the shape where one well-prompted agent with ten tools wins. The decision procedure is in multi-agent versus single agent.
What you must not skip: the sandbox with egress denied, per-run branch-scoped credentials, the forbidden-path list, the run journal, and merge-rate instrumentation broken out by task class. Those five are about three of the ten weeks and they are the difference between a system you can defend in a security review and a demo with a git token. If you want this architected and built rather than described, that is what AI product development is for, and the broader stack these components sit in is laid out in the AI product architecture guide.
- A sandbox with egress denied by default, including from the test process
- Per-run branch-scoped git credentials with a one-hour TTL and no force-push
- The forbidden-path list covering migrations, auth, billing, CI and infrastructure
- A run journal recording every turn, diff, test result, token count and dollar
- Merge rate and revert rate broken out by task class, from the first PR
- Multi-repository changes — a release-engineering problem with an agent attached
- Autonomous merge — the human click is the containment property, not the friction
- Multi-agent decomposition — 3-10x tokens for equivalent tasks on Anthropic's own numbers
- A fine-tuned model — you have no labelled dataset until reviewers record rejection reasons
Building an AI coding agent: common questions
→How much does an AI coding agent cost per pull request?
On the model in this post, about 64 cents in machine cost for a run that opens a PR — 43 cents of Sonnet-class edit turns, 7 cents of CI on GitHub-hosted Linux 2-core at $0.006 per minute, 4 cents of sandbox at published per-second rates, and the rest in planning and gating. Blended across a mix of merged, closed and abandoned runs that is about $1.21 per merged pull request. The human cost on the same run is roughly $19 at $75/hour fully loaded, which makes the machine bill about 3.4% of the total. These are modelled figures with their token counts and rates printed, not measurements of a system I have operated.
→What merge rate should I plan for?
Plan for 30-40% across a mixed task queue and treat anything higher as a consequence of good intake rather than a good model. Merge rate varies enormously by task class: bug fixes with a reproduction in well-tested code sit far above the average, and speculative refactors in untested modules sit far below it. On the arithmetic here the crossover with writing the change by hand is roughly 19%, so a system running at 22% is technically working and economically pointless. Break the metric out by task class from the first PR, because the aggregate number hides exactly the information you need.
→Should the agent be allowed to merge its own pull requests?
No, and not because of a policy preference. The human click is the containment property that makes every other risk survivable — prompt injection through dependency documentation, a diff that satisfies the tests without satisfying the requirement, a silently disabled flaky test. Each of those becomes a bad branch rather than a bad deploy specifically because merge permission is absent. If you eventually want to revisit it, the prerequisite is hundreds of PRs in one narrow task class merged without a single requested change, plus revert-rate data over months. Merge is also not exposed as a tool at all, so a compromised context cannot reach for it.
→Does the agent need a sandbox, or can it just propose diffs?
It needs a sandbox, and the reason is epistemic rather than operational. A diff nobody has executed is a hypothesis. The entire quality mechanism of a coding agent is the loop between a patch and a real test failure with a real stack trace, which is the densest and cheapest correction signal available anywhere in your engineering organisation. Without it the model optimises for plausibility, and plausible diffs are exactly the ones that pass review and break production. Modelled at published per-second rates, a fourteen-minute sandbox at 2 vCPU and 4 GiB costs under four cents.
→Which tasks are worth routing to a coding agent?
Bugs with a reproduction in code above your coverage threshold, mechanical changes across many files such as renames and API migrations, and new components that mirror a named existing one. Those three classes have an unambiguous definition of done, a real falsification signal, and a review that is a diff against a known shape. Route nothing that touches migrations, authentication, billing, CI configuration or infrastructure, and reject at intake any task without acceptance criteria — a task the critic cannot check is a task that will consume a review minute and produce nothing.
→How long does it take to build one?
About ten engineer-weeks for a two-person team to a v1 you can point at a real repository, plus roughly four engineer-days a month of maintenance. The edit loop is about one week of that. The sandbox is one, repo grounding is two, the merge gate and its critic calibration are two, and evals plus rollout are two. A narrower version — one repository, one task class, six tools, a hard eight-turn cap — is closer to five weeks and is a legitimate way to get a real merge rate before committing to the full build.
→Do AI coding tools actually make engineers faster?
The best available randomised evidence says be careful. METR's July 2025 trial found that sixteen experienced open-source developers took 19% longer on 246 real tasks in their own repositories when allowed to use early-2025 AI tools, while estimating afterwards that they had been about 20% faster. That is a study of interactive assistance on mature codebases rather than of an autonomous PR pipeline, so it does not settle the question — but it does establish that self-reported speedup is an unreliable metric, which is why the design here insists on merge rate, review minutes and revert rate as the measurements that count.