Human in the Loop, Human on the Loop... How About Loops That Learn? Adrian Hornsby's book 'Why We Still Suck at Resilience' inspires a framework for improving AI agents by applying organizational learning theories. The author argues that current agentic systems rely on single-loop learning, which adds rules after incidents but fails to handle novel situations, and proposes double-loop and deutero-learning to generalize and improve the learning process itself. Human in the Loop, Human on the Loop... How About Loops That Learn? Single-loop, double-loop and deutero-learning applied to agentic systems As you know, I’ve been reading Why We Still Suck at Resilience by Adrian Hornsby yes, I know, it’s like n-th post that starts like that, but what can I say - I’m a slow, thoughtful reader and it gives me ideas , and I read about “organisational learning” vs “learning organisation”. tldr; would be - if the companies pays attention to incidents, and after each one let’s say they add a control, or add a new verification step to their CI pipeline, then we can say this is “organisational learning” - an organisation learned something and changed their behaviour based on that. It is a good step, not every organisation has it. The problem with it though, is that they might get into accumulating documentation, accumulating validation steps and extra checks, runbooks of runbooks. They have a “how to do X” page about all kinds of issues they have encountered. But they haven’t built the knowledge and understanding of how to deal with “novel” incidents. Does that sound familiar? Let’s bring this to agentic world - you’ve created an agent, and every time it does something wrong you tell it to not do it the next time. You add that to your SOMETHING.md, you share it with your team, so they also benefit from that “rule”. You might even automate that part, like I did for my local agent, where I created a hook that is firing every time a single conversation is taking “too many turns” and is reflecting on the conversation itself, and on the things that I highlighted as “issues” and on how could they have been avoided in the first place, then let’s me pick and choose what to store in memory for the future. That works for a while, but every now and then your agent ignores the rules, deviates from them, or encounters a “slightly” different flavor of what you wrote, where you think like “well, you could have inferred that”. You are frustrated, you read about it, and you learn that you can’t fill the context forever with rules. You also hear that the agents are not that good at “novel” incidents and I call “incident” anything that didn’t go as originally planned . Well, of course they aren’t. Because they are, at best, applying “organisational learning”. Let’s read further, and see how can we go from “organisational learning” to a “learning organisation”. For that, a company would need to think more “meta”. They would need to ask themselves questions like “what patterns are we seeing across the incidents?”, “what does it tell us about the gap between how we thought the system works, and how it actually works?”, “how does this affect other teams work?”. Point being, a learning organisation uses incidents to generalize across a larger scope and learn on a larger scope. Next, Adrian brings in a classic categorization of learning types it goes back to Argyris and Schön, organizational-learning people from the 70s : “Single-loop” - add yet-another check based on the incident we observed “Double-loop” - “questions the assumptions and conditions within which single-loop learning operates” - why do we keep adding more checks? Maybe the config file is too complex? Maybe it’s not flexible enough?.. “Deuterolearning” - learning how to learn by the way I wrote a while back about learning to learn https://www.thoughtfultechnologist.com/p/learning-to-learn - I love finding out others have been thinking along the same lines - this one examines the learning process itself, and questions, are we learning effectively? is our learning process actually useful? how are we learning? how can we improve it? You know what I think, when I see loops these days. Loop Engineering Yaay ../s If you think about it, this same could be applied to an agentic system. Single-loop is my hook example from above - we notice an issue, we save in memory not to do it anymore. Double-loop learning would need to fire when we encounter the same issue one-too many times and would need to reflect, why do we keep seeing this incident, even though we had the single loop? Deuterolearning loop would fire even rarer and examine - is our process of learning efficient? or do we need to adjust that? So, creating a complete agentic system, that is capable of handling novel incidents, would require not just those loops where you ask LLM for a result until it passes the tests. No. It’ll require an outer loop, which will react on deviations from the plan and create a rule to combat those, it will require another loop that watches the rules created and checks if those can be generalized/optimized, and it will require a third loop that would question whether the previous loops are working efficiently and improve them as needed. First, I just wanted to post a note about this. Then it became an article, and then I heard the lyrics in my head again - More Than Words love that song - and decided to make a little demo. Now for the demo to work, we need an agentic system in the first place. I also wanted to update my consultancy website, which was last touched in 2018 running on hugo, nice little go-based website builder . So why don’t I combine these two? Let’s create an agentic system, that helps me do imaginary A-B testing of my new website. The new site is a rebuild from scratch: a one-page static site, plain HTML and CSS, no build step, no JavaScript, no hugo anymore sad emoji . And it comes in two variants, a and b. Each variant is its own directory of HTML and styles, and each deploys as its own CloudFront stack. Variant a is the control, variant b is where the experiments happen. All for the sake of some agentic development demo. The agentic development showcases how these websites can be modified through a taskboard: a small static board S3 + CloudFront , one Lambda behind a Function URL, one DynamoDB table. You type a task into the board: “In variant b, change the hero call-to-action button background to e7efe9”. A worker polls the board and runs the actual agentic pipeline. It’s built on the AWS Strands Agents SDK as a multi-agent Graph, Claude on Bedrock, five stages: triage → design → implement → compliance → deploy. Each stage reports back to the board, so the card walks through queued → picked up → evaluating → designing → implementing → compliance checks → deploying → deployed, and ends with the live CloudFront URL of the freshly deployed variant. The implement node has a guardrails-enforced write scope: it can touch files under the one variant the task targets variants/b for a variant-b task and nothing else, not even the other variant. The scope is a path validator that rejects everything outside it, escapes and symlinks included, so the prompt never has to say “please don’t touch other files”. Remember this part. Now that we have a pipeline that picks a task, applies it, verifies it and deploys it, we can start showcasing the learning loops. What the demo actually does, in plain words We need to do some tasks, so that some of them fail, so that we learn from it. The deliberately “bad” code is the CSS of variant b: the hero button’s contrast is broken right now, the colors live as scattered hex literals instead of named colors defined in one place so every color change tends to leave broken siblings behind , and dark mode and hover are unreadable in ways that no check in the pipeline even looks at. And we expect learning at three levels: fix the one broken instance, then rewrite the rule that keeps producing broken instances, then widen what the checks can even notice. Now let me unpack each of those. Where do the tasks come from? An imaginary designer, played by a script. They ask for the hero call-to-action button in variant b in one color, then change their mind, then change it again. Once or twice, that’s just work. But I needed them to keep going, task after task, because you cannot demonstrate learning without recurrence, and you cannot get recurrence from fifteen unique snowflake tasks. So: one button, many colors, plus the occasional user complaint. The well-behaved way to store colors is named values. A named color is declared once at :root , like --hero-bg: 13203b; , and every rule that needs that color says var --hero-bg instead of repeating the hex. Think of it as a constant in code: change the definition in one place, every rule follows. That’s the theory. What I planted in variant b in a disclosed seed commit is the real-world version of it, which is only partially that: :root { --hero-bg: 13203b; / named colors exist... / --hero-ink: f1f2f5; --hero-accent: a9bcd8; } .h1-sub { color: a9bcd8; } / ...but the rules don't use them... / .hero-method { color: a8bcd8; } / ...and this one is a digit off --hero-accent. Nobody noticed. / .btn { background: a9c8b6; / the button doesn't use them either / color: f2f1ec; / ~1.6:1 against that background. Unreadable. / } .btn:hover { background: e7efe9; / hover keeps the same light text: unreadable again / } @media prefers-color-scheme: dark { .hero { background: 101311; color: 3a3f3d; } / ~1.7:1, unreadable / .btn { background: 22312a; color: 31423a; } / same story / } Look at what’s broken here, because I broke it in three different KINDS of ways on purpose: An instance is broken. The button’s background/text pair fails contrast right now, in light mode, default state. A one-time fix exists. A frame is broken. The colors live as scattered literals instead of named values, so ANY color change tends to leave stale siblings behind the hover state, the dark-mode override, the paired text color . That a8bcd8 above is the same decay one step further: a named color’s value copy-pasted by hand somewhere in the past and drifted by a digit, which also means moving the colors back into named values is a judgment call ”is this SUPPOSED to differ from --hero-accent?” and not a find-and-replace a script could do. No single fix removes this; only a different way of making changes does. And to make it properly nasty, the agent’s rulebook explicitly forbids that different way ”do NOT introduce new abstractions or variables”, we’ll see it in a minute . The process is blind. Dark mode and hover are unreadable too, but nothing in the pipeline looks at them. These flaws produce zero failed checks. They can only enter the system as user complaints. Then I had to decide what counts as a fault. I already told you my definition: an incident is anything that didn’t go as originally planned. Here that means four things: an automated check failing, a change getting rejected in review, an implementation needing a second attempt that one rides along as a retry count on the incident that caused it , and a user complaining about something already deployed. Every one of those gets written down, and, important detail, written down WITH a class: a short name for what kind of problem it was, like stale-color-left-behind or dark-mode-contrast . The class is what makes recurrence countable. “The same thing keeps happening” is a feeling; “third incident of class X this week” is a trigger you can automate on. You’ll see in a moment that the outer loops fire on exactly that. And before building anything, I wrote down what learning SHOULD look like at each level, so that later I could judge whether it actually happened instead of squinting at the output until it resembles success: L1 learning is a fix. L2 learning is a changed rule. L3 learning is a changed way of noticing. Same demo, three different objects of learning. Keep this table in your head; the rest of the article is just building the machinery that produces each row, then verifying it actually did. Three directories, three owners Next question I had to answer: where do the rules, the checks, and the evidence live? The whole design is these three directories plus the rule about who may write into each: worker-strands/ frames/ the rules the pipeline operates UNDER policy.md how to make changes compliance.md what the reviewer flags tokens.md named-color conventions process/ how the system LEARNS probes.yaml what gets deterministically checked triggers.yaml when the outer loops wake up retro.md the retro template the double loop fills in journal/ the evidence incidents.jsonl one line per deviation metrics.jsonl one line per task frames/ is the agent’s rulebook. The design, implement, and compliance nodes don’t carry their rules in hardcoded prompts; they load these files fresh on every task. This is the seeded policy.md , in full: Implementation policy Loaded into the design and implement agents on every task. - Make the smallest, most local edit that satisfies the task. - Do NOT restructure stylesheets, rename selectors, or introduce new abstractions or variables; change concrete values in place. - Touch only what the task requires. - Git commit messages must start with " L1 ". Look familiar? These are exactly the rules from your SOMETHING.md at the beginning of this article. Sensible, defensive, written by a human who’s been burned before. The book would call them a “frame”: the assumptions and conditions the work happens inside. process/ is the configuration of the learning itself. It answers two questions: what do we check, and when do we step back? probes.yaml lists the deterministic checks that run on every implementation. Seeded, it contains exactly one probe: probes: - id: hero-cta-contrast file: styles.css relative to variants/