#What AI coding assistants do
AddedAn AI coding assistant is a large language model (LLM) wrapped in a tool that decides what the model gets to see and what the model's output is allowed to do. The model on its own only turns text into more text. Everything that makes it useful for code (reading your files, suggesting a line as you type, editing three files and running the tests) is done by the tool around it, which this page calls the harness: the editor extension, the terminal program or the cloud runner.
Assistants come in three shapes, and most products now offer all three:
- Completion: as you type, the harness sends the code around your cursor to the model and shows its continuation as grey "ghost text". You accept it with Tab or ignore it. One suggestion, a few hundred milliseconds, no conversation.
- Chat: you ask a question or give an instruction in a side panel ("explain this function", "write a test for this"), the harness attaches context (the selected code, the open file, files you mention) and the model answers in prose and code blocks. You copy or apply what you want.
- Agents: you give a goal ("make the failing cart test pass"), and the model works through it in a loop of tool calls: read a file, search, edit, run the tests, read the output, edit again, until it decides it's finished. The harness runs each tool call, often after asking you for permission.
A useful mental model is three levels of delegation. Completion is a fast typist reading over your shoulder: it finishes your sentence, and you see every word before it lands. Chat is a well-read colleague you paste things to: it only knows what you show it, and you decide what to do with its answer. An agent is a contractor you hand a ticket and a sandbox: it does many steps you don't watch, and it comes back with a diff. The more you delegate, the more your job shifts from typing to specifying the task and checking the result (see The developer's job with an assistant).
They also differ in where they run:
| Where | What it looks like | Examples (from each vendor's docs, checked 2026-10-27) |
|---|---|---|
| Editor | completions, a chat panel, an agent that edits your working copy while you watch | GitHub Copilot in VS Code, JetBrains and other IDEs; Cursor; the Claude Code extensions for VS Code and JetBrains |
| Terminal | an agent you start in a project folder; it reads files, edits them and runs shell commands | Claude Code's CLI; GitHub Copilot CLI (generally available since 2026-02-25, per GitHub's changelog) |
| Cloud | an agent that runs on the vendor's machines against your repository and comes back with a branch or a pull request | Copilot cloud agent (renamed from "Copilot coding agent", GitHub changelog 2026-04-01); Claude Code on the web |
GitHub's own overview ("What is GitHub Copilot?", docs.github.com, checked 2026-10-27) groups Copilot's features the same way: assistive features where Copilot "responds while you work" and "you review and apply each suggestion", and agentic ones where "you describe a goal, and Copilot can work through multiple steps". The GitHub Copilot chapter covers Copilot's features one by one.
Here is that loop as a runnable model. The "model" is a fixed rule that reads the context, standing in for an LLM so the run is repeatable; everything else (tools, a permission gate, a context that grows) is how real agents are structured.
// A coding agent is a loop: the model proposes one tool call, the harness runs it, and the result is appended
// to the context for the next turn. `decide` is a fixed rule standing in for the LLM, so the run is repeatable.
const files = new Map([
["src/cart.js", "export const total = (items) => items.reduce((sum, i) => sum + i.price, 0);"],
["test/cart.test.js", "total([{ price: 2, qty: 3 }, { price: 5, qty: 1 }]) === 11"],
]);
const tools = {
read_file: ({ path }) => files.get(path) ?? `error: no such file ${path}`,
edit_file: ({ path, content }) => (files.set(path, content), `wrote ${content.length} chars to ${path}`),
run_tests: async () => {
const src = files.get("src/cart.js");
const { total } = await import("data:text/javascript," + encodeURIComponent(src));
const got = total([{ price: 2, qty: 3 }, { price: 5, qty: 1 }]);
return got === 11 ? "1 passed" : `1 failed: expected 11, got ${got}`;
},
};
const needsApproval = new Set(["edit_file"]); // reads and tests run freely; writes ask the human
const approve = (call) => call.args.path.startsWith("src/"); // the human's rule for this session
// Stand-in for the model: it only sees the context (the task plus every earlier result).
function decide(context) {
const results = context.filter((m) => m.role === "tool").map((m) => m.text);
if (results.length === 0) return { tool: "read_file", args: { path: "test/cart.test.js" } };
if (results.length === 1) return { tool: "read_file", args: { path: "src/cart.js" } };
const last = results.at(-1);
if (last === "denied by user") return { answer: "Not changed: the edit to src/cart.js was denied. Proposed fix: price * qty." };
if (last.startsWith("wrote") || results.length === 2) return { tool: "run_tests", args: {} };
if (last.includes("failed")) {
const content = "export const total = (items) => items.reduce((sum, i) => sum + i.price * i.qty, 0);";
return { tool: "edit_file", args: { path: "src/cart.js", content } };
}
return { answer: `Fixed: total now multiplies price by qty. Evidence: ${last}.` };
}
const context = [{ role: "user", text: "The cart test fails. Fix it." }];
for (let turn = 1; turn <= 8; turn++) {
const step = decide(context);
if (step.answer) { console.log(`turn ${turn} answer: ${step.answer}`); break; }
const call = { tool: step.tool, args: step.args };
let result;
if (needsApproval.has(call.tool) && !approve(call)) result = "denied by user";
else result = await tools[call.tool](call.args);
const gate = !needsApproval.has(call.tool) ? "" : result === "denied by user" ? " [denied]" : " [approved]";
console.log(`turn ${turn} ${call.tool}(${call.args.path ?? ""})${gate} → ${result.split("\n")[0].slice(0, 60)}`);
context.push({ role: "assistant", text: JSON.stringify(call) }, { role: "tool", text: result });
}
const chars = context.reduce((n, m) => n + m.text.length, 0);
console.log(`context: ${context.length} messages, ${chars} characters, all re-sent to the model every turn`);turn 1 read_file(test/cart.test.js) → total([{ price: 2, qty: 3 }, { price: 5, qty: 1 }]) === 11
turn 2 read_file(src/cart.js) → export const total = (items) => items.reduce((sum, i) => sum
turn 3 run_tests() → 1 failed: expected 11, got 7
turn 4 edit_file(src/cart.js) [approved] → wrote 83 chars to src/cart.js
turn 5 run_tests() → 1 passed
turn 6 answer: Fixed: total now multiplies price by qty. Evidence: 1 passed.
context: 11 messages, 538 characters, all re-sent to the model every turn- The context starts with one message, the task. On turn 1,
decidesees no tool results yet and asks to read the test; the harness runsread_file(reads need no approval) and appends both the call and its result tocontext. Turn 2 reads the source the same way. - Turn 3: with two results in context,
decideasks forrun_tests. The harness imports the currentsrc/cart.jsand runs the check:2 + 5 = 7, not11, so the result is1 failed: expected 11, got 7. That text is now in the context, which is the only way the "model" learns anything happened. - Turn 4: the last result says
failed, sodecideproposesedit_filewithprice * qty.edit_fileis inneedsApproval, so the harness asksapprove(the human's rule for this session: edits undersrc/are fine) before running it. The model never touches the file itself; it only produces a request. - Turn 5 runs the tests again:
2 × 3 + 5 × 1 = 11,1 passed. Turn 6: the last result isn't a failure, sodecidereturns an answer instead of a tool call, and the loop ends. The answer cites the evidence (1 passed) rather than just claiming success. - The last line is the cost of agency: 11 messages and 538 characters, and the whole context is sent again on every turn because the model keeps no state between requests (Anthropic's context-window docs, checked 2026-10-27: each turn's input "contains all previous conversation history plus the current user message"). Real sessions hold whole files and test logs, which is why agents fill context windows quickly.
- If you changed
approveto allow onlydocs/, turn 4 printsedit_file(src/cart.js) [denied] → denied by user, and on turn 5 the stand-in answersNot changed: the edit to src/cart.js was denied. Proposed fix: price * qty.A denial is just another tool result: the model has to cope with it, and a well-built agent reports what it couldn't do instead of claiming success.
The loop is the same everywhere; what changes is what the tools can reach and who's watching:
- Editor agents (Copilot agent mode, Cursor's agent, the Claude Code IDE extensions) edit your working copy while you watch the diff appear. Tools are the editor's own (read, search, edit, run a terminal command), plus MCP servers you configure. You're usually present, so approvals are cheap.
- Terminal agents (Claude Code, Copilot CLI) have the shell: they can run your build, your test runner,
git,curl. That makes them powerful for "make it pass" loops and dangerous without permission rules, which is why both products document approval modes. GitHub's changelog for Copilot CLI (2026-02-25) describes a plan mode in which Copilot "asks clarifying questions, and builds a structured implementation plan before writing any code", and an autopilot mode that keeps "iterating without stopping for approval". - Cloud agents (Copilot cloud agent, Claude Code on the web) run on the vendor's machines in a fresh checkout, so you aren't watching. Their output is a branch or a pull request, and review becomes the only checkpoint. GitHub's changelog entry of 2026-04-01 ("Research, plan, and code with Copilot cloud agent", the one that announced the new name) says the cloud agent can now work on a branch without opening a pull request, or produce a plan for you to review before it writes code.
The choice is mostly about feedback: work you want to steer turn by turn belongs in the editor; a well-specified task with a test that defines "done" can go to a terminal or cloud agent.
Completions look like magic because the request is invisible. GitHub described what goes into Copilot's completion prompts in "How GitHub Copilot is getting better at understanding your code" (Johan Rosenkilde, GitHub blog, May 2023, updated February 2024): prompts are "compilations of IDE code and relevant context", built from the code before the cursor (the prefix), the code after it (the suffix, which "fill-in-the-middle" training lets the model use), and snippets from other open files ("neighboring tabs"), chosen to fit a small budget; at the time its fast models could process "about 6,000 characters at a time". This model assembles a request the same way. It's the idea from that article, not Copilot's actual algorithm, and today's limits are different.
// How a completion request can be assembled: the code before the cursor (prefix), the code after it (suffix),
// and snippets from other open files that share identifiers with the code near the cursor. This follows the
// ideas in GitHub's 2023 write-up on Copilot's prompts; it isn't Copilot's actual algorithm.
const editing = `import { Room } from "./types";
export function freeRooms(rooms: Room[], at: Date): Room[] {
return rooms.filter((r) => ▮);
}
export function roomLabel(r: Room) { return r.name + " (" + r.capacity + ")"; }`;
const openTabs = {
"types.ts": "export interface Room { id: string; name: string; capacity: number; bookings: Booking[] }\nexport interface Booking { start: Date; end: Date }",
"booking.ts": "export function overlaps(b: Booking, at: Date) { return b.start <= at && at < b.end; }",
"theme.ts": "export const colors = { primary: '#84cc16', surface: '#0f1a04' };",
};
const BUDGET = 6_000; // characters; the 2023 article says its fast models handled "about 6,000 characters"
const KEYWORDS = new Set(["import", "export", "from", "function", "return", "const", "interface", "string", "number"]);
const words = (s) => new Set(s.match(/[A-Za-z_]\w+/g).filter((w) => !KEYWORDS.has(w)));
const [prefix, suffix] = editing.split("▮");
const nearCursor = words(prefix.slice(-200) + suffix.slice(0, 50));
const scored = Object.entries(openTabs).map(([file, text]) => {
const w = words(text);
const shared = [...w].filter((x) => nearCursor.has(x));
return { file, text, shared, score: shared.length / new Set([...w, ...nearCursor]).size };
}).sort((a, b) => b.score - a.score);
let prompt = "";
for (const s of scored) {
const keep = s.score > 0.05; // a deliberately low bar, as the article reports worked best
console.log(`${s.file.padEnd(11)} score ${s.score.toFixed(2)} shared [${s.shared.join(", ")}] ${keep ? "→ included" : "→ skipped"}`);
if (keep) prompt += `// from ${s.file}\n${s.text.replace(/^/gm, "// ")}\n`;
}
prompt = `${prompt}<PRE>${prefix}<SUF>${suffix}<MID>`;
console.log(`prompt: ${prompt.length} of ${BUDGET} characters; the model generates the missing middle after <MID>`);
console.log(prompt);booking.ts score 0.17 shared [at, Date] → included
types.ts score 0.13 shared [Room, Date] → included
theme.ts score 0.00 shared [] → skipped
prompt: 495 of 6000 characters; the model generates the missing middle after <MID>
// from booking.ts
// export function overlaps(b: Booking, at: Date) { return b.start <= at && at < b.end; }
// from types.ts
// export interface Room { id: string; name: string; capacity: number; bookings: Booking[] }
// export interface Booking { start: Date; end: Date }
<PRE>import { Room } from "./types";
export function freeRooms(rooms: Room[], at: Date): Room[] {
return rooms.filter((r) => <SUF>);
}
export function roomLabel(r: Room) { return r.name + " (" + r.capacity + ")"; }<MID>- The cursor
▮splits the file into a prefix (the import, the signature,rooms.filter((r) =>) and a suffix ();, the closing brace androomLabel). The suffix matters: it tells the model the expression must end before);, so it shouldn't write a whole new function. nearCursoris the set of identifiers close to the cursor (Room,rooms,filter,at,Date…), ignoring keywords. Each open tab is scored by how many identifiers it shares with that set, relative to all identifiers involved (Jaccard similarity).booking.tssharesatandDate(0.17) andtypes.tssharesRoomandDate(0.13), so both go in, as comments above the code.theme.tsshares nothing and is skipped. The bar is deliberately low; the article reports that "setting a very low bar for when to include a match" gave the best suggestions.- The assembled prompt is 495 characters: the two snippets, then
<PRE>prefix<SUF>suffix<MID>. The model generates what belongs at the cursor. WithoverlapsandBookingin view, a plausible completion is something like!r.bookings.some((b) => overlaps(b, at)); without them, the model would have to guess that a room hasbookingsat all. - That's the practical lesson for completions: the files you have open are part of the prompt. Opening the types file and the helper you want used is the cheapest context you can give a completion engine. (The sentinel markers differ between models;
<PRE>,<SUF>and<MID>here only show the structure.)
The model is the same few model families across many products. The harness is where products differ, and it's what you configure as a team:
| Harness feature | What it does | Where you'll meet it |
|---|---|---|
| Context assembly | chooses which files, symbols and history go into each request | @file mentions, "open tabs" context, repository indexing |
| Instructions | a file of standing rules prepended to every request | .github/copilot-instructions.md, CLAUDE.md, AGENTS.md |
| Tools | what the model can ask to do | built-in read/edit/run tools, plus MCP servers |
| Permissions | which tool calls run without asking | approval prompts, allow/deny lists, sandboxed cloud runners |
| Compaction | summarises old turns when the context fills | automatic in long sessions |
That's also why "which model is best" is the wrong first question for a team: the same model with better context, better instructions and a test it can run gives better results. The Prompting for Code chapter is about the context half of that.
For each task, pick completion, chat or an agent, and say why: (a) writing the fifth near-identical Express route handler in a file that already has four; (b) understanding what a 300-line legacy function does; (c) "every endpoint under /api/v1/rooms must return the new error shape; the contract tests define it".
Show answer
(a) completion, (b) chat, (c) an agent, ideally one that can run the contract tests.
- (a) The pattern is right there in the file: four handlers above the cursor. Completion is built for exactly this: it continues the local pattern, and you see each suggested line before it lands. Chat would be slower, and an agent is overkill for one function.
- (b) You want an explanation, not a change. Chat with the function selected gives the model the code and lets you ask follow-ups ("what happens when
retriesis 0?"). Treat the explanation as a hypothesis and check the claims that matter against the code or a quick test. - (c) Many files, a clear definition of done (the contract tests), and mechanical edits: that's an agent's job. The tests turn "done" into something the loop can check by itself, so it can iterate without you watching every step.
- The general rule: the less you'll watch, the more the task needs a check the agent can run. Without one, an agent's "done" means only "looks done to the model".