All runnable examples collected in one place. Each is self-contained.
For detailed information on Distributed Global Research Team implementation - see examples/global-research/README.md
Important: Run only one example at a time on the same Redis instance. Both examples share the same Redis pub/sub channels (
kaiban-state-events,kaiban-events-completed,kaiban-hitl-decisions). Running blog-team and global-research simultaneously will cause cross-contaminated state on the board and potentially misrouted HITL decisions. Usedocker compose downbetween switching examples.
For the default Redis/BullMQ path, you can now start or stop any example with one generic wrapper:
./scripts/run-example.sh start examples/blog-team
./scripts/run-example.sh stop examples/blog-teamThis wrapper uses only <example>/docker-compose.yml and <example>/orchestrator.ts.
Three KaibanJS agents collaborate to research, write, and fact-check a blog post, with a human editorial decision at the end.
Ava (researcher) ──> Kai (writer) ──> Morgan (editor) ──> Human (HITL)
Node #1 Node #2 Node #3
| Agent | Name | Role | Queue |
|---|---|---|---|
| Researcher | Ava | Finds verifiable facts with sources | kaiban-agents-researcher |
| Writer | Kai | Drafts Markdown blog post (500–800 words) | kaiban-agents-writer |
| Editor | Morgan | Structured review + PUBLISH/REVISE/REJECT | kaiban-agents-editor |
## EDITORIAL REVIEW
**Topic:** AI Agents in 2025
**Accuracy Score:** 8.5/10
### Factual Assessment
The post is mostly accurate with one unsupported claim.
### Issues Found
- "All agents require GPT-4" is unverified — Severity: HIGH
### Required Changes
- Replace with "LLM-agnostic" language
### Recommendation: REVISE
### Rationale
One HIGH-severity error must be corrected before publication.
Terminal prompt (always available when orchestrator is running):
╔══════════════════════════════════════════════════════════╗
║ 📝 EDITORIAL REVIEW BY MORGAN ║
║ Accuracy: 8.5/10 | Recommendation: REVISE ║
╚══════════════════════════════════════════════════════════╝
Options:
[1] PUBLISH — Accept and publish
[2] REVISE — Send back to Kai with editor notes
[3] REJECT — Discard this post
[4] VIEW — View full draft before deciding
React board (if running — see Example 7): a cyan Human-in-the-Loop Review Required banner appears with Approve / Revise / Reject buttons. Clicking sends the decision directly to the orchestrator via Socket.io → Redis.
Both inputs are active simultaneously — whichever responds first wins. The other is silently ignored.
Outcomes on board:
PUBLISH→✅ WORKFLOW COMPLETEgreen banner; all tasks DONE; agents IDLEREVISE→ revision task appears as DOING; human confirms revised draftREJECT→⏹ WORKFLOW ENDEDgrey banner; editorial task BLOCKED
| Variable | Default | Description |
|---|---|---|
GATEWAY_URL |
http://localhost:3000 |
Edge Gateway URL |
REDIS_URL |
redis://localhost:6379 |
Redis for completion events |
TOPIC |
Latest developments in AI agents |
Blog topic |
MESSAGING_DRIVER |
bullmq |
bullmq or kafka — must match worker containers |
KAFKA_BROKERS |
localhost:9092 |
Kafka broker(s) when using Kafka |
RESEARCH_WAIT_MS |
120000 |
Max wait for Ava (ms) |
WRITE_WAIT_MS |
240000 |
Max wait for Kai and revisions (ms) |
EDIT_WAIT_MS |
300000 |
Max wait for Morgan (ms) |
Prepare:
# Stop any conflicting stacks and clean networks
docker compose -f examples/blog-team/docker-compose.yml down --remove-orphans 2>/dev/null
docker network prune --force 2>/dev/nullStart services:
docker compose \
-f examples/blog-team/docker-compose.yml \
--env-file .env \
up --buildServices: redis · gateway (port 3000) · researcher · writer · editor
Open the board — in a separate terminal or browser tab (choose one or both):
Option A — React board app (interactive HITL, modern UI):
# Separate terminal, from kaiban-distributed root:
cd board && npm install && npm run dev
# Open: http://localhost:5173Option B — Static HTML viewer (zero setup):
Open: examples/blog-team/viewer/board.html in your browser
→ Auto-connects to http://localhost:3000
Both can be open simultaneously alongside the terminal monitor. All views are synchronized from the backend stream at all times — each gets a full snapshot on connect and every delta in real-time.
Run the orchestrator — choose one approach:
Option A — local (requires Node.js + project deps):
GATEWAY_URL=http://localhost:3000 \
REDIS_URL=redis://localhost:6379 \
TOPIC="AI Agents in 2025" \
npx ts-node examples/blog-team/orchestrator.tsOption B — fully containerised (recommended for clean environments):
docker compose -f examples/blog-team/docker-compose.yml run --rm \
-e TOPIC="AI Agents in 2025" orchestratorOr use the wrapper script with --docker:
./scripts/blog-team.sh start --dockerMonitor (optional third terminal):
COMPOSE_FILE=examples/blog-team/docker-compose.yml ./scripts/monitor.shWhat you see on the board:
RUNNING— topic appears in header; research task immediately inTODOcolumn- Agents cycle IDLE → EXECUTING → IDLE; tasks move
TODO→DOING→DONE - Each task appears in
TODOthe moment it is queued, before the agent picks it up - Step 3: editorial task →
AWAITING_VALIDATION— cyan Human-in-the-Loop banner with Approve / Revise / Reject buttons - After decision (board or terminal):
✅ WORKFLOW COMPLETEor⏹ WORKFLOW ENDED
Prepare:
docker compose -f examples/blog-team/docker-compose.kafka.yml down --remove-orphans 2>/dev/null
docker network prune --force 2>/dev/nullStart services:
docker compose \
-f examples/blog-team/docker-compose.kafka.yml \
--env-file .env \
up --buildServices: redis · zookeeper · kafka · gateway · researcher · writer · editor
Kafka takes 30–60 seconds for Zookeeper election + topic leader assignment. Wait until
curl http://localhost:3000/healthreturns{"status":"ok"}.
Verify consumer groups joined (all 4 should appear):
docker exec blog-team-kafka-1 kafka-consumer-groups \
--bootstrap-server localhost:9092 --list
# kaiban-group
# kaiban-group-researcher
# kaiban-group-writer
# kaiban-group-editorRun the orchestrator with Kafka — choose one approach:
Option A — local:
GATEWAY_URL=http://localhost:3000 \
REDIS_URL=redis://localhost:6379 \
MESSAGING_DRIVER=kafka \
KAFKA_BROKERS=localhost:9092 \
TOPIC="AI Agents in 2025" \
npx ts-node examples/blog-team/orchestrator.tsOption B — fully containerised:
docker compose -f examples/blog-team/docker-compose.kafka.yml run --rm \
-e TOPIC="AI Agents in 2025" orchestratorOr use the wrapper script:
./scripts/blog-team.sh start --kafka --dockerVerify a task was consumed (after first step):
docker exec blog-team-kafka-1 kafka-consumer-groups \
--bootstrap-server localhost:9092 \
--describe --group kaiban-group-researcher
# CURRENT-OFFSET=1 LOG-END-OFFSET=1 LAG=0 ← task consumed
docker exec blog-team-kafka-1 kafka-run-class kafka.tools.GetOffsetShell \
--broker-list localhost:9092 --topic kaiban-events-completed --time -1
# kaiban-events-completed:0:1 ← 1 completion publishedRequires Redis running at localhost:6379:
# Terminal 1 — Ava (researcher)
REDIS_URL=redis://localhost:6379 \
OPENROUTER_API_KEY=sk-or-v1-... \
LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free \
npx ts-node examples/blog-team/researcher-node.ts
# Terminal 2 — Kai (writer)
REDIS_URL=redis://localhost:6379 \
OPENROUTER_API_KEY=sk-or-v1-... \
LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free \
npx ts-node examples/blog-team/writer-node.ts
# Terminal 3 — Morgan (editor)
REDIS_URL=redis://localhost:6379 \
OPENROUTER_API_KEY=sk-or-v1-... \
LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free \
npx ts-node examples/blog-team/editor-node.ts
# Terminal 4 — Gateway
REDIS_URL=redis://localhost:6379 \
AGENT_IDS=gateway PORT=3000 \
node dist/src/main/index.js
# Terminal 5 — Orchestrator
GATEWAY_URL=http://localhost:3000 \
REDIS_URL=redis://localhost:6379 \
TOPIC="AI Agents in 2025" \
npx ts-node examples/blog-team/orchestrator.tsSame as 1C but add MESSAGING_DRIVER=kafka KAFKA_BROKERS=localhost:9092 to every terminal. Start Kafka first:
docker compose up -d redis zookeeper kafka# Example for researcher:
REDIS_URL=redis://localhost:6379 \
MESSAGING_DRIVER=kafka KAFKA_BROKERS=localhost:9092 \
OPENROUTER_API_KEY=sk-or-v1-... \
npx ts-node examples/blog-team/researcher-node.tsA complete set of raw Kubernetes manifests is located in examples/blog-team/infra/kubernetes/.
Deploy:
# Apply ConfigMap & Secrets (edit secrets in file first)
kubectl apply -f examples/blog-team/infra/kubernetes/configmap.yaml
# Apply remaining components
kubectl apply -f examples/blog-team/infra/kubernetes/redis.yaml
kubectl apply -f examples/blog-team/infra/kubernetes/gateway.yaml
kubectl apply -f examples/blog-team/infra/kubernetes/agents.yamlAccess the board: If running on Docker Desktop / Minikube:
kubectl port-forward svc/kaiban-gateway 3000:3000Then open examples/blog-team/viewer/board.html, or run the React board:
cd board && npm install && npm run dev
# Open: http://localhost:5173Helm provides a parameterized alternative located in examples/blog-team/infra/helm/.
Deploy:
helm install blog-team examples/blog-team/infra/helm \
--set secrets.OPENAI_API_KEY="sk-..." \
--set secrets.OPENROUTER_API_KEY="sk-or-v1-..."Scale agents on the fly:
helm upgrade blog-team examples/blog-team/infra/helm \
--set agents.writer.replicas=3Deploying kaiban-distributed directly using Railway is fully supported using the provided configuration template at examples/blog-team/infra/railway/railway.toml. Since Railway utilizes a "one service per repository" by default, deploy the Actor Model incrementally:
- Create a Railway Project and spin up a Redis plugin instance.
- Link the
kaiban-distributedrepository. - Add 4 separate services all pointing to the same repository.
- Under the Deploy -> Custom Start Command settings, override each service:
- Gateway:
node dist/src/main/index.js - Researcher:
node dist/examples/blog-team/researcher-node.js - Writer:
node dist/examples/blog-team/writer-node.js - Editor:
node dist/examples/blog-team/editor-node.js
- Gateway:
- Map Variables using
${{Redis.REDIS_URL}}and supply yourOPENAI_API_KEY. - Expose the domain of the Gateway service and direct the viewer to it.
# BullMQ stack
docker compose -f examples/blog-team/docker-compose.yml --env-file .env down
# Kafka stack
docker compose -f examples/blog-team/docker-compose.kafka.yml --env-file .env down
# Clean stale networks (when switching between stacks)
docker network prune --forceScale any worker horizontally — BullMQ auto-distributes tasks across all instances:
# 5 writer nodes competing for jobs from kaiban-agents-writer
docker compose \
-f examples/blog-team/docker-compose.yml \
--env-file .env \
up --build --scale writer=5All 5 instances subscribe to kaiban-agents-writer. Each job is processed exactly once (BullMQ competing consumers).
docker build -t kaiban-distributed:latest .
docker run -p 3000:3000 \
-e REDIS_URL=redis://host.docker.internal:6379 \
-e AGENT_IDS=researcher,writer,editor \
-e OPENROUTER_API_KEY=sk-or-v1-... \
-e LLM_MODEL=meta-llama/llama-3.1-8b-instruct:free \
kaiban-distributed:latestSubmit tasks (A2A v0.3 message/send; target agent in metadata.agentId):
curl -X POST http://localhost:3000/a2a/rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{"message":{"kind":"message","role":"user","messageId":"m1","parts":[{"kind":"text","text":"Research AI agent trends 2025"}],"metadata":{"agentId":"researcher","expectedOutput":"A 200-word summary"}}}}'# Unit tests (no Docker, all mocked)
npm test
# → 108 unit-test files / 1155 tests, 100% coverage of src (board tests run separately via `cd board && npm test`)
# BullMQ E2E (Docker auto-starts Redis)
npm run test:e2e
# → 69 tests: task execution, fault tolerance, state sync, A2A protocol, completion routing
# Kafka E2E (requires Kafka — starts automatically)
npm run test:e2e:kafka
# → 3 tests: publish-subscribe round-trip, unsubscribe
# Playwright visual baselines of the board + example viewers (needs the board dev server + gateway)
cd board && npm run test:visual # add :update to regenerate baselines
# All quality gates at once
npm run lint && npm run typecheck && npm run test:coverageRunaway-spend guard. Both example orchestrators enforce a workflow-level budget after every phase (blog-team: research/write/edit; global-research: search/write/governance/editorial) and before each revision, via
MAX_WORKFLOW_COST_USD(default0.50in the example compose files) andMAX_WORKFLOW_TOKENS(0= unlimited). On breach the workflow stops gracefully (STOPPED) instead of draining the budget. This is separate from the per-agentMAX_TOKEN_BUDGET.
# Start blog-team BullMQ stack first
docker compose -f examples/blog-team/docker-compose.yml --env-file .env up -d
# Then open the terminal monitor
COMPOSE_FILE=examples/blog-team/docker-compose.yml ./scripts/monitor.shShows in real-time (colour-coded):
- Workflow status transitions
- All 3 agent status changes (IDLE/EXECUTING/DONE/ERROR)
- Task Kanban movements
- BullMQ queue depths (every 5s)
- All container logs with error highlighting
Multiple specialized agents execute concurrently (fan-out), their results are aggregated automatically (fan-in), and an auto-approver validates the combined outcome — no human in the loop.
Orchestrator
│ publish N sub-tasks to shared queue (fan-out)
▼
[Agent-0] [Agent-1] [Agent-2] [Agent-N] ← competing consumers / multi-node
│ │ │ │
└────────┴────────┴────────┘
publish to kaiban-events-completed (fan-in)
│
Aggregator collects
│
Auto-Approver validates
│
kaiban-fanin-approved → ✅ / ❌
import { BullMQDriver } from 'kaiban-distributed';
import { AgentActor } from 'kaiban-distributed';
const REDIS = { connection: { host: 'localhost', port: 6379 } };
const QUEUE = 'research-fan-out';
const AGENT = 'researcher';
// 1. Fan-out: spawn N workers — all share the same queue (competing consumers)
for (let i = 0; i < 4; i++) {
const driver = new BullMQDriver(REDIS);
const actor = new AgentActor(AGENT, driver, QUEUE, async (payload) => {
const result = await myLLM.call(payload.data.instruction);
return result; // published to kaiban-events-completed automatically
});
await actor.start();
}
// 2. Fan-in: collect completions
const completions = new Set<string>();
const collector = new BullMQDriver(REDIS);
await collector.subscribe('kaiban-events-completed', async (p) => {
completions.add(p.taskId);
});
// 3. Publish tasks (fan-out)
const taskIds = ['task-a', 'task-b', 'task-c', 'task-d'];
for (const taskId of taskIds) {
await collector.publish(QUEUE, { taskId, agentId: AGENT, timestamp: Date.now(), data: { instruction: `research ${taskId}` } });
}
// 4. Wait for all completions (fan-in gate)
while (completions.size < taskIds.length) {
await new Promise(r => setTimeout(r, 200));
}
// 5. Auto-approve (no HITL)
const approved = completions.size === taskIds.length;
await collector.publish('kaiban-fanin-approved', {
taskId: 'workflow-1',
agentId: 'approver',
timestamp: Date.now(),
data: { status: approved ? 'approved' : 'rejected', count: completions.size },
});| Property | Behaviour |
|---|---|
| Distribution | BullMQ competing-consumer pattern — each job claimed by exactly one worker |
| Horizontal scale | Add more AgentActor instances (same queue) for more throughput |
| Retry / DLQ | Each actor retries up to 3× before publishing to kaiban-events-failed |
| Exactly-once fan-in | Aggregator uses a Set<taskId> — duplicates are idempotently ignored |
| No HITL required | Auto-approver validates success ratio against a configurable threshold |
See tests/e2e/fan-out-fan-in.test.ts for
full end-to-end scenarios covering:
- Golden Path — 4 agents, all succeed, approver passes
- Scaled 8-node — 8 agents (horizontal fan-out)
- Partial Failure + Retry — 1 flaky agent recovers, workflow approves
- Total Failure — all retries exhausted → DLQ, approver rejects
- Late-joining agent — BullMQ delivers persisted jobs to late consumer
- Duplicate task IDs — aggregator counts each taskId exactly once
- Approver threshold — strict vs lenient ratio comparison
The board/ directory is a standalone React + Vite + TypeScript app that provides
a modern real-time Kanban view of any running kaiban-distributed workflow, with
interactive Human-in-the-Loop Approve / Revise / Reject controls.
- Any kaiban-distributed gateway running (e.g.
./scripts/blog-team.sh start) - Node.js ≥ 20.19 (board uses Vite 8, which needs Node 20.19+/22.12+; gateway needs ≥ 22)
cd board
cp .env.example .env # optional: set VITE_GATEWAY_URL if gateway is not :3000
npm install
npm run dev # → http://localhost:5173The board connects automatically. Within 15 seconds all running agents appear.
http://localhost:5173?gateway=http://remote-gateway.example.com:3000
- Run the blog-team orchestrator as normal (terminal or Docker).
- When Morgan's editorial review completes, a cyan Human-in-the-Loop Review Required banner appears on the board showing the task name.
- Click Approve, Revise, or Reject on the board.
- The orchestrator receives the decision via Redis (
kaiban-hitl-decisions) and continues — no terminal input required.
The terminal prompt ([1] PUBLISH [2] REVISE [3] REJECT) remains active simultaneously; whichever responds first wins.
| Section | Content |
|---|---|
| Header | Logo · topic · gateway URL · workflow status pill · connection badge (LIVE / CONNECTING / OFFLINE) |
| WorkflowBanner | HITL buttons when AWAITING_VALIDATION; FINISHED / STOPPED / ERRORED banners otherwise |
| AgentGrid | One card per agent: name, role, status badge with pulse animation, current task chip |
| KanbanBoard | 5 columns: TODO · DOING · REVIEW · DONE · BLOCKED; tasks move in real-time |
| EconomicsPanel | Total tokens · estimated cost · start/end time · elapsed duration |
| EventLog | Reverse-chronological event stream (WORKFLOW / AGENT / TASK / HITL / CONNECT), capped at 200 |
cd board && npm run build # → board/dist/
# Serve with any static host or CDN
npx serve board/distPoint to a remote gateway at runtime with ?gateway=:
https://your-cdn.example.com/board/?gateway=https://gateway.example.com:3000
cd board && npm run typecheck # tsc --noEmit — strict mode