| published | false | ||||
|---|---|---|---|---|---|
| type | workshop | ||||
| title | Agent Quality & Token Optimization | ||||
| short_title | Agent Quality & Token Optimization | ||||
| description | Hands-on workshop for improving agent output quality while reducing total token waste through better model choice, context, prompts, workflow, and deterministic guardrails. | ||||
| level | intermediate | ||||
| authors |
|
||||
| contacts |
|
||||
| duration_minutes | 180 | ||||
| tags | github-copilot, ai-agent, prompt-engineering, context-engineering, token-optimization | ||||
| navigation_levels | 3 | ||||
| navigation_numbering | false | ||||
| sections_title |
|
Core principle: Don't minimize tokens - maximize their value. Better quality → fewer retries → lower total cost.
This workshop turns the Agent Quality & Token Optimization workshop into a set of hands-on labs. You will not just read about the techniques, you will practice them and measure the difference they make.
The fastest way to start is a Codespace, Node.js, the sample app dependencies, and the Copilot extensions are all pre-configured by .devcontainer/devcontainer.json.
This workshop uses fimdim/sample-app: a tiny TypeScript + Express task API. It works well enough to run, but it contains planted problems that you will discover and fix during the workshop. Do not fix them ahead of time; each module asks you to drive the agent to find and resolve them.
- Click the badge (or Code ▸ Codespaces ▸ Create codespace).
- Wait for the container to build,
npm ciruns automatically at the repo root. - Open a terminal and verify:
npm run build && npm test(the build passes; one test fails on purpose, that's your first exercise).
Prefer local? Any machine with Node.js 20+ works too, see the track setup module.
A mixed audience, from developers new to AI agents through to experienced Copilot users. Modules are ordered by increasing difficulty. Each module clearly marks its level:
| Badge | Level | Meaning |
|---|---|---|
| 🟢 | Beginner | Safe to do with no prior agent experience |
| 🟡 | Intermediate | Assumes you've done the beginner modules |
| 🔴 | Advanced | Power-user techniques, deeper config |
The workshop ships as two fully separate tracks covering the same concepts with tool-native instructions:
| Track | Tool | Start here |
|---|---|---|
| VS Code | GitHub Copilot Chat / Agent mode in VS Code | VS Code track |
| GitHub Copilot CLI | copilot command-line agent in a terminal |
GitHub Copilot CLI track |
Both tracks share:
concepts: short, tool-agnostic thinking exercises (no tool required).sample-app: a small, deliberately imperfect TypeScript API you will improve throughout the labs. It lives in its own repo: github.com/fimdim/sample-app.
You can complete the concepts with pen and paper. The track modules use the sample app so your prompts have something real to work on.
The workshop has three parts; the labs follow the same arc:
- Why agent quality matters → Modules 00–01
- LLM & agent fundamentals → Module 02
- Optimization techniques (the 5 levers) → Modules 03–07
- Advanced controls & power-user habits → Modules 08–09
- Capstone → Module 10
| # | Module | Concept | Level |
|---|---|---|---|
| 00 | Setup & ROI mindset | Maximize token value | 🟢 |
| 01 | Why quality matters | Agent ROI + compounding error | 🟢 |
| 02 | How the model "thinks" | Stateless LLM, context window | 🟢 |
| 03 | Lever 1 : Model selection | Right model for the job | 🟡 |
| 04 | Lever 2 : Context optimization | As little as possible, as much as necessary | 🟡 |
| 05 | Lever 3 : Prompt engineering | Precise prompts + stop conditions | 🟡 |
| 06 | Lever 4 : Workflow design | Research → Plan → Implement | 🟡 |
| 07 | Lever 5 : Deterministic controls | Tests, linters, security gates | 🟡 |
| 08 | Advanced controls | Instructions, agents, skills, MCP, sub-agents | 🔴 |
| 09 | Power-user tips | Scripting, filtering, usage analysis | 🔴 |
| 10 | Capstone | Put all 5 levers together | 🔴 |
Common
- Node.js 20+ and npm (for the sample app)
- Git
- A GitHub account with GitHub Copilot enabled
- The
sample-apprepository — clone it from github.com/fimdim/sample-app, or use the Codespace (which already includes it)
VS Code track
CLI track
- GitHub Copilot CLI installed
(
npm install -g @github/copilotor the official installer) and authenticated (copilotthen/login).
Verify your setup in Module 00 of your chosen track before continuing.
Throughout the labs you'll use these proxy metrics to make the abstract idea of "token value" concrete:
- Retries to success : how many times you had to re-prompt before the result was correct.
- Turns / tool calls : how many round-trips the agent took.
- Correction edits : how many manual fixes you made afterward.
- Guardrail signal : did tests / lint / type-check pass on the first agent attempt?
Lower numbers = higher token value. You'll record these in a simple scorecard in each module.
1. Model selection → large = plan/debug · medium = implement · small = trivial · Auto by default
2. Context optimization → only relevant files · reset sessions often
3. Prompt engineering → be precise · add stop conditions · supply context explicitly
4. Workflow design → Research → Plan → Implement (separate, clean contexts)
5. Deterministic control → tests · linters · security checks to stop compounding errors
- Choose the right model.
- Write clear prompts.
- Split tasks.
- Add deterministic guardrails.
- Maintain concise instructions.
→ Make every token count.
These three short exercises build the mental models behind the workshop. They need no tools, just thinking, a calculator, and a couple of minutes each. Do them before (or alongside) your chosen track.
| # | Concept | Section | Time |
|---|---|---|---|
| C1 | Agent ROI | Agent ROI model | ~5 min |
| C2 | Compounding error | Compounding error problem | ~5 min |
| C3 | Context engineering | Context engineering & windows | ~10 min |
Each section ends with an Expected takeaway so you can self-check.
Agent ROI = Value of Output ÷ Token Cost
Key insight : increasing quality often reduces token spend.
The naive reaction to usage-based billing is "use fewer tokens." This exercise shows why that's the wrong optimization target.
💡 How tokens map to cost. GitHub Copilot prices every model per 1 million tokens, then converts that into AI credits (1 AI credit = $0.01 USD). So a model's rate of, say, $2.00 per 1M input tokens = 200 AI credits per 1M tokens. We'll keep counting in tokens below, since tokens are the thing you actually control. See Models and pricing for GitHub Copilot.
A common failure loop:
- Write a weak prompt with minimal context.
- Get a mediocre result.
- Retry until something works.
Each retry re-sends the prompt, the files, and the growing conversation, so the cheap-looking first prompt quietly becomes the most expensive path to the answer.
Imagine two developers solving the same task with the same model.
| Attempt | Tokens sent | Outcome |
|---|---|---|
| 1 | 3,000 | Wrong — too vague |
| 2 | 5,000 | Wrong — missing context |
| 3 | 7,000 | Wrong — agent guessed |
| 4 | 9,000 | ✅ Finally correct |
| Attempt | Tokens sent | Outcome |
|---|---|---|
| 1 | 6,000 | ✅ Correct (clear prompt + the 2 right files) |
Your turn — fill in the blanks:
- Total tokens for Developer A =
3,000 + 5,000 + 7,000 + 9,000= __ Tokens → __ AI credits - Total tokens for Developer B = __ Tokens → __ AI credits
- If both produce the same correct output (same "Value of Output"), whose ROI is higher, and by roughly what factor on cost?
Answer
- Developer A = 24,000 tokens → 4.8 AI credits ($0.048).
- Developer B = 6,000 tokens → 1.2 AI credits ($0.012).
- Same value of output, but A spent 4× the tokens (and 4× the AI credits). Developer B's ROI is ~4× higher, purely by investing a little more effort into the first prompt. The "cheaper" first prompt was the more expensive strategy.
- Where in your own work do you "gamble", retrying instead of improving the prompt?
- "Don't minimize tokens, maximize their value." A 6,000-token prompt that works once beats a 3,000-token prompt you run four times.
Multi-step agent workflows amplify errors. Per-step accuracy that looks "good enough" collapses over many steps.
This is the single most important reason quality matters more for agents than for one-shot chat.
If each step succeeds independently with probability p, then completing n steps in a row succeeds with probability:
That exponent is brutal. Examples:
| Per-step accuracy | After 50 steps ( |
|---|---|
| 99% | ~61% |
| 95% | ~8% |
A 4-point drop in per-step accuracy (99% → 95%) turns a probably fine workflow into a almost certainly broken one.
Use
- At p = 0.90, what is the success rate after n = 10 steps?
- At p = 0.90, what is it after n = 50 steps?
- A teammate says "my agent is 98% reliable per step, that's basically perfect." Over a 30-step refactor, what's the end-to-end success rate?
Answers
-
$0.90^{10} \approx 0.349$ → about 35%. -
$0.90^{50} \approx 0.005$ → about 0.5%, essentially never. -
$0.98^{30} \approx 0.545$ → about 55%, barely a coin flip. "98% per step" is not basically perfect once you chain 30 steps.
Two ways to keep
- Raise
p— better model choice, cleaner context, precise prompts, guardrails. (Levers 1–3 and 5.) - Lower
n— split a giant task into short, independently-verified phases so errors can't silently compound. (Lever 4: Research → Plan → Implement, plus frequent session resets.)
Deterministic guardrails (tests, linters, type-checks) effectively reset p back toward 1.0 after each step by catching errors before they propagate, which is exactly why Module 07 exists.
Quality compounds exponentially. Small per-step improvements and shorter chains produce huge swings in whether the whole task succeeds. "Good enough per step" is a trap.
Provide as little as possible, but as much as necessary.
The model is a stateless, probabilistic text engine. It has no memory between calls, the entire context is re-sent on every step. What you put in that context window (and what you leave out) determines quality.
| Too little context | Too much context |
|---|---|
| Model hallucinates missing details | Relevant signal is buried in noise |
| Invents APIs, file names, behavior | "Lost in the middle" — key info ignored |
| Confident but wrong | Slower, pricier, and often wrong |
The goal is the narrow band in the middle: exactly the files and facts needed, nothing more.
- Lost in the middle : models attend most to the start and end of a long context; information in the middle is frequently ignored.
- Recency bias : the model favors the latest inputs, so stale or contradictory earlier turns get over-weighted or forgotten.
Best practice: reset sessions often and split tasks so each context stays short and relevant.
You ask an agent to "add input validation to the createTask endpoint." Below is everything you could attach. Mark each ✅ include or ❌ exclude.
| Item | Include? |
|---|---|
routes/tasks.ts (contains createTask) |
|
The project's validation/schema utility file |
|
README.md (project intro & badges) |
|
The entire node_modules/ tree |
|
| An example of an existing validated endpoint | |
| The CI workflow YAML | |
The Task type/interface definition |
Suggested answer
| Item | Include? |
|---|---|
routes/tasks.ts (contains createTask) |
✅ |
The project's validation/schema utility file |
✅ |
README.md (project intro & badges) |
❌ |
The entire node_modules/ tree |
❌ |
| An example of an existing validated endpoint | ✅ |
| The CI workflow YAML | ❌ |
The Task type/interface definition |
✅ |
You have a long session. The crucial constraint is "never log request bodies, they contain PII." Where should it live to survive lost-in-the-middle and recency bias?
- (a) Buried in turn 3 of a 20-turn chat
- (b) In a persistent instructions file and restated in your latest prompt
- (c) Said once at the very start and never repeated
Answer
(b). Persistent instructions keep it always-present (it rides at a stable position every call), and restating it in the latest prompt exploits recency bias in your favor. Option (c) gets lost as the session grows; (a) sits in the ignored middle.
You are an editor of context, not a hoarder. Curate the minimal relevant set, put critical constraints where the model actually looks (start/end + persistent instructions), and reset often so the window stays clean.
This track teaches the workshop using the GitHub Copilot CLI (copilot). Work through the modules in order using the sample-app.
New to the ideas? Do the tool-agnostic concepts first.
- Copilot CLI installed (e.g.
npm install -g @github/copilot) — runcopilot --version - Authenticated: run
copilot, then/login - Sample app dependencies installed (
npm install) - You can start an interactive session by running
copilotfrom the repo root
| # | Module | Level |
|---|---|---|
| 00 | Setup & ROI mindset | 🟢 |
| 01 | Why quality matters | 🟢 |
| 02 | How the model thinks | 🟢 |
| 03 | Lever 1 — Model selection | 🟡 |
| 04 | Lever 2 — Context optimization | 🟡 |
| 05 | Lever 3 — Prompt engineering | 🟡 |
| 06 | Lever 4 — Workflow design | 🟡 |
| 07 | Lever 5 — Deterministic controls | 🟡 |
| 08 | Advanced controls | 🔴 |
| 09 | Power-user tips | 🔴 |
| 10 | Capstone | 🔴 |
| Action | How |
|---|---|
| Start interactive session | copilot (from the repo root) |
| In-session help | /help |
| Clear/reset context | /clear (or just exit and relaunch — a clean session) |
| Run a command inside Copilot | prefix it with !, e.g. !npm test (runs in the shell without leaving the session) |
| Check your usage | /usage (review AI credits / token usage — run it after every step) |
| Add a file to context | reference it by path in your prompt, e.g. @sample-app/src/tasks.ts |
| One-shot (non-interactive) | copilot -p "your prompt" |
| Resume a previous session | copilot --resume (or --continue) |
| Pick a model | model flag/slash command (see Module 03) |
Exact token counts aren't always shown, so track these proxy metrics per task. Run /usage after every step to watch your AI credit / token consumption move in real time, and keep a my-scorecard.md open in another window:
Module: __
Task: ______________________________________
Retries to success : ___
Agent turns / tool calls : ___
Manual correction edits : ___
Guardrails green 1st try? : yes / no
Model used : ___
/usage output:
Tokens used : ___
AI credits used : ___
What I'd do differently next time: ______________
Watch these numbers drop as you apply each lever.
| Mode | Command | Best for |
|---|---|---|
| Interactive | copilot |
Multi-step work, planning, iterating |
| Non-interactive | copilot -p "..." |
Scripted one-shots, piping output, CI-like tasks |
Non-interactive mode is a power lever for this workshop, it composes with shell pipes so you can filter context and collapse output (Module 09).
Goal: get the Copilot CLI working, run the sample app, and adopt the workshop's core principle before your first prompt.
Don't minimize tokens, maximize their value. Better quality → fewer retries → lower total cost.
copilot --versionThen start a session and authenticate if needed:
copilotInside the session:
/login # if not already signed in
/help # see available slash commands
Ask a trivial question to confirm it responds:
What can you help me with in this repo?
✅ Checkpoint: the CLI responds and /help lists commands.
In a normal terminal (not inside the Copilot session):
cd sample-app
npm install
npm run build
npm testYou should see the build succeed and one test fail on purpose (includes tasks exactly equal to the threshold). That failing test is your first quality signal, don't fix it yet.
💡 Or run it from inside Copilot. Prefix a command with ! to run it in the shell without leaving your session, handy for staying in one context:
!cd sample-app && npm install
!npm run build
!npm test
✅ Checkpoint: 3 tests pass, 1 fails.
Before each task, ask yourself:
- What's the smallest correct context? (specific files, not the whole repo)
- What does "done" look like? (a stop condition the agent can verify)
- Which model fits the difficulty?
- Can a test/lint verify it instead of me eyeballing it?
This 10-second habit is the highest-ROI thing in the workshop.
Create a scratch my-scorecard.md at the repo root and copy the template from the README. One per module.
- Run
copilotfrom the directory that matters so the agent's working context is scoped. - Use
/clear(or relaunch) between unrelated tasks, a fresh session is a free context reset. - For quick one-offs, prefer
copilot -p "..."over a long interactive session.
- The CLI works and you can use
/help. - The sample app builds with exactly one intentional failing test.
- You have a scorecard ready and the ROI questions in mind.
➡️ Next: 01 — Why quality matters
Goal: feel the "agent gambling" anti-pattern in the terminal, then do it right, and compare scorecards.
Concepts: Agent ROI · Compounding error
-
Start a fresh session from the repo root:
copilot -
Give it a deliberately weak, context-free prompt:
the tasks thing is broken, fix it -
Respond to any questions with equally vague nudges ("still broken", "nope"). Stop after 3 retries even if unsolved.
Note: Part A can occasionally succeed on the first shot, but it usually burns more tokens/credits because the agent is guessing.
Record the scorecard for Part C.
-
Exit and relaunch (or
/clear) for a clean session. -
Use a precise prompt that names the file, the bug, and a stop condition:
In @src/tasks.ts, the test "includes tasks exactly equal to the threshold" (in @src/tasks.test.ts) is failing. filterByMinPriority should treat minPriority as INCLUSIVE (>=), not exclusive (>). Fix only that function, then tell me to run `npm test` and stop. Do not change anything else. -
In a normal terminal:
npm test→ expect 4/4 pass.
Record the scorecard for Part C.
| Metric | Part A (gamble) | Part B (invest) |
|---|---|---|
| Tokens | ||
| AI Credits | ||
| Retries to success | ||
| Agent turns | ||
| Tests green? |
Part B almost always wins on every axis, fewer tokens and a correct result. The "bigger" first prompt was the cheaper path. That's the ROI principle.
Try the same correct fix as a single non-interactive command and compare the effort:
copilot --allow-all-tools --allow-all-paths -p "update tasks file"
cd sample-app && npm testThe extra flags avoid non-interactive permission blocks when the command needs to edit files.
A well-specified one-shot can solve a clear task in a single, cheap call.
Part A failed because the agent had to guess several things in a row (which file, which bug, how to verify). Each guess is a step with p < 1, and p^n collapses fast. Part B removed the guesses.
Two scorecards proving that up-front context + a stop condition beats retry-until-it-works.
➡️ Next: 02 — How the model thinks
Goal: internalize that the model is stateless and that the context window has biases, observed from the terminal.
Concept: Context engineering
- An LLM is a probabilistic text engine, it predicts likely next tokens.
- It is stateless: no memory between calls. The entire context is re-sent every step.
- Context-window biases:
- Lost in the middle : middle-of-context info is often ignored.
- Recency bias : the latest inputs dominate.
-
Start
copilot. Send:Remember the secret word: aardvark. Just acknowledge. -
Exit the session (Ctrl+D or
/exit) and start a newcopilotsession. Ask:What was the secret word? Don't search the codebase.
It won't know from the memory, a new session is a blank context. The "memory" you feel within a session is just the transcript being re-sent.
Note:
copilot --resume/--continuereloads a prior transcript, that's re-sending saved context, not the model truly remembering. Try it to see the difference.
✅ Takeaway: every fresh session starts from zero. Exploit it as a cheap reset.
-
Start
copilot. Ask it to read and summarize three files:Summarize @sample-app/src/routes.ts, @sample-app/src/tasks.ts, and @sample-app/src/store.ts — one line each. -
Follow up:
Now explain how a POST request flows through these.
The second answer implicitly re-includes everything from turn 1. In a long session this is how tokens balloon, every file and prior answer rides along on each new turn.
✅ Takeaway: long sessions get expensive and noisier. Short, focused sessions stay sharp.
-
New session. Paste a long prompt with the key rule buried in the middle:
Here are some coding standards: [8–10 lines of filler standards]. IMPORTANT: every function must return a Result type, never throw. [8–10 more lines of filler standards] Now write a TypeScript function that divides two numbers. -
Did it honor the buried "never throw" rule?
-
New session: move that rule to the end, right before the request. Compare compliance.
Here are some coding standards: [write 8–10 lines of filler standards]. [write 8–10 more lines of filler standards] Now, write a TypeScript function that divides two numbers. IMPORTANT: every function must return a Result type, never throw.
You'll usually get better adherence when the critical rule is last (recency) rather than buried in the middle.
You've seen, from the CLI: (1) sessions are stateless, (2) context accumulates per turn, and (3) placement within the window changes compliance, the basis for every lever ahead.
➡️ Next: 03 — Lever 1: Model selection
Goal: match model size to task difficulty from the CLI.
Rule of thumb: large = planning/debugging · medium = implementation · small = simple tasks · Auto by default.
Suggested starting point from GitHub's model comparison page:
- Deep reasoning/debugging (large):
GPT-5.5,GPT-5.4,Claude Opus 4.7,Gemini 2.5 Pro - General implementation (medium):
GPT-5 mini,GPT-4.1,Claude Sonnet 4.6 - Simple/repetitive tasks (small/fast):
Claude Haiku 4.5,Gemini 3 Flash
Pick one available in your org/plan. If a model isn't available, choose the closest option in the same category.
Model selection varies by CLI version. Check what your install supports:
copilot --help # look for a model flag, e.g. --modelInside a session, run /help and look for a model command (often /model). Use whichever your version exposes. If neither exists, your CLI uses an automatic default, that's fine; do the exercises by comparing task difficulty and noting outcomes.
# Example shapes (confirm against your version):
copilot --model <large-model> -p "..."
# or, inside a session:
/modelExamples:
copilot --model "GPT-5.5" -p "..." # deep reasoning/debugging
copilot --model "GPT-5 mini" -p "..." # general coding
copilot --model "Claude Haiku 4.5" -p "..." # quick/simple edits-
Start a session with a capable model (for example:
GPT-5.5,GPT-5.4,Claude Opus 4.7, orGemini 2.5 Pro). Don't let it edit yet:Don't change code. Read @sample-app/src/tasks.ts and @sample-app/src/tasks.test.ts and explain why the "exactly equal to the threshold" test fails and exactly what the fix is. Be specific about the comparison operator.
Record: did it pinpoint > vs >=? How clear was the reasoning?
-
New session with a smaller/faster model (for example:
Claude Haiku 4.5,Gemini 3 Flash, or another fast model in your list) (or a one-shot):copilot --model <small-model> -p "In sample-app/src/tasks.ts, change the comparison in filterByMinPriority from > to >=. Nothing else."
-
cd sample-app && npm test→ expect 4/4 green.
A small model handles a one-line mechanical edit perfectly, cheaper and faster.
- Ask the small model to do Exercise A's diagnosis. Did it miss nuance?
- Ask the large model to do Exercise B's one-liner. Overkill?
| Diagnosis (hard) | One-liner (easy) | |
|---|---|---|
| Large model | quality? speed? | overkill? |
| Small model | missed nuance? | fine? |
Lesson: big models earn their cost on ambiguous reasoning; small models win on well-specified mechanical work. The wrong-sized model either wastes value or causes retries.
Use the automatic/default model. Choose deliberately only when the task is clearly very hard (planning/debugging) or clearly trivial (rename, format, one-liner).
You've matched three tasks to model choices from the CLI and can justify why right-sizing maximizes token value.
➡️ Next: 04 — Lever 2: Context optimization
Goal: give the CLI agent only the relevant files and use session resets to keep context clean.
Provide as little as possible, but as much as necessary. Avoid full-repo context. Reset sessions frequently.
Concept: Context engineering
- Files you reference by path (e.g.
@sample-app/src/routes.ts) are pulled in. - The agent can also explore the working directory on its own, which is powerful but can drag in irrelevant files. Scope it by running
copilotfrom a subfolder when appropriate, or by naming the exact files.
Task: add input validation to POST /tasks.
-
Decide the minimal set first: likely just
routes.tsandtypes.ts. -
Start
copilotand reference only those:In @sample-app/src/routes.ts, validate the POST /tasks body before creating a task: - title: required, non-empty string, max 200 chars - priority: required integer 1–5 Return HTTP 400 with a clear message if invalid. Use the types in @sample-app/src/types.ts. Don't touch other endpoints. Stop after editing routes.ts.
Record the scorecard.
-
New session from the repo root. This time, tell the agent to read broadly:
Read the whole repo including the README and concepts, then add validation to POST /tasks. -
Compare: slower? Did it wander into unrelated files or restate things it didn't need?
Extra context isn't free, it dilutes signal and invites "lost in the middle" errors.
- After Exercise A, don't pile the next unrelated task into the same session.
/clearor relaunch. - Try scoping by directory:
cd sample-app && copilot, now the agent's default exploration is limited to the app, not the whole workshop repo.
Habit: one task, one session. When a task is done, reset.
- The file(s) you're changing.
- The type/interface definitions they depend on.
- One example of the pattern you want imitated (if any).
- Stop. Add more only if the agent asks or guesses wrong.
You can assemble a minimal, relevant context from the CLI, you've seen over-context hurt, and you scope sessions by directory and reset between tasks.
➡️ Next: 05 — Lever 3: Prompt engineering
Goal: turn vague asks into precise prompts with explicit context and stop conditions, which matters even more in non-interactive one-shots.
Be precise · add stop conditions · provide context explicitly.
[INTENT] What you want, in one sentence.
[CONTEXT] Which files/facts matter (@path references or stated rules).
[CONSTRAINTS] What NOT to touch; rules to follow.
[DONE WHEN] The stop condition the agent can verify.
A stop condition is the biggest single upgrade, it stops the agent from wandering into unrelated changes (which compounds errors and burns tokens). In copilot -p "..." one-shots, it's essential because you're not there to interrupt.
Weak:
make the search better
Rewrite it for GET /tasks/search in routes.ts.
Sample strong rewrite (as a one-shot)
copilot --allow-all-tools --allow-all-paths -p "In sample-app/src/routes.ts, the GET /tasks/search endpoint builds a RegExp directly from the user's q param, allowing ReDoS. Replace it with a safe case-insensitive substring match (includes on lower-cased values). Do not change other endpoints. Done when q=review still returns the 'Review pull request' task and no 'new RegExp' remains in the file."Run it, then verify the "Done when" clause:
cd sample-app && npm run build && grep -n "new RegExp" src/routes.ts # should print nothing-
One-shot without a stop condition:
copilot --allow-all-tools --allow-all-paths -p "clean up sample-app/src/tasks.ts"Inspect the diff (
cd sample-app && git diff). How far did it roam? -
Revert (
git checkout -- src/tasks.ts), then one-shot with a tight stop condition:copilot --allow-all-tools --allow-all-paths -p "In sample-app/src/tasks.ts, add a one-line JSDoc comment above topTasks describing what it returns. Change nothing else. Done when only that comment is added."
Compare scope and correction effort.
State conventions instead of hoping the agent infers them:
Follow these rules: 2-space indentation; prefer const; no `any`; functions return values rather than throwing for expected cases.
The model can't read your team wiki, explicit beats implicit.
- One clear intent
- The right files referenced (not too many)
- Explicit constraints / "don't touch X"
- A verifiable Done when stop condition
You can write precise, bounded prompts (especially for one-shots) and have seen stop conditions shrink scope creep and rework.
➡️ Next: 06 — Lever 4: Workflow design
Goal: split a non-trivial change into Research → Plan → Implement, each in its own clean session, and see why this beats one-shot "do everything."
Benefits: cleaner context · better quality · lower token usage. Shorter chains keep
p^nhigh (see compounding error).
Refactor the sample app so business logic, routing, and storage are cleanly separated, and add validation, without breaking existing tests. Too big for one prompt.
-
Start
copilot. Capable model. Ask for analysis only, and save it to a file:Don't change code. Map how a POST /tasks request flows through @sample-app/src/routes.ts, @sample-app/src/tasks.ts, @sample-app/src/store.ts, and @sample-app/src/types.ts. List the responsibilities currently mixed together and the smallest set of changes to separate routing, validation, business logic, and storage. Write your analysis to RESEARCH.md.
You now have RESEARCH.md as a durable artifact (and it cost one focused session).
-
Exit
/clearfor a clean window, or use/plan. Then:Read @RESEARCH.md and produce PLAN.md: a numbered checklist of small, independently testable steps, ordered so tests stay green throughout. Do not write code.
Review PLAN.md and edit it directly if needed, it's cheap to fix a list, expensive to redo code.
If you use /plan, point it at @RESEARCH.md, then choose option 2 (Exit plan mode and I will prompt myself) so you keep the same constraint: produce PLAN.md, stay read-only, and do not write code.
-
Exit
/clearagain. Then feed one step, with only the files it needs:From @PLAN.md, do step 1 only: extract POST /tasks validation into a validateCreateTask function in a new file sample-app/src/validation.ts, called from @sample-app/src/routes.ts. Keep behavior identical for valid input. Done when `npm run build` passes and existing tests stay green. -
After each step, verify in a normal terminal:
cd sample-app && npm run build && npm test
Only advance when green.
In a fresh session, try the whole refactor in a single prompt and compare:
| Research→Plan→Implement | One-shot | |
|---|---|---|
| Tests stayed green throughout? | ||
| Retries / corrections | ||
| Could you review before code? |
The phased approach yields reviewable diffs and fewer surprises. Bonus: RESEARCH.md/PLAN.md are reusable artifacts.
You ran a real change through Research → Plan → Implement with session resets between phases and can explain how shorter, verified steps stop errors from compounding.
➡️ Next: 07 — Lever 5: Deterministic controls
Goal: add tests, linters, and security checks so the agent's work is machine-verified, stopping errors before they compound. In the CLI these double as scriptable gates.
Deterministic guardrails effectively reset per-step accuracy toward 1.0 by catching mistakes immediately. See compounding error.
An agent that can run a command and see it fail fixes its own mistake before it propagates.
Without that signal, a wrong step becomes the foundation for the next, the p^n collapse.
Guardrails make agent output trustworthy at scale.
The sample app has no ESLint config.
copilot --allow-all-tools --allow-all-paths -p "In sample-app, add ESLint for TypeScript: install needed dev deps, create a flat eslint config that lints src/**/*.ts with recommended TypeScript rules, and make `npm run lint` work. Done when `npm run lint` runs and reports results."
cd sample-app && npm run lintTriage the findings with the agent's help. You've given every future task a new automatic check.
copilot --allow-all-tools --allow-all-paths -p "In sample-app, write Jest + supertest tests in src/routes.test.ts for POST /tasks: valid input -> 201; missing title -> 400; empty title -> 400; priority out of 1–5 -> 400. Use src/tasks.test.ts as a style reference. It's fine if some fail because validation isn't implemented yet."
cd sample-app && npm testFailing tests now define the validation to implement. Tests-first turns a fuzzy requirement into a deterministic target.
GET /tasks/search builds a RegExp from raw user input, a DoS vector.
copilot --allow-all-tools --allow-all-paths -p "In sample-app/src/routes.ts, GET /tasks/search passes user input into new RegExp(), allowing ReDoS. Replace it with a safe case-insensitive substring match. Add a test in src/routes.test.ts that a normal query still works. Done when no `new RegExp` remains and tests pass."
cd sample-app && npm test
curl "http://localhost:3000/tasks/search?q=ship"Start the server with npm run dev in another terminal for the curl.
End implementation prompts with a verifiable check, e.g.:
...Done when `npm run build`, `npm test`, and `npm run lint` all pass.
That single clause makes the agent self-correct instead of handing you broken work. In CI-like scripts you can even chain them:
copilot -p "...your task... Done when the build, tests, and lint pass." \
&& cd sample-app && npm run build && npm test && npm run lintThe sample app now has a linter, validation tests, and a fixed security hole, and "green checks" are your default stop condition.
➡️ Next: 08 — Advanced controls
Goal: make quality repeatable with persistent instructions, custom agents, skills, MCP tools, and sub-agents, so you don't re-explain context every session.
Persistent instructions · custom agents · skills (dynamic context) · MCP tools (use carefully) · sub-agents for research.
The Copilot CLI reads project instruction files (commonly AGENTS.md, and it also respects .github/copilot-instructions.md). The sample app has none, so the agent re-guesses conventions each time. Fix that:
copilot --allow-all-tools --allow-all-paths -p "Create AGENTS.md in sample-app capturing: TypeScript strict, 2-space indent, prefer const, no `any`; Express routes in src/routes.ts; business logic in src/tasks.ts; validate all request bodies and return 400 on bad input; never build a RegExp from user input; every change must keep `npm run build`, `npm test`, and `npm run lint` green. Keep it concise."Open the file, trim anything bloated (instructions are re-sent every turn, keep them tight).
Test it: new session, ask the agent to add a small endpoint. It should follow the rules without you restating them. Run /help to confirm how your CLI surfaces loaded instructions.
Example prompt to try:
Add POST /tasks/:id/complete. Put routing in src/routes.ts and business logic in src/tasks.ts. Validate request input and return 400 on invalid input. Keep the change minimal and run build, test, and lint.
Keep instructions concise, a giant instructions file is itself a context tax.
Many CLI versions support custom agents or saved prompt configurations. Use this to lock in repeatable behavior, including token discipline. Example: a tiny tdd-fixer profile for bounded bug fixes.
---
name: tdd-fixer
description: Smallest safe fix with minimal output.
---
Goal: fix only the reported failure with the smallest safe edit.
Rules:
1. Output max 5 bullets unless asked for more.
2. State the failing check in 1 line.
3. Change only files needed for that failure.
4. Run `npm test`.
5. Stop when green; do not refactor unrelated code.Save that in your CLI's custom-agent location, then launch with /tdd-fixer.
Quick test prompt:
Fix only the failing test for task completion; keep the patch minimal and stop when tests pass.
Why this helps token efficiency: you pre-commit to short output, minimal edits, and a strict stopping rule.
Skills let the agent load domain knowledge only when relevant, instead of pasting long guidance into every prompt.
Example skill (keep outside core instructions so it loads only when needed):
---
name: api-validation-rules
description: Validation and error-response rules for task endpoints.
---
Use for endpoint edits in `src/routes.ts` and `src/tasks.ts`.
Rules:
1. Validate every request body and route param.
2. Return 400 with a short error message on invalid input.
3. Never build a RegExp from user input.
4. Keep changes minimal and avoid unrelated refactors.Then ask:
/skills list
Quick test prompt:
Add POST /tasks/:id/complete. Keep output brief.
Why this helps token efficiency: heavy, domain-specific rules are loaded on demand, not carried in every turn.
The CLI can connect to MCP servers (databases, browsers, issue trackers, etc.). Inspect/manage them (check /help for an /mcp command or your CLI's MCP config file).
Caution: every enabled MCP tool adds tool definitions to the context and can take real actions. Enable only what a task needs; disable the rest. More tools = more tokens + more ways to go wrong. Treat MCP tools like context: as few as necessary.
Example prompts:
/mcp list
Use only the GitHub MCP for this task. Disable other MCP tools, then summarize open issues labeled "bug".
Before editing code, confirm which MCP tools are enabled and explain why each one is needed.
Offload heavy read-only exploration so your main session stays clean. If your CLI exposes a research/sub-agent capability, ask it to "explore the codebase and report how validation is handled." The long exploration runs in its own context; only the summary returns, keeping your primary window lean.
If no dedicated sub-agent exists, emulate it: run a separate copilot session whose only job is to produce RESEARCH.md, then feed that summary into your main work.
Example prompts:
Use a research sub-agent to scan `src/` and summarize where request validation happens. Return only: files, patterns, and gaps.
Run a sub-agent to compare validation behavior between routes and tests, then give me a 5-bullet action plan.
Do read-only exploration in a sub-agent first; after it returns, propose the smallest patch to add POST /tasks/:id/complete.
The sample app has concise persistent instructions, you've created a reusable agent/persona, and you know when skills, MCP tools, and sub-agents add value vs. noise.
➡️ Next: 09 — Power-user tips
Goal: exploit the terminal's superpower, pipes and scripts, to filter inputs, collapse outputs, and analyze usage. This is where the CLI track shines.
Use scripts to filter data · optimize CLI outputs · collapse tool calls · analyze usage patterns.
The CLI composes with the shell, so you can hand the agent a pre-filtered slice instead of a whole file:
# Feed only the relevant lines into a one-shot prompt
grep -n "RegExp" sample-app/src/*.ts | copilot -p "Explain the security risk in these lines and how to fix it. Do not edit files."
# Summarize only the failing test output, not the full log
cd sample-app && npm test 2>&1 | tail -n 20 | copilot -p "Why is this test failing and what's the minimal fix? Do not edit files."
# Just the dependency names
node -p "Object.keys(require('./sample-app/package.json').dependencies)" | copilot -p "Are any of these unnecessary for a basic Express API? Do not edit files."You decide what's relevant, the model never wades through noise.
When the agent runs commands, noisy output is tokens. Prefer quiet commands:
| Noisy | Lean |
|---|---|
npm test (full log) |
npm test 2>&1 | tail -n 20 |
cat bigfile.ts |
sed -n '40,80p' bigfile.ts |
ls -R |
git ls-files src |
Tell the agent in your prompt: "run the quietest command that proves it works."
When a session grows (and gets pricey), compress instead of extending:
Summarize what we changed and the current state in 5 bullet points.
Save that summary, start a new session, and paste it as the seed. You've turned a huge transcript into a few lines, a manual, high-leverage reset that fights "lost in the middle" and recency bias.
copilot --resumereloads a full transcript; a summary-seeded new session is often far cheaper for the same continuity.
Or let the CLI do it in one command with /compact:
/compact
/compact compresses your conversation history in place, replacing the long transcript with a summary while keeping you in the same session. The CLI also auto-compacts in the background when you approach ~95% of the token limit, so sessions can run almost indefinitely. Use /context to see a token-usage breakdown and decide when a manual /compact is worth it. Press Esc to cancel a compaction if you change your mind.
Rule of thumb:
/compactto continue the same task cheaply; a summary-seeded new session to start a clean, unrelated task.
Bundle a task with its verification so the agent's work is gated automatically:
copilot -p "Implement <change> in sample-app. Done when build, tests, and lint pass." \
&& cd sample-app && npm run build && npm test && npm run lint \
&& echo "ALL GREEN" || echo "NEEDS WORK"This is Lever 5 (deterministic controls) expressed as a one-liner.
Review your scorecards:
- Which tasks needed the most retries? Missing context, no stop condition, wrong model?
- Where did you over-stuff context?
- Which guardrail caught the most mistakes?
Promote recurring fixes into AGENTS.md (Module 08) so you never pay for that lesson twice. If your CLI exposes usage/billing info (/help or account dashboard), correlate big sessions with low-quality outcomes, those are your prime optimization targets.
You can pre-filter inputs with pipes, keep output and sessions lean, script guardrail loops,
compress context with /compact and use scorecard history to improve your defaults.
➡️ Next: 10 — Capstone
Goal: combine all five levers to ship one clean, verified feature on the sample app from the terminal, and prove the difference with your scorecard.
Add a
PATCH /tasks/:idendpoint that lets a client update a task'stitle,priority, ordonestatus. It must validate input, reject unknown IDs with 404, follow project conventions, and keep all checks green.
This touches routing, validation, business logic, and storage, a realistic multi-step change.
-
Model selection (Lever 1) : capable model for research/plan; you may drop to a smaller model (or one-shot) for mechanical edits.
-
Workflow design (Lever 4) : phases with resets between each:
- Research:
copilot→Don't change code. Analyze how updates should flow through @sample-app/src/routes.ts, @sample-app/src/store.ts, @sample-app/src/tasks.ts, and @sample-app/src/types.ts, and what the store needs to support update-by-id. Write it to RESEARCH.md. - Plan: fresh session →
Read @RESEARCH.md and write PLAN.md as a small, ordered, testable checklist. No code.Review and editPLAN.md. - Implement: fresh session → feed one step at a time.
- Research:
-
Context optimization (Lever 2) :reference only the files each phase needs;
/clearbetween phases; considercd sample-app && copilotto scope exploration. -
Prompt engineering (Lever 3) : every implement prompt includes explicit constraints and a verifiable Done when stop condition.
-
Deterministic controls (Lever 5) : write tests first in
src/routes.test.ts(valid patch → 200; unknown id → 404; invalid priority → 400), and gate each step:cd sample-app && npm run build && npm test && npm run lint
If you created AGENTS.md in Module 08, the agent should honor conventions without restating them.
-
PATCH /tasks/:idupdatestitle,priority, and/ordone - Invalid input →
400; unknown id →404; success →200 - New tests cover all three cases and pass
-
npm run build,npm test,npm run lintall green - No
any, nonew RegExpon user input, conventions followed - You completed it in phases with session resets
Fill in one last scorecard for the whole capstone, then answer:
- How many retries did this take vs. the "gamble" task in Module 01?
- Which lever saved you the most rework?
- What will you add to
AGENTS.mdso it's free next time?
- Choose the right model.
- Write clear prompts.
- Split tasks.
- Add deterministic guardrails.
- Maintain concise instructions.
👉 Make every token count.
🎉 You've finished the Copilot CLI track. Want the same lessons in the editor? Try the VS Code track.
This track teaches the workshop using GitHub Copilot in Visual Studio Code (Chat and Agent modes). Work through the modules in order, each builds on the last and uses the sample-app.
New to the ideas? Do the tool-agnostic concepts first.
- VS Code installed
- GitHub Copilot + GitHub Copilot Chat extensions installed and signed in
- Sample app dependencies installed (
npm install) - You can open the Chat view (
Ctrl+Alt+I/⌃⌘I) and see the mode dropdown (Ask · Plan · Agent) and the model picker
| # | Module | Level |
|---|---|---|
| 00 | Setup & ROI mindset | 🟢 |
| 01 | Why quality matters | 🟢 |
| 02 | How the model thinks | 🟢 |
| 03 | Lever 1 — Model selection | 🟡 |
| 04 | Lever 2 — Context optimization | 🟡 |
| 05 | Lever 3 — Prompt engineering | 🟡 |
| 06 | Lever 4 — Workflow design | 🟡 |
| 07 | Lever 5 — Deterministic controls | 🟡 |
| 08 | Advanced controls | 🔴 |
| 09 | Power-user tips | 🔴 |
| 10 | Capstone | 🔴 |
Because exact token counts aren't always visible, track these proxy metrics for each task. Copy this into a notes file and fill it in as you go:
Module: __
Task: ______________________________________
Retries to success : ___
Agent turns / tool calls : ___
Manual correction edits : ___
Guardrails green 1st try?: yes / no
Model used : ___
Tokens / AI credits used : ___
What I'd do differently next time: ______________
The whole point of the workshop: watch these numbers drop as you apply each lever.
| Mode | Use it for |
|---|---|
| Ask | Questions, explanations — no file edits |
| Plan | Research and planning: produces a plan/checklist, no code changes |
| Agent | Multi-step autonomous work: reads files, edits, runs tasks |
Choosing the right mode is itself an optimization: Ask/Plan for thinking, Agent for doing.
| Action | How |
|---|---|
| Open the Chat view | Ctrl+Alt+I / ⌃⌘I |
| Switch mode | mode dropdown: Ask · Plan · Agent |
| Pick a model | model picker in the Chat input box |
| Add a file to context | type # and pick the file |
| Clear/reset context | start a new Chat session |
| Run a command | VS Code terminal (Ctrl+`) |
| Track your usage | review token / AI credit consumption after each step |
Goal: get Copilot working in VS Code, run the sample app, and adopt the workshop's core principle before you write a single prompt.
Don't minimize tokens, maximize their value. Better quality → fewer retries → lower total cost.
- Open the Chat view:
Ctrl+Alt+I(Windows/Linux) or⌃⌘I(macOS). - Confirm you can see:
- the mode dropdown: Ask · Plan · Agent
- the model picker (e.g. "Auto", plus named models)
- In Ask mode, type:
What can you help me with in this workspace?and confirm you get a response.
✅ Checkpoint: You received a reply and can switch between Ask/Plan/Agent.
Note: The content in fimdim/sample-app enables Agent Debug Log file logging via .vscode/settings.json. To disable writing logs to disk, set
"github.copilot.chat.agentDebugLog.fileLogging.enabled": falsein your workspace settings, but you won't be able to follow this step nor see metrics easily.
After your first conversation:
- Click
Show Agent Debug Logsin the three-dot menu next to the gear icon in the upper-right corner (Views and more actions...), or open the Command Palette and runDeveloper: Open Agent Debug Logs. - Click the conversation title to see metrics such as turns, token usage, tool calls, etc.
- If you don't see any numbers, restart your VS Code instance.
✅ Checkpoint: You can see your token usage for every conversation in the Agent Debug Logs.
See Agent Debug Log Panel for guidance.
In a VS Code terminal (Ctrl+`):
npm install
npm run build
npm testYou should see the build succeed and one test fail on purpose (includes tasks exactly equal to the threshold). That failing test is your first quality signal, don't fix it yet.
✅ Checkpoint: 3 tests pass, 1 fails.
Before each task in this workshop, pause and ask:
- What's the smallest correct context for this task? (not the whole repo)
- What does "done" look like? (a stop condition the agent can check)
- Which model fits the difficulty?
- Can a test/lint verify it instead of me eyeballing it?
This 10-second habit is the highest-ROI thing in the whole workshop.
Create a scratch file my-scorecard.md at the repo root (it's just for you). Copy the scorecard template from the track README. You'll fill one in per module.
- Start a new Chat between unrelated tasks, a fresh session is a free context reset.
- Add only the files that matter, not whole folders.
- Use Ask mode for thinking and Agent mode for doing, don't burn Agent turns on a question.
- Copilot Chat works and you can switch modes/models.
- The sample app builds, with exactly one intentional failing test.
- You have a scorecard ready and the ROI questions in mind.
➡️ Next: 01 — Why quality matters
Goal: feel the cost of the "agent gambling" anti-pattern by deliberately doing it, then doing it right, and compare the scorecards.
Concepts: Agent ROI · Compounding error
-
New Chat session, Agent mode, model Auto.
-
Give it this deliberately weak prompt, no file context, vague ask:
the tasks thing is broken, fix it -
Whatever it does, resist helping. If it asks questions or guesses wrong, give equally vague nudges ("still broken", "no that's wrong"). Stop after 3 retries even if unsolved.
Note: Part A can occasionally succeed on the first shot, but it usually burns more tokens/credits because the agent is guessing.
Record the scorecard for Part C.
-
Start a brand-new Chat session (clean context, important).
-
Agent mode. Add precise context: drag and drop
sample-app/src/tasks.tsandsample-app/src/tasks.test.tsinto the chat. -
Use a precise prompt with a stop condition:
In sample-app, the test "includes tasks exactly equal to the threshold" is failing. The function filterByMinPriority should treat minPriority as INCLUSIVE (>=), not exclusive (>). Fix only that function, then tell me to run `npm test` and stop. Do not change anything else. -
Run
npm testin the terminal. Confirm 4/4 pass.
Record the scorecard for Part C.
| Metric | Part A (gamble) | Part B (invest) |
|---|---|---|
| Tokens / AI credits used | ||
| Retries to success | ||
| Agent turns | ||
| Tests green? |
Reflection: Part B almost always wins on every axis, fewer tokens and a correct result. That's the ROI principle in action: the "bigger" first prompt was the cheaper path.
Part A failed partly because the agent had to guess several things in a row (which file, which bug, how to verify). Each guess is a step with p < 1, and p^n collapses fast. Part B removed the guesses, pushing each step's p toward 1.0.
You have two scorecards proving that an up-front investment in context + a stop condition beats retry-until-it-works on retries, turns, and correctness.
➡️ Next: 02 — How the model thinks
Goal: internalize that the model is stateless and that the context window has biases, then observe both directly in VS Code.
Concept: Context engineering
- An LLM is a probabilistic text engine, it predicts likely next tokens.
- It is stateless: no memory between calls. The entire context is re-sent every step.
- The context window has two biases:
- Lost in the middle : info in the middle of a long context is often ignored.
- Recency bias : the latest inputs dominate.
-
New Chat (Ask mode). Send:
Remember the secret word: aardvark. Just acknowledge. -
Now open a brand-new Chat session and ask:
What was the secret word? Don't search the codebase.
It won't know, the new session is a blank context. Memory you feel in a session is just the transcript being re-sent, not the model remembering.
✅ Takeaway: every new session starts from zero. That's a feature you'll exploit (cheap resets).
- New Chat (Agent mode). Ask it to read three files (add
sample-app/src/routes.ts,sample-app/src/tasks.ts,sample-app/src/store.ts) and summarize each. - Then ask a follow-up: "now explain how a POST request flows through these."
Notice the second answer implicitly re-includes everything from turn 1. In a long session this is how tokens balloon, every file and every prior answer rides along on each new turn.
✅ Takeaway: long sessions get expensive and noisier. Short, focused sessions stay sharp.
-
New Chat (Ask mode). Paste a long prompt where a key instruction is buried in the middle:
Here are some coding standards: [write 8–10 lines of filler standards]. IMPORTANT: every function must return a Result type, never throw. [write 8–10 more lines of filler standards] Now, write a TypeScript function that divides two numbers. -
See whether it honored the buried "never throw / return a Result" rule.
-
Try again in a new session, but put that rule last, right before the request. Compare:
Here are some coding standards: [write 8–10 lines of filler standards]. [write 8–10 more lines of filler standards] Now, write a TypeScript function that divides two numbers. IMPORTANT: every function must return a Result type, never throw.
You'll usually get better compliance when the critical rule is at the end (recency) rather than buried in the middle.
You've seen, hands-on: (1) sessions are stateless, (2) context accumulates per turn, and (3) placement within the window changes compliance. These three facts justify every lever that follows.
➡️ Next: 03 — Lever 1: Model selection
Goal: match model size to task difficulty using the VS Code model picker, and feel the tradeoff between capability, speed, and cost.
Rule of thumb: large = planning/debugging · medium = implementation · small = simple tasks · Auto by default.
Suggested starting point from GitHub's model comparison page:
- Deep reasoning/debugging (large):
GPT-5.5,GPT-5.4,Claude Opus 4.7,Gemini 2.5 Pro - General implementation (medium):
GPT-5 mini,GPT-4.1,Claude Sonnet 4.6 - Simple/repetitive tasks (small/fast):
Claude Haiku 4.5,Gemini 3 Flash
Pick one available in your org/plan. If a model isn't available, choose the closest option in the same category.
In the Chat input box there's a model dropdown. You'll typically see Auto plus several named models of varying capability. Auto lets Copilot route the request, a great default, but choosing deliberately is a skill worth practicing.
The sample app's tasks.ts has a subtle correctness bug.
-
New Chat, Ask mode, pick a large/most-capable model.
-
Add
tasks.tsandtasks.test.ts. -
Prompt:
Don't change code yet. Explain why the "exactly equal to the threshold" test fails, and what the correct fix is. Be specific about the comparison operator.
Record: did it pinpoint > vs >=? How clear was the reasoning?
-
New Chat, Agent mode, pick a smaller/faster model.
-
Add
tasks.ts. -
Prompt:
Change the comparison in filterByMinPriority from `>` to `>=`. Nothing else. -
Run
npm test. Expect 4/4 green.
A small model is perfectly capable of a one-line mechanical edit, and it's cheaper/faster.
Repeat Exercise A's diagnosis with a small model, and Exercise B's one-liner with a large model. Note in your scorecard:
| Diagnosis (hard) | One-liner (easy) | |
|---|---|---|
| Large model | quality? speed? | overkill? |
| Small model | missed nuance? | fine? |
Lesson: big models earn their cost on ambiguous reasoning; small models win on well-specified mechanical work. Using a big model for a one-liner wastes value; using a tiny model for a gnarly bug causes retries (which costs more overall).
Leave it on Auto. Reach for a specific model when you know the task is unusually hard (reasoning/planning/debugging) or unusually trivial (rename, format, one-line change).
You've consciously matched three tasks to three model choices and can articulate why the right-sized model maximizes token value.
➡️ Next: 04 — Lever 2: Context optimization
Goal: practice giving the agent only the relevant files, and use session resets to keep context clean.
Provide as little as possible, but as much as necessary. Avoid full-repo context. Reset sessions frequently.
Concept: Context engineering
Task: add input validation to POST /tasks.
-
New Chat, Agent mode.
-
First, decide the minimal set. Which files matter? Likely:
sample-app/src/routes.ts(the endpoint)sample-app/src/types.ts(theCreateTaskInput/Prioritytypes)
-
Add only those two. Prompt:
In POST /tasks, validate the request body before creating a task: - title: required, non-empty string, max 200 chars - priority: required integer 1–5 Return HTTP 400 with a clear message if invalid. Don't touch other endpoints. Stop after editing routes.ts and tell me to run the server.
Record the scorecard.
- New Chat, Agent mode.
- This time add a lot of irrelevant context: the whole
sample-appfolder, the workshopREADME.md, theconcepts/files, then give the same prompt as Exercise A. - Compare: was the answer slower? Did it wander into unrelated files? Did it restate things it didn't need?
You'll usually see more drift and noise. Extra context isn't free, it dilutes the signal and invites "lost in the middle" mistakes.
- After finishing Exercise A, don't keep piling new unrelated tasks into that session.
- Start a fresh Chat for the next task. Notice how a clean window means you re-supply only what's relevant, and the agent isn't anchored to earlier, now-irrelevant decisions.
Habit: one task, one session. When a task is done, reset.
- The file(s) you're changing.
- The type/interface definitions they depend on.
- One example of the pattern you want imitated (if any).
- Stop. Add more only if the agent asks or guesses wrong.
You can assemble a minimal, relevant context for a real change, you've seen over-context hurt, and you've adopted the one-task-one-session reset habit.
➡️ Next: 05 — Lever 3: Prompt engineering
Goal: turn vague asks into precise prompts with explicit context and stop conditions.
Be precise · add stop conditions · provide context explicitly.
[ROLE/INTENT] What you want, in one sentence.
[CONTEXT] Which files/facts matter.
[CONSTRAINTS] What NOT to touch; rules to follow.
[DONE WHEN] The stop condition the agent can verify.
A stop condition is the single biggest upgrade most people can make. It stops the agent from "helpfully" wandering into unrelated changes (which compounds errors and burns tokens).
Weak prompt:
make the search better
Rewrite it for the GET /tasks/search endpoint in routes.ts. Aim for all four parts above.
Sample strong rewrite
In sample-app/src/routes.ts, the GET /tasks/search endpoint builds a RegExp directly
from the user's `q` query param, which allows ReDoS. Replace the regex matching with a
safe case-insensitive substring match (String.prototype.includes on lower-cased values).
Do not change any other endpoint. Done when: searching `q=review` still returns the
"Review pull request" task and no `new RegExp` remains in the file.
Run it (Agent mode, add only routes.ts). Verify the behavior in the "Done when" clause.
-
New Chat, Agent mode. Add
tasks.ts. -
Prompt without a stop condition:
clean up tasks.tsObserve how far it roams (renames? comments? reformat? touches other files?).
-
New Chat. Same file. Prompt with a tight stop condition:
In tasks.ts, add a one-line JSDoc comment above topTasks describing what it returns. Change nothing else. Done when only that comment is added.
Compare scope, edits, and your correction effort.
Instead of "you know the conventions", state them:
Follow these rules: use 2-space indentation; prefer `const`; no `any`; functions return
values rather than throwing for expected cases.
Notice the model can't read your mind or your team wiki, explicit beats implicit every time.
- One clear intent
- The right files added (not too many)
- Explicit constraints / "don't touch X"
- A verifiable Done when stop condition
You can convert vague requests into precise, bounded prompts, and you've seen stop conditions visibly shrink scope creep and rework.
➡️ Next: 06 — Lever 4: Workflow design
Goal: split a non-trivial change into Research → Plan → Implement, each with its own clean context, and see why this beats one-shot "do everything."
Benefits: cleaner context · better quality · lower token usage. Shorter chains keep
p^nhigh (see compounding error).
Refactor the sample app so business logic, routing, and storage are cleanly separated, and add proper validation, without breaking the existing tests. That's too big for one prompt.
-
New Chat, Agent mode, capable model.
-
Prompt:
Don't change any source code. Map how a POST /tasks request flows through these files and list the responsibilities that are currently mixed together. Identify the smallest set of changes to separate routing, validation, business logic, and storage. Write your analysis to RESEARCH.md.
Agent mode can write files, so it saves RESEARCH.md directly, the "Don't change any source code" instruction keeps it read-only against the app. You now have a durable, reusable research artifact.
-
New Chat (reset!). Stay in Agent mode. Add
RESEARCH.mdas context. -
Prompt:
Read RESEARCH.md and write PLAN.md: a numbered checklist of small, independently testable steps, ordered so tests stay green throughout. Don't change any source code yet.
Agent mode writes PLAN.md for you while the instruction keeps it from touching the app. Review PLAN.md before spending tokens on implementation, it's cheap to fix a list, expensive to redo code.
Note: VS Code also has a dedicated Plan mode that's purpose-built for this phase and stays read-only automatically (no "don't change code" reminder needed), but it renders the plan in chat rather than writing a file. We use Agent mode in all three phases here so you learn the underlying workflow, Research → Plan → Implement, with explicit control and durable
RESEARCH.md/PLAN.mdartifacts. Once the pattern is second nature, reach for Plan mode to do Phase 2 with less ceremony.
-
New Chat (reset again). Switch to Agent mode.
-
Add
PLAN.mdplus only the files the first step needs, then prompt one step. Example:From PLAN.md, do step 1 only: extract input validation for POST /tasks into a validateCreateTask function in a new file src/validation.ts, and call it from routes.ts. Keep behavior identical for valid input. Done when `npm run build` passes and existing tests stay green. -
Run
npm run buildandnpm testafter each step. Only advance when green.
Because the plan lives in PLAN.md, the reset doesn't lose it, each Agent step reads the file instead of relying on a discarded chat.
For contrast, in a fresh session try the whole refactor in a single prompt ("separate everything and add validation"). Compare:
| Research→Plan→Implement | One-shot | |
|---|---|---|
| Tests stayed green throughout? | ||
| Retries / corrections | ||
| Could you review before code? |
The phased approach almost always produces cleaner diffs you can actually review, with fewer surprises.
You ran a real change through Research → Plan → Implement with resets between phases, and you can explain how shorter, verified steps keep error from compounding.
➡️ Next: 07 — Lever 5: Deterministic controls
Goal: add tests, linters, and security checks so the agent's work is verified by machines, not vibes, stopping errors before they compound.
Deterministic guardrails effectively reset per-step accuracy back toward 1.0 by catching mistakes immediately. See compounding error.
An agent that can run a test and see red will fix its own mistake before it propagates. Without that signal, a wrong step silently becomes the foundation for the next step, the classic p^n collapse. Guardrails are how you make agent output trustworthy at scale.
The sample app has no ESLint config (npm run lint can't enforce anything yet).
-
New Chat, Agent mode. Add
package.jsonandtsconfig.json. -
Prompt:
Add ESLint for this TypeScript project: install the needed dev dependencies, create a flat eslint config that lints src/**/*.ts with the recommended TypeScript rules, and make `npm run lint` work. Done when `npm run lint` runs and reports results. -
Run
npm run lint. Fix or triage what it flags (with the agent's help).
You just gave every future agent task a new automatic check.
-
New Chat, Agent mode. Add
routes.tsand the existingtasks.test.tsas a style reference. -
Prompt:
Write Jest + supertest tests for POST /tasks covering: valid input returns 201; missing title returns 400; empty title returns 400; priority out of 1–5 returns 400. Put them in src/routes.test.ts. Done when the tests run; it's fine if some fail because validation isn't implemented yet. That's the point. -
Run
npm test. Failing tests now define the validation you (or the agent) must make pass. Tests-first turns a fuzzy requirement into a deterministic target.
GET /tasks/search builds a RegExp from raw user input, a denial-of-service vector.
-
New Chat, Agent mode. Add only
routes.ts. -
Prompt:
The GET /tasks/search endpoint passes user input straight into new RegExp(), which allows ReDoS. Replace it with a safe case-insensitive substring match. Add a test in src/routes.test.ts that a normal query still works. Done when no `new RegExp` remains and tests pass. -
Confirm with
npm testand a manualcurl "http://localhost:3000/tasks/search?q=ship".
Start the server with npm run dev in another terminal for the curl.
From now on, end implementation prompts with a verifiable check, e.g.:
...Done when `npm run build`, `npm test`, and `npm run lint` all pass.
That single clause makes the agent self-correct instead of handing you broken work.
The sample app now has a linter, validation tests, and a fixed security hole, and you've made "green checks" your default stop condition.
➡️ Next: 08 — Advanced controls
Goal: make quality repeatable with persistent instructions, custom agents, skills, MCP tools, and sub-agents, so you don't re-explain context every session.
Tools covered:
copilot-instructions.md· custom agents · skills (dynamic context) · MCP tools (use carefully) · sub-agents for research.
The sample app has no project instructions, so the agent re-guesses conventions each time. Fix that.
-
New Chat, Agent mode.
-
Prompt:
Create .github/copilot-instructions.md for the sample-app. Capture: TypeScript strict, 2-space indent, prefer const, no `any`; Express routes live in src/routes.ts; business logic in src/tasks.ts; validate all request bodies and return 400 on bad input; never build a RegExp from user input; every change must keep `npm run build`, `npm test`, and `npm run lint` green. Keep it concise. -
Open the file, trim anything bloated (instructions are re-sent every turn, keep them tight).
-
Test it: new Chat, ask the agent to add any new endpoint. Notice it now follows the rules without you restating them. That's persistent context working for you.
Example prompt to try:
Add POST /tasks/:id/complete. Put routing in src/routes.ts and business logic in src/tasks.ts. Validate request input and return 400 on invalid input. Keep the change minimal and run build, test, and lint.
Keep instructions concise. A 300-line instructions file is itself a context tax.
Create a focused agent that locks in repeatable behavior, including token discipline. Example: a tiny tdd-fixer agent for bounded, test-driven bug fixes.
- Command Palette → Chat: New Custom Agent.
- Create one named
tdd-fixerwith content like:
---
name: tdd-fixer
description: Smallest safe fix with minimal output.
---
Goal: fix only the reported failure with the smallest safe edit.
Rules:
1. Output max 5 bullets unless asked for more.
2. State the failing check in 1 line.
3. Change only files needed for that failure.
4. Run `npm test`.
5. Stop when green; do not refactor unrelated code.- Use it to fix any remaining issue in the app and notice the consistent, bounded behavior.
A custom agent is a saved bundle of your best prompt habits, reusable token value. Why this helps token efficiency: you pre-commit to short output, minimal edits, and a strict stopping rule.
Quick test prompt:
Fix only the failing test for task completion; keep the patch minimal and stop when tests pass.
Skills let the agent pull in domain knowledge only when relevant, instead of you pasting it every time. Explore what's available:
- Ask in Chat:
What skills are available and when would each be used? - Note how a skill is loaded on demand, this is context engineering at the tooling level: the heavy instructions live in a file and enter the window only when needed.
Example skill file (kept out of core instructions so it loads only when relevant):
---
name: api-validation-rules
description: Validation and error-response rules for task endpoints.
---
Use for endpoint edits in `src/routes.ts` and `src/tasks.ts`.
Rules:
1. Validate every request body and route param.
2. Return 400 with a short error message on invalid input.
3. Never build a RegExp from user input.
4. Keep changes minimal and avoid unrelated refactors.MCP servers give the agent extra tools (databases, browsers, issue trackers, etc.).
- Review your MCP configuration (Command Palette → search MCP).
- Caution: each enabled MCP tool adds tool definitions to the context and can take real-world actions. Enable only what a task needs; disable the rest. More tools = more tokens + more ways to go wrong.
Treat MCP tools like context: as few as necessary.
Offload heavy read-only exploration to a sub-agent so your main session stays clean.
- Ask the agent to "explore the codebase and report how validation is currently handled" using a sub-agent capability.
- The sub-agent's long exploration happens in its own context; only the summary returns to your main session, keeping your primary window lean.
Example prompts:
Use a sub-agent to scan `src/` and summarize where request validation happens. Return only: files, patterns, and gaps.
Do read-only exploration in a sub-agent first; after it returns, propose the smallest patch to add POST /tasks/:id/complete.
The sample app has concise persistent instructions, you've built a reusable custom agent, and you understand when skills, MCP tools, and sub-agents add value vs. add noise.
➡️ Next: 09 — Power-user tips
Goal: squeeze more value per token with filtering, collapsed tool output, and usage awareness.
Use scripts to filter data · optimize outputs · collapse tool calls · analyze usage patterns.
Don't make the agent read a giant file to find one thing. Pre-filter in the terminal, then paste only the result.
Examples (VS Code terminal):
# Only the lines that matter, instead of the whole file
grep -n "RegExp" sample-app/src/*.ts
# Just the dependency names, not the whole package.json
node -p "Object.keys(require('./sample-app/package.json').dependencies)"
# A test summary instead of full output
cd sample-app && npm test 2>&1 | tail -n 20Paste the small result into Chat. You decide what's relevant, not the model wading through noise.
When the agent runs commands, noisy output is tokens. Prefer commands that emit only what's needed:
| Noisy | Lean |
|---|---|
npm test (full log) |
`npm test 2>&1 |
cat bigfile.ts |
sed -n '40,80p' bigfile.ts |
ls -R |
git ls-files src |
Ask the agent in your prompt to "run the quietest command that proves it works."
When a session gets long (and expensive), don't keep extending it:
- Ask:
Summarize what we changed and the current state in 5 bullet points. - Copy that summary, start a new Chat, and paste it as the seed. You've compressed a huge transcript into a few lines, a manual, high-leverage context reset.
This directly fights "lost in the middle" and recency bias from long histories.
/compactworks here too. Type/compactin the Chat input to compress the current conversation in place. Copilot also auto-summarizes older turns as a conversation grows. Use/compactto keep working the same task cheaply, and a New Chat (+) seeded with a tight summary when you're starting something unrelated.
Reflect on the scorecards you've collected:
- Which tasks needed the most retries? What was missing: context, a stop condition, the wrong model?
- Where did you over-stuff context?
- Which guardrail caught the most agent mistakes?
Turn recurring fixes into persistent instructions (Module 08) so you never pay for that lesson twice.
End every implementation prompt with its verification so the agent self-gates instead of handing you broken work:
Implement <change> in sample-app. Done when `npm run build`, `npm test`, and `npm run lint`
all pass.
Then confirm in the terminal:
cd sample-app && npm run build && npm test && npm run lintThis is Lever 5 (deterministic controls) wired straight into your prompt loop.
- Ask for thinking; Agent for doing. Don't burn Agent turns on a question.
- Reach for a big model only when reasoning is genuinely hard.
- Reset early and often.
You can pre-filter inputs, keep tool output and sessions lean, compress context with /compact or reset before it bloats, review chat history for recurring fixes, and use your scorecard history to improve your defaults.
➡️ Next: 10 — Capstone
Goal: combine all five levers to ship one clean, verified feature on the sample app, and prove the difference with your scorecard.
Add a
PATCH /tasks/:idendpoint that lets a client update a task'stitle,priority, ordonestatus. It must validate input, reject unknown IDs with 404, follow project conventions, and keep all checks green.
This touches routing, validation, business logic, and storage, a realistic, multi-step change.
Treat this as a graded exercise. Hit every lever consciously:
-
Model selection (Lever 1)
- Use a capable model for the research/plan phases; you may drop to a smaller model for mechanical edits.
-
Workflow design (Lever 4) : do it in phases, resetting between each:
- Research (Ask): add
routes.ts,store.ts,tasks.ts,types.ts. Ask how updates should flow and what's missing in the store to support an update-by-id. Write the analysis toRESEARCH.md. - Plan (Plan mode): new session, read
RESEARCH.mdand writePLAN.md: a small, ordered, testable checklist. Review it before any code. - Implement (Agent): new session, one step at a time.
- Research (Ask): add
-
Context optimization (Lever 2)
- In each phase, add only the files that phase needs. Reset between phases.
-
Prompt engineering (Lever 3)
- Every implementation prompt must include explicit constraints and a verifiable Done when stop condition.
-
Deterministic controls (Lever 5)
- First write/extend tests in
src/routes.test.tsdescribing the new behavior (valid patch → 200; unknown id → 404; invalid priority → 400). - End every implement step with:
Done when npm run build, npm test, and npm run lint pass.
- First write/extend tests in
If you built .github/copilot-instructions.md in Module 08, the agent should already honor conventions without restating them.
-
PATCH /tasks/:idupdatestitle,priority, and/ordone - Invalid input →
400; unknown id →404; success →200 - New tests cover all three cases and pass
-
npm run build,npm test,npm run lintall green - No
any, nonew RegExpon user input, conventions followed - You completed it in phases with session resets
Fill in one last scorecard for the whole capstone, then answer:
- How many retries did this take vs. the "gamble" task back in Module 01?
- Which lever saved you the most rework?
- What will you turn into a persistent instruction so it's free next time?
- Choose the right model.
- Write clear prompts.
- Split tasks.
- Add deterministic guardrails.
- Maintain concise instructions.
👉 Make every token count.
🎉 You've finished the VS Code track. Want the same lessons from the terminal? Try the Copilot CLI track.