Knowledge / Article

Recursive Agent Loops: How I Keep AI Agents From Drifting

The model can change. The standard cannot.

Problem

Agents are quick to say done before you have used what they built.

Practice

Write down the job, check the real product, and keep unfinished work visible.

Takeaway

The model can change. The standard cannot.

You give an agent a task, go make coffee, and come back to a cheerful message: “Done.”

Then you open the product. The bug is still there. Or it fixed the wrong problem. Or it changed fourteen files for what should have been a two-line edit.

If you use agents to build anything, you have probably had some version of this morning. It happens to vibe coders, experienced engineers, and founders trying to move quickly.

The problem is not that agents cannot build. They can. The problem is that they are far too comfortable judging their own work.

Mine once spent a night rebuilding a feature we had already built and merged. A stale tracker said “not started.” Git said finished. The agent trusted the tracker.

That was when I found the real problem. The code, the tests, the tracker, and the final report were telling four different stories. I started calling that gap drift.

At the time, I was building an unreleased workforce-management product. It handles schedules, approvals, payroll rules, permissions, and audit history. A small mistake can change someone's pay or show data to the wrong company.

I am keeping the product name and a few details private until it is ready. The mistakes and lessons in this article are exactly what happened.

Why this happens

The codebase was moving quickly. Several agents were working across backend services, interface work, tests, project notes, and release preparation.

The speed helped. The handoffs did not.

A feature was called finished because the code compiled. A placeholder test was described as end to end. A branch with no failing checks was called green, even though none of the checks had run.

The agents were often good builders. They were simply poor judges of whether their own work was finished.

I began asking four questions after every piece of work:

  • /What changed?
  • /How did we check it?
  • /What still does not work?
  • /Where does a person need to take over?

The loop grew from those questions. Its job is to keep the work and the report telling the same story.

The simple model

I eventually reduced my way of working to four lines:

text
The agent makes a judgment.
A person or script chooses what happens next.
Project files remember what happened.
The real product decides whether it works.

That change helped more than another complicated prompt.

An agent is useful when I ask, “Given what is true right now, what should happen next?” It is less reliable when I also ask it to run the whole job and decide when its own work is finished.

So I ask for one next move at a time. Sometimes I choose what runs next. Sometimes a small script does. Either way, the agent that wrote the code does not get the final vote.

The loop I use

The diagram looks tidy. Real work rarely is. What matters is being clear about what each step means.

Choose the task. I start with the current project note, then check the code and Git history. A tracker is a clue, not the truth. If the work already exists, I write that down and move on.

I learned this after our tracker showed several finished payroll and platform features as open. Looking at the repository first would have saved an entire duplicate build.

The cheapest round is the one I do not run.

Set the boundaries. Before changing code, I write down the promise, what the agent may touch, and what it must leave alone. If the promise changes, I stop and change it deliberately.

Payroll, permissions, company boundaries, and audit history need more checking than a small visual correction. I match the care to the consequence, not to how easy the task sounds.

This stops a small fix from becoming forty changed files and an unexpected refactor.

Build. The agent works on the agreed job and nothing else. If it discovers a larger problem, it reports the problem instead of quietly adding it to the work.

I learned to name the forbidden areas too. “Do not change the API shape” can be more useful than a long list of files the agent may edit.

Try it for real. This is the part I refuse to delegate back to the builder. I read the change, run the right checks, and use the feature where it actually lives.

For this product, that may include:

  • /Backend and frontend builds run from the current branch.
  • /Tests that cover the difficult case, not only the happy path.
  • /A live page or API check after restarting the current build.
  • /Permission, company, and audit checks for sensitive changes.
  • /A separate calculation for payroll or other regulated rules.

One mistake cost me hours. An API kept returning an old validation error, so I chased the new code as if it were broken. The running service was stale. Restarting the current branch fixed it.

Since then, “live” means rebuilt and restarted from the code I am reviewing. A process listening on the right port proves very little.

I also learned that nothing red does not mean green. One integration branch did not run CI at all. The quiet screen looked reassuring until I checked whether a job had started.

That left me with a question I still ask: did it pass, or did nothing run?

Decide. If the change works and the required checks pass, I commit it. If something fails, it goes back with the exact failure named.

I do not move sensitive problems into a vague “later” list. If a change affects pay, access, company data, or audit history, the difficult case is part of the job.

Write it down. I add the commit, the checks I ran, and anything unfinished to the project note. If the result exists only in chat, I assume the next agent will never see it.

Unfinished work stays unfinished. I would rather have an honest follow-up than a polished summary hiding a gap.

Stop or go again. I compare what now works with the goal. If another job remains, I choose it. If the goal is met, the loop stops before anything that needs a person's approval.

Deployments, secrets, production data, and irreversible changes remain human decisions. An agent can prepare the work. It does not get to press every button.

One action at a time

During each round, I ask the agent for one small response:

json
{
  "next": "read | plan | change | test | review | stop",
  "why": "why this should happen next",
  "touch": ["files or areas involved"],
  "check": ["what should be true afterwards"]
}

The agent suggests the move. I, or a small script, decide whether to run it.

This makes mistakes much easier to understand. I can inspect one decision instead of unwinding a long conversation that mixed reading, editing, testing, and reporting.

Each response has one job. Reading changes nothing. Planning sets the boundaries. Editing stays inside them. Testing returns the actual result. Reviewing compares the request with what changed.

The tracker changes only after I have checked the work. “Done” is accepted only when the original request is actually satisfied.

This did not begin as a platform. It began as a checklist, a few Markdown files, and the habit of asking for one next move.

Files are the memory

Chat is useful context. It is terrible project memory.

For larger jobs, I keep a small folder with the goal, what I found, the boundaries, important choices, test results, and a final summary.

A new agent can read those files and understand the work without trusting the previous agent's version of events.

I keep it proportional. A one-file fix may need one progress note and the test output. A change across permissions and payroll needs a better trail.

The point is not paperwork. The point is having a record that survives the conversation.

This matters even more when several agents are involved. One agent's summary becomes the next agent's starting point. Shared files are the only handoff I trust.

Documented does not mean followed

Writing the rules down was supposed to end the reminders. It did not.

I keep the important rules in project files every agent reads at the start of a session. Do not seed demo data. Run local checks before CI. Regenerate the knowledge pack before committing.

These are not suggestions buried in an old chat. They are in the repository, in files that exist for exactly this purpose.

The agent reads them. Then it does the thing anyway.

I have reminded agents about the same documented rule in the morning and again after lunch. One rule about running checks locally existed because ignoring it had already wasted a day of CI time.

The next agent read that file, said the right things, and pushed without running the checks. The apology was excellent. The behavior was identical.

I stopped treating this as a discipline problem and started treating it as a physics problem.

A rule written once is one line inside everything else the agent is holding. The task in front of it is loud. The rule from last month is quiet. Quiet loses.

Every new session starts the fight again because the agent that learned the lesson is not the agent that shows up tomorrow.

Three things actually moved the needle.

I keep fewer rules and state them harder. A long handbook gets skimmed. Five binding lines with consequences get respected.

I also include the receipt. “This rule exists because it cost us a day” sticks better than a polite reminder with no history behind it.

I put the rule where the mistake happens. A warning in a central file loses to a comment on the dangerous line. Both lose to a check that fails the build and explains why.

My best-behaved rules are not the best written ones. They are the ones a script enforces.

I count the reminders too. The first reminder is normal. The second is information. If I have to repeat a documented rule, documentation is not the fix. I turn it into a guard.

That same lesson changed how I push code.

Before I push a branch, I run the GitHub Actions jobs locally with act. Two checks do not mirror GitHub cleanly there, so I run those natively. Green means the checks actually ran.

Then I run two local reviews. Copilot reads the changed files and the code around them first. AGY reviews the same work with a different model.

I fix what they find, then run both reviewers again on the final diff. Any fix also goes back through the checks it could affect.

Only then do I push and spend GitHub Actions minutes. Actions and the pull request's Copilot review get one clean candidate instead of a trail of hopeful attempts.

If either finds something new, I batch the fixes into the next push and repeat the loop. I do not push one small guess at a time just to ask CI whether it worked.

This has saved a surprising amount of time. It also stopped the push, fail, fix, and push again cycle that used to turn one branch into an afternoon.

The honest summary is simple. Documentation gives an agent the chance to comply. Enforcement gives it no choice.

I keep files for judgment calls and move mechanical rules into checks. A rule that depends on being remembered will eventually meet an agent that does not remember it.

Split the work, keep one owner

Parallel work became useful only after I made each helper's job small and clear.

  • /A helper does not declare the whole job finished.
  • /A helper returns what changed and what it checked.
  • /A helper cannot quietly take on extra work.
  • /A helper says when it is unsure instead of guessing.
  • /A helper stops when its assigned job is complete.

One agent can build while another reviews. A third can update the project note. The main agent still compares their results and owns the final answer.

Without one owner, several agents only create several versions of the truth faster.

The model can change. The standard cannot.

I use different models for different jobs. A fast model can handle a small edit. A stronger one can review a risky change. The model is replaceable. The standard is not.

text
Model can change.
Standard cannot.

This changed how I think about long agent runs. I do not need every builder to be equally capable. I need every change to face the same honest checks.

A weaker builder may need another attempt. That is fine. Weak checks let bad work pass with a better-sounding report. That is not fine.

Knowing when to stop

A loop that cannot stop is not working. It is stuck.

Mine stops and asks for help when:

  • /The next step needs a product decision.
  • /A choice could affect pay, access, safety, or accessibility.
  • /The next action is destructive or cannot be undone.
  • /The same job fails the same check two or three times.

Repeated failure tells me something. I would rather get a clear explanation than watch an agent spend another hour trying random variations.

The loop also needs a clear goal. “Keep improving” is not a goal. A short list of outcomes another person can check is.

Words that mean one thing

Replace “done” with

Built

code exists

Checked

behavior works

Merged

on main

Finished

promise met

Partial

useful but incomplete

A surprising amount of confusion came from one overloaded word: done.

I replaced it with five clearer states. Built means the code exists. Checked means I used it or tested the real behavior. Merged means it reached the main branch. Finished means the original promise is met.

Partial means useful work exists, but something is still missing.

“Built but not checked” is a healthy status. It tells me exactly what happened and what still needs to happen.

“Done” hid several different realities inside one confident word.

Start manually

I nearly paused the product to build a tool for running all of this. I am glad I did not.

I adopted the habits by hand. I wrote down what I saw. I separated building from checking. I asked for one next move. I gave helper agents smaller jobs. I changed the language in every final report.

Some of the loop is still manual. That is fine. The value comes from how I work, not from turning every habit into more software.

If I automate more, it will be because the manual version stayed useful long enough to earn it.

The first habit I recommend is the smallest one: stop saying “done.” Say Built, Checked, Merged, Finished, or Partial, then show how you know.

That small change exposed more problems in my agent work than any larger prompt or new tool.

What I keep

Let agents suggest and build. Keep a short written record. Check the real product. Give each helper one job. Never let the same agent do the work and decide that it is finished.

The reason is ordinary.

I do not want to wake up to a polished report that does not match the product.

Faster agents without stronger checks only gave me faster drift. Once the standard became honest, the speed finally became useful.