Back to the library
Agent loop, open
Variant: Open loop
Think and act with nothing wired back — the observe step is drawn dashed and never lights, and the payload dies at the gate on every pass. With no measurement there is nothing to size the next correction against, so the same step lands three times and the error marches straight through the goal and out the far side. The marker is hollow because nothing measured it.
controldiagramllmloop
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Agentic loop, open</title>
<style>
:root {
--ivory: #faf9f5;
--slate: #141413;
--clay: #d97757;
--oat: #e3dacc;
--gray-500: #87867f;
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--wire: cubic-bezier(0.65, 0, 0.35, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.loop {
width: 264px;
}
.loop svg {
display: block;
width: 100%;
height: auto;
}
.box {
fill: var(--ivory);
stroke: var(--oat);
stroke-width: 1.5;
}
.box-on {
fill: rgba(217, 119, 87, 0.1);
stroke: var(--clay);
stroke-width: 1.5;
opacity: 0;
}
.label {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.07em;
fill: var(--slate);
text-anchor: middle;
}
.wire {
fill: none;
stroke: var(--oat);
stroke-width: 1.5;
}
.head {
fill: var(--oat);
}
/* Everything downstream of act is drawn but dashed, and never lights.
The missing step has to be visible: an absence you can point at reads
as a design, a blank space reads as an unfinished drawing. */
.off {
stroke-dasharray: 3 3.5;
}
.head-off {
fill: none;
stroke: var(--oat);
stroke-width: 1.5;
}
.label-off {
fill: var(--gray-500);
}
.node {
animation: node-lift 3.6s infinite;
animation-delay: var(--d);
}
.node .box-on {
animation: node-on 3.6s infinite;
animation-delay: var(--d);
}
.n-think {
--d: 0s;
transform-origin: 40px 38px;
}
.n-act {
--d: 0.3s;
transform-origin: 132px 38px;
}
@keyframes node-on {
0% {
opacity: 0;
animation-timing-function: var(--ease-out);
}
2%,
3.5% {
opacity: 1;
animation-timing-function: var(--ease-out);
}
5.5% {
opacity: 0;
}
25% {
opacity: 0;
animation-timing-function: var(--ease-out);
}
27%,
28.5% {
opacity: 1;
animation-timing-function: var(--ease-out);
}
30.5% {
opacity: 0;
}
50% {
opacity: 0;
animation-timing-function: var(--ease-out);
}
52%,
53.5% {
opacity: 1;
animation-timing-function: var(--ease-out);
}
55.5%,
100% {
opacity: 0;
}
}
@keyframes node-lift {
0% {
transform: scale(1);
animation-timing-function: var(--ease-out);
}
2%,
3.5% {
transform: scale(1.03);
animation-timing-function: var(--ease-out);
}
5.5% {
transform: scale(1);
}
25% {
transform: scale(1);
animation-timing-function: var(--ease-out);
}
27%,
28.5% {
transform: scale(1.03);
animation-timing-function: var(--ease-out);
}
30.5% {
transform: scale(1);
}
50% {
transform: scale(1);
animation-timing-function: var(--ease-out);
}
52%,
53.5% {
transform: scale(1.03);
animation-timing-function: var(--ease-out);
}
55.5%,
100% {
transform: scale(1);
}
}
.token {
fill: var(--clay);
animation: token 3.6s linear infinite;
}
@keyframes token {
0% {
transform: translate(0px, 0px);
opacity: 0;
}
1.5% {
opacity: 1;
}
3.33% {
transform: translate(0px, 0px);
animation-timing-function: var(--wire);
}
8.33%,
12.22% {
transform: translate(92px, 0px);
animation-timing-function: var(--wire);
}
17.22% {
transform: translate(142px, 0px);
opacity: 1;
animation-timing-function: var(--ease-out);
}
20.8% {
transform: translate(142px, 0px);
opacity: 0;
}
25% {
transform: translate(0px, 0px);
opacity: 0;
}
26.5% {
opacity: 1;
}
28.33% {
transform: translate(0px, 0px);
animation-timing-function: var(--wire);
}
33.33%,
37.22% {
transform: translate(92px, 0px);
animation-timing-function: var(--wire);
}
42.22% {
transform: translate(142px, 0px);
opacity: 1;
animation-timing-function: var(--ease-out);
}
45.8% {
transform: translate(142px, 0px);
opacity: 0;
}
50% {
transform: translate(0px, 0px);
opacity: 0;
}
51.5% {
opacity: 1;
}
53.33% {
transform: translate(0px, 0px);
animation-timing-function: var(--wire);
}
58.33%,
62.22% {
transform: translate(92px, 0px);
animation-timing-function: var(--wire);
}
67.22% {
transform: translate(142px, 0px);
opacity: 1;
animation-timing-function: var(--ease-out);
}
70.8%,
100% {
transform: translate(142px, 0px);
opacity: 0;
}
}
.track {
fill: var(--oat);
}
.err {
fill: var(--clay);
transform-origin: 132px 114px;
animation: err-march 3.6s infinite;
}
/* Hollow, because nothing measured it. The bar is the truth; the agent
is not holding this number. */
.state {
fill: var(--ivory);
stroke: var(--clay);
stroke-width: 2;
animation: state-march 3.6s infinite;
}
/* Three identical corrections — with no observation there is nothing to
size the next one against, so the step never changes and the third one
walks straight through the goal and out the far side. */
@keyframes err-march {
0% {
transform: scaleX(-0.85);
opacity: 0;
animation-timing-function: var(--ease-out);
}
1.5%,
8.33% {
transform: scaleX(-0.85);
opacity: 1;
animation-timing-function: var(--ease-out);
}
13.9%,
33.33% {
transform: scaleX(-0.45);
animation-timing-function: var(--ease-out);
}
38.9%,
58.33% {
transform: scaleX(-0.05);
animation-timing-function: var(--ease-out);
}
63.9%,
94% {
transform: scaleX(0.35);
opacity: 1;
animation-timing-function: var(--ease-out);
}
98%,
100% {
transform: scaleX(0.35);
opacity: 0;
}
}
@keyframes state-march {
0% {
transform: translateX(-78px);
opacity: 0;
animation-timing-function: var(--ease-out);
}
1.5%,
8.33% {
transform: translateX(-78px);
opacity: 1;
animation-timing-function: var(--ease-out);
}
13.9%,
33.33% {
transform: translateX(-41px);
animation-timing-function: var(--ease-out);
}
38.9%,
58.33% {
transform: translateX(-5px);
animation-timing-function: var(--ease-out);
}
63.9%,
94% {
transform: translateX(32px);
opacity: 1;
animation-timing-function: var(--ease-out);
}
98%,
100% {
transform: translateX(32px);
opacity: 0;
}
}
.goal {
stroke: var(--slate);
stroke-width: 2;
}
.caption {
font-size: 9px;
letter-spacing: 0.1em;
fill: var(--gray-500);
text-anchor: middle;
}
@media (prefers-reduced-motion: reduce) {
.node,
.node .box-on,
.token,
.err,
.state {
animation: none;
}
/* Terminal state, and it is the whole argument: the marker has walked
past the goal and the error bar has changed sign. Both must survive
— the bar without the marker is movement without meaning. The dashed
OBSERVE box is static markup, so the missing step is still drawn. */
.token {
opacity: 0;
}
.err {
transform: scaleX(0.35);
}
.state {
opacity: 1;
transform: translateX(32px);
}
}
</style>
</head>
<body>
<div
class="loop"
role="img"
aria-label="An open loop: think and act fire three times with the observe step missing, so the same correction is applied every pass and the error walks straight past the goal"
>
<svg viewBox="0 0 264 144">
<line class="wire" x1="76" y1="48" x2="90" y2="48" />
<polygon class="head" points="90,44.5 96,48 90,51.5" />
<line class="wire off" x1="168" y1="48" x2="182" y2="48" />
<polygon class="head-off" points="182,44.5 188,48 182,51.5" />
<path class="wire off" d="M224 58 V74 Q224 82 216 82 H48 Q40 82 40 74 V65" />
<polygon class="head-off" points="36.5,65 40,59 43.5,65" />
<g class="node n-think">
<rect class="box" x="6" y="18" width="68" height="40" rx="8" />
<rect class="box-on" x="6" y="18" width="68" height="40" rx="8" />
<text class="label" x="40" y="35">THINK</text>
</g>
<g class="node n-act">
<rect class="box" x="98" y="18" width="68" height="40" rx="8" />
<rect class="box-on" x="98" y="18" width="68" height="40" rx="8" />
<text class="label" x="132" y="35">ACT</text>
</g>
<g>
<rect class="box off" x="190" y="18" width="68" height="40" rx="8" />
<text class="label label-off" x="224" y="35">OBSERVE</text>
</g>
<circle class="token" cx="40" cy="48" r="4.5" />
<rect class="track" x="40" y="112" width="184" height="4" rx="2" />
<rect class="err" x="132" y="111" width="92" height="6" rx="3" />
<circle class="state" cx="132" cy="114" r="5" />
<line class="goal" x1="132" y1="104" x2="132" y2="124" />
<text class="caption" x="132" y="138">GOAL</text>
</svg>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.