Declarative orchestration and deep observability for autonomous agent loops, built on the Claude Agent SDK.
You describe a loop — a stable goal, an iteration prompt, stop conditions, and safety caps — and looplens runs it inside a local daemon (loops survive your terminal), while capturing a full, queryable trace: per-iteration token/cost accounting, a live event stream, an immutable audit log, and automatic "is this loop stuck?" detection — all visible in a live React UI.
New here? Read GETTING-STARTED.md — a hands-on walkthrough: the simulator tour, loop-file syntax explained line by line, daemon lifecycle, and the phone setup (ntfy notifications + Tailscale remote control).
Writing your own loops? Start with docs/CREATE-A-LOOP.md — no coding required:
loop new "<what you want, in plain english>"drafts, validates, and registers a loop for you, or paste the included interview prompt into Claude Code to design one conversationally. The full authoring reference is LOOPS.md; worked examples live inloops/.
Claude Code is pair programming. looplens is delegation. A task is loop-shaped when (1) "done"
is verifiable by a command or a number, (2) progress comes in repeatable units, and (3) you
wouldn't enjoy babysitting it. Nightly dependency audits, post-refactor cleanup, migrations,
issue triage, doc upkeep — and building an MVP overnight from a plain-English spec
(loops/build-from-spec.loop.ts). For everything else, just
talk to Claude.
// loops/fix-tests.loop.ts
import { defineLoop } from "@looplens/core";
export default defineLoop({
name: "fix-failing-tests",
goal: "Make the entire test suite pass without weakening any test.",
prompt: (ctx) => ctx.iter === 1
? "Run the suite, find the first failing test, fix the root cause."
: `Last result: "${ctx.lastResultText}". Run the suite again and fix the next failure.`,
allowedTools: ["Bash", "Read", "Edit", "Grep", "Glob"],
permissionMode: "acceptEdits",
maxIterations: 8,
budgetUsd: 5, // hard spend ceiling — run is killed if crossed
progressSignal: (ctx) => (ctx.lastResultText.match(/(\d+)\s+failing/)?.[1] ?? "0"),
stop: [
{ kind: "predicate", name: "all-pass", fn: (ctx) => /all tests passing/i.test(ctx.lastResultText) },
],
});# Install (published package — https://www.npmjs.com/package/looplens)
npm install -g looplens
# Or work from this repo instead:
# npm install && npm run build:ui
# npm link -w @looplens/cli # installs the global `loop` command (one time)
# Run for real (uses your Claude Code login or ANTHROPIC_API_KEY). Auto-starts the daemon.
loop run loops/fix-tests.loop.ts
# Or prove the observability with the built-in simulator — no API spend, no auth
loop run loops/fix-tests.loop.ts --sim converging
loop run loops/fix-tests.loop.ts --sim thrashing # watch stuck-detection fire
loop dash # open the UI (http://127.0.0.1:4317)
loop list # run history
loop status # daemon + active runs
loop kill <runId> # abort an active run
loop daemon stop # stop the daemonWithout linking, use
npm run loop -- <args>from the repo root instead.
Raw agent harnesses give weak headless observability: a completion notification and whatever you write to a state file. looplens treats the SDK's structured message stream as a trace source and builds the missing layer on top: history, traces, live progress, cost tracking, stuck-detection.
Daemon-first. Loops execute inside loopd, a local daemon that owns the trace store and all
active runs — the CLI and UI are just clients. Close your terminal; the loop keeps going.
packages/
core/ engine abstraction, loop runner, trace store, health engine
server/ loopd: run supervisor + HTTP/WS API + hosts the built UI
ui/ React + Vite — run list, KPIs, health panel, live event stream
cli/ loop run|list|kill|status|dash|daemon — thin daemon client
defineLoop() declarative author config
│
Supervisor (in loopd) ─ LoopRunner ─ AgentEngine ┬─ SdkEngine (real Claude Agent SDK)
│ └─ SimEngine (deterministic scripted streams)
│ normalize() → one NormalizedEvent vocabulary
▼
TraceStore ─┬─ events.jsonl source of truth + audit (per run, replayable)
├─ SQLite index derived, queryable (rebuildable from JSONL)
└─ live tail WS pushes events-since-seq deltas to UI clients
Key design choices:
- The SDK event stream is the only trace source. Every assistant turn carries token
usage; the result message carries authoritativetotal_cost_usd. No transcript scraping. - Each iteration resumes the same SDK session (
resume), so agent context carries across iterations for free; the daemon process holds loop state between them. - JSONL is canonical; SQLite is a rebuildable index (
store.rebuildFromJsonl). - The engine is injectable.
SimEngineexercises the whole stack deterministically with zero API spend — that's what the test suite and demos run on. - Safety rails are first-class: hard total/per-iteration spend caps (SDK-native
maxBudgetUsd), tool allowlists, permission modes,loop kill, and an append-only audit trail. - Localhost-only, no auth — single-user by design; the API binds 127.0.0.1.
No harness gives you a "stuck" signal — it must be derived. Five detectors
(see packages/core/src/health.ts):
| Signal | Fires when |
|---|---|
stall |
no activity for N seconds mid-iteration (hung tool / wedged agent) |
thrash |
the same tool call and result repeats — circling with no change |
repeated_error |
the same tool error recurs — not learning from feedback |
plateau |
grader score flat for N iterations — converged short of the goal |
cost_no_progress |
spend climbs while the declared progress signal is unchanged |
The thrash detector keys on call + result together, so a healthy loop that legitimately re-runs
npm test each iteration (with a changing failure count) is not flagged — only a loop getting
the same result from the same action is.
- predicate — author code over accumulated context
- command — a shell command's exit code (tests, linters, build)
- grader — an LLM sub-agent scores the work 0–100 against a rubric; stop at a threshold
Start a loop on the desktop, walk away, get pinged on your phone. See SPEC.md for the
full design; $LOOPLENS_HOME/config.json:
- Notifications fire on transitions, not states: loop stuck (with the exact signals), budget 80% crossed, run finished/killed — one ping each, deduped in the daemon.
- Auth: loopback clients are never asked for a token; everything else needs
Authorization: Bearer <token>. The web UI prompts for it on 401 and remembers it. - Recommended transport: Tailscale — bind the tailnet address, open the UI from your phone, install it as a PWA (manifest included). No ports exposed to the internet.
- Killing a runaway loop from your phone is one tap.
npm test # core e2e suite, all on the simulator (5 tests)
npm run typecheck # all four packages
npm run dev -w @looplens/ui # UI with HMR (proxies /api + /ws to the daemon)
npm run build:ui # production UI → served by the daemonWorking: daemon, CLI, React UI, live streaming, health engine, budget kills, sim + real engines.
Next: worktree-per-loop isolation, scheduled re-invocation mode (cron-style discovery loops), native notifications on stuck/killed, packaged binaries, optional desktop (Tauri tray) shell over the same daemon API.
{ "remote": { "bind": "0.0.0.0", // default 127.0.0.1; use your Tailscale/LAN address "token": "long-random-string" // REQUIRED for any non-loopback bind (daemon refuses otherwise) }, "notifications": { "ntfy": { "topic": "my-secret-loops-topic" }, // install the ntfy app, subscribe, done "webhook": { "url": "https://example.com/hook" } // or integrate with anything } }