Skip to content

Repository files navigation

loops

A node can be a loop. A loop can be a node.

npm status: alpha TypeScript node >=20 license: MIT

loops orchestrates AI agents through real engineering work: pick up an issue, research it, write a plan a person can veto, build until the tests really pass, rework until the reviewers are satisfied, open the PR — then pick up the next issue. You write that discipline once, as code — a DAG of steps with loops nested inside — and it runs unattended, for an afternoon or a whole backlog. Any model can sit in any seat: a cheap one researches, a strong one builds, a different vendor reviews the work.

You can't make an LLM deterministic, and there is rarely one right answer in software. But the process of building it can be exact: which steps run and in what order, what each must pass, who reviews the work, when a person must step in. loops makes the process code — deterministic, repeatable, enforced — and leaves the creativity to the agent inside each step.

The process is what keeps a long run honest. Every step ends at a gate: tests that must pass, judges on other models, juries that must agree, people who approve the risky parts. A failed step gets the evidence and tries again with a clean context; a run can pause, crash, or hit a rate limit and pick up where it left off. Nothing moves forward on the model's say-so.

Agent memory is git itself. Each step starts fresh and reads the decisions so far from the commit log; when work lands, the reasoning is written back into the commit. No vector store, no database, no extra infrastructure — nothing to sync or go stale.

Execution is cheap. Convergence is expensive.Convergence Count (February 2026), the essay loops grew from.

npm i @loops-adk/core   # Node >= 20

A day's work, as one file

Loops and graphs are not mutually exclusive. Real work isn't a straight line — some steps run once, some iterate until a bar is met — so a node in the graph can be a whole loop, and a loop's body can be a whole graph, at any depth. That combination is the simplest way to model how people actually work, and it's older than it looks: the structured program theorem (Böhm & Jacopini, 1966) showed that any computable control flow can be built from sequence, selection, and iteration alone. The graph carries the first two. The loop carries the third.

import {
  loop, dag, agentJob, agentCheck, commandSucceeds, predicate,
  humanGate, reviewPanel, pullRequestJob,
} from '@loops-adk/core';

// Build until lint and tests pass, then face the reviewers.
// A rejection re-runs the loop with their findings in the prompt.
const build = loop({
  name: 'build',
  max: 10,
  body: agentJob({
    prompt: 'Implement the next increment from PLAN.md. Small steps, with tests.',
    ground: true,          // read the plan, notes, and commit log first
    consumeFeedback: true, // review findings land here
  }),
  until: [commandSucceeds('npm', ['run', 'lint']), commandSucceeds('npm', ['test'])],
  review: reviewPanel({
    label: 'review',
    reviewers: [
      { name: 'adversarial', review: agentCheck({ question: 'What breaks?', engine: 'codex' }) }, // a different vendor
      { name: 'security', review: agentCheck({ question: 'Is it safe?', model: 'opus' }) },
      { name: 'completeness', review: agentCheck({ question: 'Is every acceptance criterion met?', model: 'sonnet' }) },
    ],
  }),
  commit: true,
});

// One issue, worked end to end.
const issue = dag({
  name: 'issue',
  nodes: {
    pickup:   agentJob({ label: 'pickup', prompt: 'Pick the next ready issue; branch; write ISSUE.md with acceptance criteria and a complexity call.' }),
    research: { needs: ['pickup'], job: agentJob({ label: 'research', prompt: 'Explore the code this touches; write NOTES.md.', ground: true }) },
    plan:     { needs: ['research'], job: agentJob({ label: 'plan', prompt: 'Write PLAN.md: increments, tests, risks.', ground: true }) },

    approval: {
      needs: ['plan'],
      when: highComplexity, // simple plans skip straight through
      job: humanGate({ name: 'plan-approval', prompt: 'Read PLAN.md, then approve.' }),
    },

    build:  { needs: ['approval'], job: build },
    docs:   { needs: ['build'], job: agentJob({ label: 'docs', prompt: 'Update docs and changelog.', ground: true }) },
    ship:   { needs: ['docs'], job: pullRequestJob({ base: 'main' }) },
    notify: { needs: ['ship'], optional: true, job: agentJob({ label: 'notify', prompt: 'Post a one-line PR summary to the team channel.' }) },
  },
});

// Keep going until the backlog is empty.
export default loop({ name: 'engineer', body: issue, until: predicate(backlogEmpty, 'backlog is empty'), max: 20 });

A high-complexity plan stops the run until a person reads it and resumes with --ack plan-approval. Posting to Slack is the agent's own job through its tools — the pipeline just decides when. Full file, including the highComplexity and backlogEmpty conditions: examples/engineer.loop.ts.

The unit

Every step above is built from the same unit: do the work, check it, repeat until the check passes.

import { loop, agentJob, commandSucceeds, agentCheck } from '@loops-adk/core';

export default loop({
  name: 'build-feature',
  max: 20,
  body: agentJob({
    prompt: (c) => `Iteration ${c.iteration}: make concrete progress on TASK.md.`,
    ground: true, // read the commit log and notes before working
  }),
  until: [
    commandSucceeds('npm', ['test']),                                    // the truth
    agentCheck({ question: 'Does it match TASK.md?', threshold: 0.85 }), // the intent
  ],
  commit: { subject: 'feat: TASK.md' }, // one commit when it's done, reasoning included
});

Three rules hold everywhere:

  • "Done" is checked, not claimed. The tests must pass, and a separate judge reviews the result. The model never grades its own work.
  • Every attempt starts fresh. Progress lives in files and commits, not a growing chat history.
  • Git is the memory. Decisions are written into commit messages and read back on the next attempt.

Iterate, depend, judge

Strip the library to its verbs and three remain. In the theorem, selection and iteration both run on predicates the machine evaluates for free. In agent work, the predicate — is it done, is it safe, is it what was asked — is itself the contested question, so loops replaces the predicate with a gate. Judgment isn't a fourth structure; it's the predicate made honest. That triple is how the library models a working engineer:

  • Iterate — drafts, retries, rework. A loop with a bar to clear.
  • Depend — research before the plan, approval before the build. An edge in the graph.
  • Judge — tests, juries, a person's veto. The gate that decides pass or go again.

The theorem also draws the line between loops and graph frameworks that allow cycles. A back-edge over shared state is a flowchart jump — goto, the thing structured programming retired. Here the graph is always acyclic, and iteration lives in one named construct with its own gate, caps, and stall detection. Their cycles are goto; loop() is while. That is not a claim about power — back-edges compute everything while does, just as goto did. The win was always reasoning, not capability (Dijkstra's argument in Go To Statement Considered Harmful, 1968), and it's the same win here: because the graph never jumps, its shape is knowable before a token is spent — loops validate and assertGraph exist because of that. Turing complete where it must be, decidable where it can be (docs/theory.md).

One behaviour is deliberately still missing: preemption. A person also drops the plan mid-flight when the world changes. A loop whose body picks its next job at runtime (the Tend pattern in docs/concepts.md) approximates it; a graph that can be rewritten mid-run is on the roadmap.

Building blocks

Check more than one thing. An array means all of them must pass:

until: [
  commandSucceeds('npm', ['test']),                 // the truth
  agentCheck({ question: 'Ready to ship?', threshold: 0.85 }), // the intent
]

Ask a jury, not a judge.

quorum(2, // any 2 of 3 must agree
  agentCheck({ question: 'Is it correct?', model: 'opus' }),
  agentCheck({ question: 'Would this pass code review?', model: 'sonnet' }),
  agentCheck({ question: 'What breaks?', engine: 'codex' }),
)

Show the failure to the next attempt. A failed check hands over its evidence — test output, a judge's findings — so the next attempt fixes instead of rediscovering:

prompt: (c) => c.lastGate?.met === false
  ? `The gate failed:\n${c.lastGate.output}\n\nFix exactly that.`
  : 'Implement the feature in TASK.md.',

Pause for a person.

humanGate({ name: 'prod-approval', prompt: 'Review the staging deploy, then approve.' })
// the run stops (exit 75) until: loops run … --resume … --ack prod-approval

Send work back. A reviewer in a dag can return work to an earlier step; that step and everything after it run again:

revisionRequest({ target: 'implement', findings })

Work in parallel without collisions. Each writer gets its own git branch, merged back on pass:

dag({ name: 'team', isolation: 'worktree', nodes: { server, web, integrate } })
tournament({ name: 'best-of-3', n: 3, candidate, judge }) // try 3 approaches, keep the winner

Remember between attempts.

agentJob({ prompt, ground: true })  // read the notes and commit log before working
commit: { subject: 'feat: search' } // on success, write the why into the commit body

Ground steps in the company knowledge base, alongside the git log. Two memories feed each step: a small agent curates the exact business context from the knowledge base, and git already carries the implementation history — the decisions, the constraints, the why:

// A cheap agent pulls just the relevant business context…
context: agentJob({
  label: 'context',
  prompt: 'Query the company knowledge base (`qmd query …`) for everything relevant to ISSUE.md. Write a short brief to BRIEF.md.',
  model: 'haiku',
}),

// …and later steps ground on both: the curated brief and the commit history.
implement: {
  needs: ['context'],
  job: agentJob({
    prompt: 'Implement the next increment from PLAN.md.',
    ground: {
      sources: ['BRIEF.md', 'ISSUE.md'],                              // business context, declared
      curate: { engine: 'anthropic-api', model: 'claude-haiku-4-5' }, // one cheap turn keeps only what helps
    },
  }),
},

Stop runaway loops.

loop({ max: 50, noProgress: 3 })       // cap attempts; stop after 3 attempts that change nothing
await run(job, { budget: 2_000_000 })  // cap tokens for the whole run

Test the running thing. An Environment brings up a local stack or a preview deploy, and its variables reach the checks:

await run(job, { environment: dockerStack }) // gates now see BASE_URL of a live stack

Engines

Any model can fill any role — a worker on one model, its reviewer on another.

name backend notes
claude-cli claude subprocess uses host Claude auth, no key
agent-sdk @anthropic-ai/claude-agent-sdk fresh query() per call
anthropic-api @anthropic-ai/sdk cheapest for judges; needs a key
codex codex exec subprocess a different vendor behind the same interface
mock scripted, offline for tests and examples

Bring your own by implementing one method:

const myEngine: Engine = {
  name: 'my-provider',
  async run(req, onEvent, signal) {
    return { text, usage: { inputTokens, outputTokens }, model: req.model ?? 'x' };
  },
};

fallbackEngine(['claude-cli', 'codex']) moves to the next engine when one dies. loops preflight checks every engine with one tiny turn before the run spends anything.

CLI

loops run feature.loop.ts               # run it (live TUI; --no-tui / --json)
loops validate feature.loop.ts          # load + print the shape; no model calls
loops describe feature.loop.ts --json   # machine-readable shape

Watch long runs from another terminal — no daemon, no socket, just files:

loops run build.loop.ts --supervise
loops list                            # every supervised run, with state
loops status <runId>                  # where it is and what's blocking it
loops tail <runId>                    # stream events live
loops records <runId> --kind revision # the decision stream, filtered

loops helm is a conversational front end: plain English in, validated commands out (docs/helm.md).

loops run <file> imports and executes that file's module, like node <file>. Only run definition files you trust.

Scope

loops is the flow, nothing more. Scheduling belongs in cron or CI; durable replay belongs in Temporal, LangGraph, or Mastra — embed a loops job inside them (comparison). Acting in the outside world — Slack, GitHub, calendars — is the agent's job through its own tools; the loop decides when.

Examples

Every example in examples/ is a runnable definition file:

Docs

Roadmap

  • Preemption — the fourth verb. Pause a running graph, rewrite it on new information, resume. Until then, the Tend pattern (a loop that picks its next job at runtime) is the approximation.
  • Out-of-process control: pause, abort, and kick back a running loop from outside — the substrate preemption needs
  • convergence count (turns from intent to outcome) and cost per accepted change as first-class reported metrics
  • Calibration helpers for agent judges
  • More engine adapters (OpenAI, local models)

Develop

git clone https://github.com/jonny981/loops.git && cd loops && npm install
npm test               # offline, deterministic
npm run typecheck
node bin/loops.mjs --help

No build step from a checkout — the CLI runs the TypeScript source via tsx. Status: alpha; CHANGELOG.md records what each version added or broke.

Contributions welcome. Open an issue to discuss anything substantial first.

License

MIT

About

A small, nestable agent-loop job primitive: loop(body→until→review) composed with dag, pluggable engines, agent-judge gates, budget + run records. TypeScript.

Resources

Stars

13 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages