agent-loops

Agent Loops

GitHub License: CC BY-SA 4.0 Node.js Docker

Multi-agent orchestration system that automatically decomposes tasks via LLM, assigns them to specialized agents running in isolated Docker containers, and tracks progress on a SQLite Kanban board.

Looking for a way to write specs for this orchestrator? Check out specsmith-agent — an opencode agent that turns rough ideas into detailed task specifications.

How it works

                         ┌──────────────────────────────────────────┐
                         │           Orchestrator (Node.js)         │
                         │                                          │
  Task JSON ───────────► │  queue-watcher ──► board (SQLite)        │
  in queue/              │       │                                  │
                         │       ▼                                  │
                         │  dispatcher (every 30s)                  │
                         │    ├── reclaimStale (heartbeat timeout)  │
                         │    ├── auto-decompose (LLM → subtasks)   │
                         │    ├── promoteReady (deps met)           │
                         │    ├── claimNextReady → spawn container  │
                         │    ├── collectResults → commit & push    │
                         │    ├── render BOARD.md                   │
                         │    └── checkRootCompletion               │
                         │             │ dockerode                  │
                         │    ┌────────┼────────┐                   │
                         │    ▼        ▼        ▼                   │
                         │ ┌───────┐┌───────┐┌───────┐              │
                         │ │backend││ qa    ││devops │  ...         │
                         │ │iteratr││iteratr││iteratr│              │
                         │ └──┬────┘└──┬────┘└──┬────┘              │
                         │    └────────┴────────┘                   │
                         │         results + BOARD.md               │
                         └──────────────────────────────────────────┘

Task lifecycle

  1. Drop a JSON file in queue/ (or use the HTTP API)
  2. The queue-watcher picks it up and creates a task in SQLite (status: triage)
  3. The dispatcher decomposes the task into subtasks via LLM (LiteLLM)
  4. Each subtask goes through: todoreadyrunningdone
  5. A Docker container is spawned per task with the assigned agent
  6. The agent works in an isolated workspace
  7. On completion, results are committed and optionally pushed to a git remote
  8. The dispatcher consolidates results into the root task
  9. All progress is reflected in BOARD.md (auto-generated)

Available agents

Agent Role Description
backend Maker APIs, databases, server logic
frontend Maker UI components, state, styles
devops Maker Docker, CI/CD, infrastructure
sre Maker Monitoring, alerting, observability
qa Checker Tests, validation, verification
security Checker Audits, vulnerabilities, compliance

Makers run in parallel. Checkers wait for Makers to finish.

Stack

Component Technology
Orchestrator Node.js 22 (ESM)
Kanban board SQLite (better-sqlite3, WAL mode)
Containers Docker via dockerode
Agent runtime iteratr (Go) + opencode CLI
LLM Any provider via LiteLLM proxy
API Node.js http built-in

Requirements

Quick start

# Clone
git clone https://github.com/danifernandezs/agent-loops.git && cd agent-loops

# 1. Configure
cp .env.example .env
# Edit .env with your LLM endpoint, API key, and model names

# 2. Build the agent image (compiles iteratr from source + installs opencode)
docker build -f Dockerfile.agent -t iteratr-agent:latest .

# 3. Build and start the orchestrator
docker compose up -d --build

# 4. (Optional) Start the Telegram bot
docker compose --profile telegram up -d

# 5. Enqueue a task
cp queue/example-task.json queue/my-task.json
# NOTE: remove the example file from queue/ before deploying to production,
# otherwise it will be picked up as a real task on first boot.

# 6. Check status
docker exec agent-loops-orchestrator cat /opt/agent-loops/data/BOARD.md

Important: The agent image must be built before starting the orchestrator. The dispatcher spawns containers using the iteratr-agent:latest image. If it is not built, tasks will fail to start.

Configuration

All configuration goes in .env (see .env.example):

LLM

Variable Default Description
LLM_BASE_URL http://host.docker.internal:4000 LiteLLM endpoint
LLM_API_KEY API key for the proxy
LLM_PROVIDER openai Provider (openai, anthropic, etc.)

Models per role

Variable Default Description
MAKER_MODEL claude-sonnet-4-20250514 Model for maker agents
MAKER_MAX_TOKENS 32768 Max response tokens for makers
MAKER_TEMPERATURE 0.4 Temperature for makers
CHECKER_MODEL claude-3-5-haiku-latest Model for checker agents
CHECKER_MAX_TOKENS 32768 Max response tokens for checkers
CHECKER_TEMPERATURE 0.2 Temperature for checkers
DECOMPOSER_MODEL claude-3-5-haiku-latest Model for task decomposition
DECOMPOSER_MAX_TOKENS 32768 Max tokens for decomposition
DECOMPOSER_TEMPERATURE 0.3 Temperature for decomposition

Operation

Variable Default Description
MAX_PARALLEL_CONTAINERS 3 Simultaneous containers
DISPATCH_INTERVAL_MS 30000 Dispatcher tick interval
STALE_TIMEOUT_MINUTES 120 Heartbeat timeout (marks as stale)
CONTAINER_TIMEOUT_MS 36000000 Container timeout (10 hours)
SHUTDOWN_TIMEOUT_MS 300000 Graceful shutdown wait (5 minutes)
CHECKER_ROLES qa,security Roles that use the checker model

Task format

Auto-decompose (LLM splits into subtasks)

{
  "title": "Add user registration endpoint",
  "body": "Create POST /api/users/register with email, password, JWT token response",
  "repo_url": "https://github.com/org/api-service.git",
  "repo_branch": "main",
  "auto_decompose": true
}

Pre-decomposed (you define the subtasks)

{
  "title": "Add health check + deploy config",
  "repo_url": "https://github.com/org/api-service.git",
  "tasks": [
    {
      "title": "Add /health endpoint",
      "body": "GET /health returns { status: 'ok', timestamp }",
      "assignee": "backend",
      "priority": 0,
      "depends_on": []
    },
    {
      "title": "Write tests for health endpoint",
      "body": "Unit and integration tests",
      "assignee": "qa",
      "priority": 1,
      "depends_on": ["Add /health endpoint"]
    }
  ]
}

HTTP API

The orchestrator exposes an API on port 3000:

GET    /api/boards                     Boards with stats
GET    /api/tasks?status=ready         List tasks (filters: status, assignee, tenant)
GET    /api/tasks/:id                  Detail with parents, children, runs, comments
POST   /api/tasks                      Create task (JSON body)
POST   /api/tasks/:id/decompose        Decompose manually (must be in triage)
POST   /api/tasks/:id/unblock          Unblock a blocked task
POST   /api/tasks/:id/archive          Archive a task

Example: create task via API

curl -X POST http://localhost:3000/api/tasks \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <your-token>' \
  -d '{
    "title": "Fix login bug",
    "body": "Users getting 500 on POST /login when email is empty",
    "repo_url": "https://github.com/org/api-service.git",
    "assignee": "backend",
    "priority": 0
  }'

Project structure

agent-loops/
├── orchestrator/
│   ├── src/
│   │   ├── main.js              Entry point
│   │   ├── api.js               HTTP API (7 endpoints)
│   │   ├── dispatcher.js        Main loop (30s tick)
│   │   ├── decomposer.js        LLM-powered decomposition
│   │   ├── docker-runner.js     Dockerode: spawn, monitor, cleanup
│   │   ├── git-manager.js       Git init, commit, push for results
│   │   ├── board.js             SQLite kanban (schema, CRUD, events)
│   │   ├── spec-generator.js    Generates spec.md with worker context
│   │   ├── status-renderer.js   Generates BOARD.md (atomic write)
│   │   ├── llm.js               LLM config + generic callLLM
│   │   ├── notifier.js          Webhooks to callback_url
│   │   └── queue-watcher.js     fs.watch on queue/
│   └── test/                    Tests for all modules
├── agents/
│   ├── profiles/                6 agent profiles (.md)
│   ├── hooks/                   6 hook configs (.yml)
│   └── templates/               6 iteratr templates
├── connectors/
│   ├── Dockerfile               Telegram bot container
│   ├── package.json
│   ├── telegram-bot.js          Telegram bot interface
│   └── telegram.js              Telegram polling client
├── queue/
│   └── example-task.json        Example task (remove before deploy)
├── docs/
│   ├── SPEC.md                  System architecture spec
│   └── RESEARCH.md              Design decisions and sources
├── docker-compose.yml           Orchestrator + Telegram bot
├── Dockerfile.agent             iteratr (Go) + opencode on Alpine
├── Dockerfile.orchestrator      Node.js 22 + git on Alpine
├── .env.example                 Configuration template
└── LICENSE.txt                  CC BY-SA 4.0

Kanban board

Task states:

triage ──► todo ──► ready ──► running ──► done
                                  │
                                  ├──► blocked (needs intervention)
                                  └──► gave_up (max retries exceeded)

Container resources

Resource Limit
Memory 1.5 GB
CPU 1 core
Logs 10 MB × 3 rotated files
Timeout 10 hours

Notifications

If a task has a callback_url, the system sends a POST with:

{
  "event": "task_completed",
  "task_id": "t_abc123",
  "title": "Build API endpoint",
  "status": "done",
  "timestamp": "2025-01-15T14:32:00.000Z"
}

Events: task_completed, task_failed, task_blocked, root_completed

Development

cd orchestrator
npm install
npm test
node src/main.js        # Run without Docker (needs git + SQLite)

Documentation

License

This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.

Please read the LICENSE file for more details.